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,
"semi": true,
"singleQuote": true,
"printWidth": 120,
"printWidth": 85,
"bracketSpacing": true,
"importOrder": ["^react", "^@mui/(.*)$", "^api*/(.*)$", "<THIRD_PARTY_MODULES>", "^[./]"],
"importOrderSeparation": true,

View File

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

View File

@@ -12,7 +12,8 @@
local('Roboto'),
local('Roboto-Regular'),
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,
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,
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, 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;
}

View File

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

View File

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

View File

@@ -1,7 +1,11 @@
import type { FC } from 'react';
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';

View File

@@ -41,9 +41,12 @@ const SignIn: FC = () => {
const [processing, setProcessing] = useState<boolean>(false);
const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>();
const { send: callSignIn, onSuccess } = useRequest((request: SignInRequest) => AuthenticationApi.signIn(request), {
const { send: callSignIn, onSuccess } = useRequest(
(request: SignInRequest) => AuthenticationApi.signIn(request),
{
immediate: false
});
}
);
onSuccess((response) => {
if (response.data) {
@@ -80,7 +83,9 @@ const SignIn: FC = () => {
const submitOnEnter = onEnterCallback(signIn);
const onLocaleSelected: ChangeEventHandler<HTMLInputElement> = async ({ target }) => {
const onLocaleSelected: ChangeEventHandler<HTMLInputElement> = async ({
target
}) => {
const loc = target.value as Locales;
localStorage.setItem('lang', loc);
await loadLocaleAsync(loc);
@@ -110,7 +115,14 @@ const SignIn: FC = () => {
>
<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">
<img src={DEflag} style={{ width: 16, verticalAlign: 'middle' }} />
&nbsp;DE
@@ -182,7 +194,13 @@ const SignIn: FC = () => {
/>
</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 }} />
{LL.SIGN_IN()}
</Button>

View File

@@ -3,5 +3,7 @@ import type { APSettingsType, APStatusType } from 'types';
import { alovaInstance } from './endpoints';
export const readAPStatus = () => alovaInstance.Get<APStatusType>('/rest/apStatus');
export const readAPSettings = () => alovaInstance.Get<APSettingsType>('/rest/apSettings');
export const updateAPSettings = (data: APSettingsType) => alovaInstance.Post<APSettingsType>('/rest/apSettings', data);
export const readAPSettings = () =>
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_SEARCH = 'loginSearch';
export const verifyAuthorization = () => alovaInstance.Get('/rest/verifyAuthorization');
export const signIn = (request: SignInRequest) => alovaInstance.Post<SignInResponse>('/rest/signIn', request);
export const verifyAuthorization = () =>
alovaInstance.Get('/rest/verifyAuthorization');
export const signIn = (request: SignInRequest) =>
alovaInstance.Post<SignInResponse>('/rest/signIn', request);
export function getStorage() {
return localStorage || sessionStorage;

View File

@@ -19,7 +19,8 @@ export const alovaInstance = createAlova({
requestAdapter: xhrRequestAdapter(),
beforeRequest(method) {
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';
export const readMqttStatus = () => alovaInstance.Get<MqttStatusType>('/rest/mqttStatus');
export const readMqttSettings = () => alovaInstance.Get<MqttSettingsType>('/rest/mqttSettings');
export const readMqttStatus = () =>
alovaInstance.Get<MqttStatusType>('/rest/mqttStatus');
export const readMqttSettings = () =>
alovaInstance.Get<MqttSettingsType>('/rest/mqttSettings');
export const updateMqttSettings = (data: MqttSettingsType) =>
alovaInstance.Post<MqttSettingsType>('/rest/mqttSettings', data);

View File

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

View File

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

View File

@@ -8,10 +8,12 @@ import type { ESPSystemStatus, LogSettings, OTASettings, SystemStatus } from 'ty
import { alovaInstance, alovaInstanceGH } from './endpoints';
// 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
export const readSystemStatus = () => alovaInstance.Get<SystemStatus>('/rest/systemStatus');
export const readSystemStatus = () =>
alovaInstance.Get<SystemStatus>('/rest/systemStatus');
// commands
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');
// OTA
export const readOTASettings = () => alovaInstance.Get<OTASettings>(`/rest/otaSettings`);
export const updateOTASettings = (data: OTASettings) => alovaInstance.Post('/rest/otaSettings', data);
export const readOTASettings = () =>
alovaInstance.Get<OTASettings>(`/rest/otaSettings`);
export const updateOTASettings = (data: OTASettings) =>
alovaInstance.Post('/rest/otaSettings', data);
// SystemLog
export const readLogSettings = () => alovaInstance.Get<LogSettings>(`/rest/logSettings`);
export const updateLogSettings = (data: LogSettings) => alovaInstance.Post('/rest/logSettings', data);
export const readLogSettings = () =>
alovaInstance.Get<LogSettings>(`/rest/logSettings`);
export const updateLogSettings = (data: LogSettings) =>
alovaInstance.Post('/rest/logSettings', data);
export const fetchLog = () => alovaInstance.Post('/rest/fetchLog');
export const fetchLogES = () => alovaInstance.Get('/es/log');
@@ -47,6 +53,6 @@ export const uploadFile = (file: File) => {
formData.append('file', file);
return alovaInstance.Post('/rest/uploadFile', formData, {
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 {
constructor(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) {
options.trusted = true;
if (!options.structures && options.useRecords != false) {
@@ -46,7 +47,8 @@ export class Unpackr {
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) {
(options.structures = []).uninitialized = true; // this is what we use to denote an uninitialized structures
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
return saveState(() => {
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)
source = typeof Buffer !== 'undefined' ? Buffer.from(source) : new Uint8Array(source);
source =
typeof Buffer !== 'undefined' ? Buffer.from(source) : new Uint8Array(source);
if (typeof options === 'object') {
srcEnd = options.end || source.length;
position = options.start || 0;
@@ -86,14 +91,21 @@ export class Unpackr {
// new ones
try {
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) {
// if it doesn't have a buffer, maybe it is the wrong type of object
src = null;
if (source instanceof Uint8Array) throw error;
throw new Error(
'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) {
@@ -117,7 +129,9 @@ export class Unpackr {
try {
sequentialMode = true;
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(value) === false) return;
while (position < size) {
@@ -145,9 +159,11 @@ export class Unpackr {
}
_mergeStructures(loadedStructures, existingStructures) {
if (onLoadedStructures) loadedStructures = onLoadedStructures.call(this, loadedStructures);
if (onLoadedStructures)
loadedStructures = onLoadedStructures.call(this, 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++) {
const structure = loadedStructures[i];
if (structure) {
@@ -162,7 +178,8 @@ export class Unpackr {
const existing = existingStructures[id];
if (existing) {
if (structure)
(loadedStructures.restoreStructures || (loadedStructures.restoreStructures = []))[id] = structure;
(loadedStructures.restoreStructures ||
(loadedStructures.restoreStructures = []))[id] = structure;
loadedStructures[id] = existing;
}
}
@@ -181,10 +198,16 @@ export function checkedRead(options: any) {
try {
if (!currentUnpackr.trusted && !sequentialMode) {
const sharedLength = currentStructures.sharedLength || 0;
if (sharedLength < currentStructures.length) currentStructures.length = sharedLength;
if (sharedLength < currentStructures.length)
currentStructures.length = sharedLength;
}
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);
src = null; // dispose of this so that recursive unpack calls don't save state
if (!(options && options.lazy) && result) result = result.toJSON();
@@ -198,7 +221,8 @@ export function checkedRead(options: any) {
if (position == srcEnd) {
// finished reading this source, cleanup references
if (currentStructures && currentStructures.restoreStructures) restoreStructures();
if (currentStructures && currentStructures.restoreStructures)
restoreStructures();
currentStructures = null;
src = null;
if (referenceMap) referenceMap = null;
@@ -208,10 +232,9 @@ export function checkedRead(options: any) {
} else if (!sequentialMode) {
let jsonView;
try {
jsonView = JSON.stringify(result, (_, value) => (typeof value === 'bigint' ? `${value}n` : value)).slice(
0,
100
);
jsonView = JSON.stringify(result, (_, value) =>
typeof value === 'bigint' ? `${value}n` : value
).slice(0, 100);
} catch (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
return result;
} catch (error) {
if (currentStructures && currentStructures.restoreStructures) restoreStructures();
if (currentStructures && currentStructures.restoreStructures)
restoreStructures();
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;
}
throw error;
@@ -243,7 +271,8 @@ export function read() {
if (token < 0x40) return token;
else {
const structure =
currentStructures[token & 0x3f] || (currentUnpackr.getStructures && loadStructures()[token & 0x3f]);
currentStructures[token & 0x3f] ||
(currentUnpackr.getStructures && loadStructures()[token & 0x3f]);
if (structure) {
if (!structure.read) {
structure.read = createStructureReader(structure, token & 0x3f);
@@ -282,7 +311,10 @@ export function read() {
// fixstr
const length = token - 0xa0;
if (srcStringEnd >= position) {
return srcString.slice(position - srcStringStart, (position += length) - srcStringStart);
return srcString.slice(
position - srcStringStart,
(position += length) - srcStringStart
);
}
if (srcStringEnd == 0 && srcEnd < 140) {
// for small blocks, avoiding the overhead of the extract call is helpful
@@ -298,8 +330,16 @@ export function read() {
case 0xc1:
if (bundledStrings) {
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));
else return bundledStrings[0].slice(bundledStrings.position0, (bundledStrings.position0 -= value));
if (value > 0)
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
case 0xc2:
@@ -338,7 +378,8 @@ export function read() {
value = dataView.getFloat32(position);
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
const multiplier = mult10[((src[position] & 0x7f) << 1) | (src[position + 1] >> 7)];
const multiplier =
mult10[((src[position] & 0x7f) << 1) | (src[position + 1] >> 7)];
position += 4;
return ((multiplier * value + (value > 0 ? 0.5 : -0.5)) >> 0) / multiplier;
}
@@ -391,7 +432,8 @@ export function read() {
value = dataView.getBigInt64(position).toString();
} else if (currentUnpackr.int64AsType === 'auto') {
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);
position += 8;
return value;
@@ -433,7 +475,10 @@ export function read() {
// str 8
value = src[position++];
if (srcStringEnd >= position) {
return srcString.slice(position - srcStringStart, (position += value) - srcStringStart);
return srcString.slice(
position - srcStringStart,
(position += value) - srcStringStart
);
}
return readString8(value);
case 0xda:
@@ -441,7 +486,10 @@ export function read() {
value = dataView.getUint16(position);
position += 2;
if (srcStringEnd >= position) {
return srcString.slice(position - srcStringStart, (position += value) - srcStringStart);
return srcString.slice(
position - srcStringStart,
(position += value) - srcStringStart
);
}
return readString16(value);
case 0xdb:
@@ -449,7 +497,10 @@ export function read() {
value = dataView.getUint32(position);
position += 4;
if (srcStringEnd >= position) {
return srcString.slice(position - srcStringStart, (position += value) - srcStringStart);
return srcString.slice(
position - srcStringStart,
(position += value) - srcStringStart
);
}
return readString32(value);
case 0xdc:
@@ -504,7 +555,8 @@ function createStructureReader(structure, firstId) {
.join(',') +
'})}'
)(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
}
const object = {};
@@ -527,7 +579,8 @@ const createSecondByteReader = (firstId, read0) =>
function () {
const highByte = src[position++];
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];
if (!structure) {
throw new Error('Record id is not defined for ' + id);
@@ -542,7 +595,10 @@ export function loadStructures() {
src = null;
return currentUnpackr.getStructures();
});
return (currentStructures = currentUnpackr._mergeStructures(loadedStructures, currentStructures));
return (currentStructures = currentUnpackr._mergeStructures(
loadedStructures,
currentStructures
));
}
var readFixedString = readStringJS;
@@ -563,7 +619,11 @@ export function setExtractor(extractStrings) {
if (string == null) {
if (bundledStrings) return readStringJS(length);
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') {
string = extraction;
strings = EMPTY_ARRAY;
@@ -593,7 +653,8 @@ function readStringJS(length) {
if (length < 16) {
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 units = [];
result = '';
@@ -616,7 +677,8 @@ function readStringJS(length) {
const byte2 = src[position++] & 0x3f;
const byte3 = 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) {
unit -= 0x10000;
units.push(((unit >>> 10) & 0x3ff) | 0xd800);
@@ -810,7 +872,8 @@ function shortStringInJS(length) {
position -= 14;
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++];
if ((o & 0x80) > 0) {
position -= 15;
@@ -862,14 +925,17 @@ function readExt(length) {
const type = src[position++];
if (currentExtensions[type]) {
let end;
return currentExtensions[type](src.subarray(position, (end = position += length)), (readPosition) => {
return currentExtensions[type](
src.subarray(position, (end = position += length)),
(readPosition) => {
position = readPosition;
try {
return read();
} finally {
position = end;
}
});
}
);
} else throw new Error('Unknown extension type ' + type);
}
@@ -881,14 +947,20 @@ function readKey() {
length = length - 0xa0;
if (srcStringEnd >= position)
// 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 {
// not cacheable, go back and do a standard read
position--;
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 checkPosition = position;
let end = position + length - 3;
@@ -947,7 +1019,8 @@ const recordDefinition = (id, highByte) => {
}
const existingStructure = currentStructures[id];
if (existingStructure && existingStructure.isShared) {
(currentStructures.restoreStructures || (currentStructures.restoreStructures = []))[id] = existingStructure;
(currentStructures.restoreStructures ||
(currentStructures.restoreStructures = []))[id] = existingStructure;
}
currentStructures[id] = structure;
structure.read = createStructureReader(structure, firstByte);
@@ -1009,7 +1082,8 @@ export const typedArrays = [
currentExtensions[0x74] = (data) => {
const typeCode = data[0];
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
return new glbl[typedArrayName](Uint8Array.prototype.slice.call(data, 1).buffer);
};
@@ -1033,11 +1107,20 @@ currentExtensions[0x62] = (data) => {
currentExtensions[0xff] = (data) => {
// 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)
return new Date(
((data[0] << 22) + (data[1] << 14) + (data[2] << 6) + (data[3] >> 2)) / 1000000 +
((data[3] & 0x3) * 0x100000000 + data[4] * 0x1000000 + (data[5] << 16) + (data[6] << 8) + data[7]) * 1000
((data[0] << 22) + (data[1] << 14) + (data[2] << 6) + (data[3] >> 2)) /
1000000 +
((data[3] & 0x3) * 0x100000000 +
data[4] * 0x1000000 +
(data[5] << 16) +
(data[6] << 8) +
data[7]) *
1000
);
else if (data.length == 12)
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 savedStructures = currentStructures;
const savedStructuresContents = currentStructures.slice(0, currentStructures.length);
const savedStructuresContents = currentStructures.slice(
0,
currentStructures.length
);
const savedPackr = currentUnpackr;
const savedSequentialMode = sequentialMode;
const value = callback();
@@ -1122,7 +1208,10 @@ const u8Array = new Uint8Array(f32Array.buffer, 0, 4);
export function roundFloat32(float32Number) {
f32Array[0] = float32Number;
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) {
readStruct = updatedReadStruct;

View File

@@ -14,27 +14,44 @@ export interface MessageBoxProps extends BoxProps {
message: string;
}
const LEVEL_ICONS: { [type in MessageBoxLevel]: React.ComponentType<SvgIconProps> } = {
const LEVEL_ICONS: {
[type in MessageBoxLevel]: React.ComponentType<SvgIconProps>;
} = {
success: CheckCircleOutlineOutlinedIcon,
info: InfoOutlinedIcon,
warning: ReportProblemOutlinedIcon,
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,
info: (theme: Theme) => theme.palette.info.main,
warning: (theme: Theme) => theme.palette.warning.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 Icon = LEVEL_ICONS[level];
const backgroundColor = LEVEL_BACKGROUNDS[level](theme);
const color = 'white';
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 />
<Typography sx={{ ml: 2, flexGrow: 1 }} variant="body1">
{message}

View File

@@ -14,7 +14,16 @@ const SectionContent: FC<SectionContentProps> = (props) => {
return (
<Paper id={id} sx={{ p: 2, m: 2 }}>
{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}
</Paper>

View File

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

View File

@@ -12,9 +12,14 @@ interface ValidatedFieldProps {
export type ValidatedTextFieldProps = ValidatedFieldProps & TextFieldProps;
const ValidatedTextField: FC<ValidatedTextFieldProps> = ({ fieldErrors, ...rest }) => {
const ValidatedTextField: FC<ValidatedTextFieldProps> = ({
fieldErrors,
...rest
}) => {
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 (
<>
<TextField error={!!errors} {...rest} />

View File

@@ -21,7 +21,12 @@ const LayoutAppBar: FC<LayoutAppBarProps> = ({ title, onToggleDrawer }) => (
}}
>
<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 />
</IconButton>
<Typography variant="h6" noWrap component="div">

View File

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

View File

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

View File

@@ -2,7 +2,14 @@ import type { FC } from 'react';
import { Link } from 'react-router-dom';
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';
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 ? (
<ListItem
disablePadding
secondaryAction={
<ListItemIcon style={{ justifyContent: 'right', color: 'lightblue', verticalAlign: 'middle' }}>
<ListItemIcon
style={{
justifyContent: 'right',
color: 'lightblue',
verticalAlign: 'middle'
}}
>
<NavigateNextIcon />
</ListItemIcon>
}
>
<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>
</ListItem>
) : (

View File

@@ -22,7 +22,13 @@ const ApplicationError: FC<ApplicationErrorProps> = ({ message }) => (
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" />
<Box ml={2}>
<Typography variant="h4">Application Error</Typography>

View File

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

View File

@@ -13,7 +13,14 @@ const LoadingSpinner: FC<LoadingSpinnerProps> = ({ height = '100%' }) => {
const { LL } = useI18nContext();
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
sx={(theme: Theme) => ({
margin: theme.spacing(4),

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -13,7 +13,15 @@ import SettingsBackupRestoreIcon from '@mui/icons-material/SettingsBackupRestore
import SettingsEthernetIcon from '@mui/icons-material/SettingsEthernet';
import SettingsInputAntennaIcon from '@mui/icons-material/SettingsInputAntenna';
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';
@@ -92,7 +100,11 @@ const Settings: FC = () => {
};
const renderRestartDialog = () => (
<Dialog sx={dialogStyle} open={confirmRestart} onClose={() => setConfirmRestart(false)}>
<Dialog
sx={dialogStyle}
open={confirmRestart}
onClose={() => setConfirmRestart(false)}
>
<DialogTitle>{LL.RESTART()}</DialogTitle>
<DialogContent dividers>{LL.RESTART_CONFIRM()}</DialogContent>
<DialogActions>
@@ -128,7 +140,11 @@ const Settings: FC = () => {
);
const renderFactoryResetDialog = () => (
<Dialog sx={dialogStyle} open={confirmFactoryReset} onClose={() => setConfirmFactoryReset(false)}>
<Dialog
sx={dialogStyle}
open={confirmFactoryReset}
onClose={() => setConfirmFactoryReset(false)}
>
<DialogTitle>{LL.FACTORY_RESET()}</DialogTitle>
<DialogContent dividers>{LL.SYSTEM_FACTORY_TEXT_DIALOG()}</DialogContent>
<DialogActions>
@@ -189,9 +205,26 @@ const Settings: FC = () => {
to="ntp"
/>
<ListMenuItem icon={DeviceHubIcon} 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
icon={DeviceHubIcon}
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
icon={MemoryIcon}
@@ -242,7 +275,9 @@ const Settings: FC = () => {
</>
);
return <SectionContent>{restarting ? <RestartMonitor /> : content()}</SectionContent>;
return (
<SectionContent>{restarting ? <RestartMonitor /> : content()}</SectionContent>
);
};
export default Settings;

View File

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

View File

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

View File

@@ -3,7 +3,15 @@ import type { FC } from 'react';
import CancelIcon from '@mui/icons-material/Cancel';
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';
@@ -43,7 +51,12 @@ const MqttSettings: FC = () => {
const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>();
const updateFormValue = updateValueDirty(origData, dirtyFlags, setDirtyFlags, updateDataValue);
const updateFormValue = updateValueDirty(
origData,
dirtyFlags,
setDirtyFlags,
updateDataValue
);
const content = () => {
if (!data) {
@@ -63,10 +76,22 @@ const MqttSettings: FC = () => {
return (
<>
<BlockFormControlLabel
control={<Checkbox name="enabled" checked={data.enabled} onChange={updateFormValue} />}
control={
<Checkbox
name="enabled"
checked={data.enabled}
onChange={updateFormValue}
/>
}
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}>
<ValidatedTextField
fieldErrors={fieldErrors}
@@ -144,7 +169,9 @@ const MqttSettings: FC = () => {
name="keep_alive"
label="Keep Alive"
InputProps={{
endAdornment: <InputAdornment position="end">{LL.SECONDS()}</InputAdornment>
endAdornment: (
<InputAdornment position="end">{LL.SECONDS()}</InputAdornment>
)
}}
fullWidth
variant="outlined"
@@ -173,7 +200,13 @@ const MqttSettings: FC = () => {
</Grid>
{data.enableTLS !== undefined && (
<BlockFormControlLabel
control={<Checkbox name="enableTLS" checked={data.enableTLS} onChange={updateFormValue} />}
control={
<Checkbox
name="enableTLS"
checked={data.enableTLS}
onChange={updateFormValue}
/>
}
label={LL.ENABLE_TLS()}
/>
)}
@@ -190,11 +223,23 @@ const MqttSettings: FC = () => {
)}
<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()}
/>
<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()}
/>
@@ -215,7 +260,13 @@ const MqttSettings: FC = () => {
<MenuItem value={2}>{LL.MQTT_NEST_2()}</MenuItem>
</TextField>
<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()}
/>
{!data.ha_enabled && (
@@ -229,7 +280,13 @@ const MqttSettings: FC = () => {
>
<Grid item>
<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()}
/>
</Grid>
@@ -237,7 +294,11 @@ const MqttSettings: FC = () => {
<Grid item>
<BlockFormControlLabel
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()}
/>
@@ -246,10 +307,22 @@ const MqttSettings: FC = () => {
</Grid>
)}
{!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>
<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()}
/>
</Grid>
@@ -312,14 +385,22 @@ const MqttSettings: FC = () => {
<Typography sx={{ pt: 2 }} variant="h6" color="primary">
{LL.MQTT_PUBLISH_INTERVALS()}&nbsp;(0=auto)
</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}>
<ValidatedTextField
fieldErrors={fieldErrors}
name="publish_time_heartbeat"
label="Heartbeat"
InputProps={{
endAdornment: <InputAdornment position="end">{LL.SECONDS()}</InputAdornment>
endAdornment: (
<InputAdornment position="end">{LL.SECONDS()}</InputAdornment>
)
}}
fullWidth
variant="outlined"
@@ -334,7 +415,9 @@ const MqttSettings: FC = () => {
name="publish_time_boiler"
label={LL.MQTT_INT_BOILER()}
InputProps={{
endAdornment: <InputAdornment position="end">{LL.SECONDS()}</InputAdornment>
endAdornment: (
<InputAdornment position="end">{LL.SECONDS()}</InputAdornment>
)
}}
fullWidth
variant="outlined"
@@ -349,7 +432,9 @@ const MqttSettings: FC = () => {
name="publish_time_thermostat"
label={LL.MQTT_INT_THERMOSTATS()}
InputProps={{
endAdornment: <InputAdornment position="end">{LL.SECONDS()}</InputAdornment>
endAdornment: (
<InputAdornment position="end">{LL.SECONDS()}</InputAdornment>
)
}}
fullWidth
variant="outlined"
@@ -364,7 +449,9 @@ const MqttSettings: FC = () => {
name="publish_time_solar"
label={LL.MQTT_INT_SOLAR()}
InputProps={{
endAdornment: <InputAdornment position="end">{LL.SECONDS()}</InputAdornment>
endAdornment: (
<InputAdornment position="end">{LL.SECONDS()}</InputAdornment>
)
}}
fullWidth
variant="outlined"
@@ -379,7 +466,9 @@ const MqttSettings: FC = () => {
name="publish_time_mixer"
label={LL.MQTT_INT_MIXER()}
InputProps={{
endAdornment: <InputAdornment position="end">{LL.SECONDS()}</InputAdornment>
endAdornment: (
<InputAdornment position="end">{LL.SECONDS()}</InputAdornment>
)
}}
fullWidth
variant="outlined"
@@ -394,7 +483,9 @@ const MqttSettings: FC = () => {
name="publish_time_water"
label={LL.MQTT_INT_WATER()}
InputProps={{
endAdornment: <InputAdornment position="end">{LL.SECONDS()}</InputAdornment>
endAdornment: (
<InputAdornment position="end">{LL.SECONDS()}</InputAdornment>
)
}}
fullWidth
variant="outlined"
@@ -409,7 +500,9 @@ const MqttSettings: FC = () => {
name="publish_time_sensor"
label={LL.TEMP_SENSORS()}
InputProps={{
endAdornment: <InputAdornment position="end">{LL.SECONDS()}</InputAdornment>
endAdornment: (
<InputAdornment position="end">{LL.SECONDS()}</InputAdornment>
)
}}
fullWidth
variant="outlined"
@@ -423,7 +516,9 @@ const MqttSettings: FC = () => {
<TextField
name="publish_time_other"
InputProps={{
endAdornment: <InputAdornment position="end">{LL.SECONDS()}</InputAdornment>
endAdornment: (
<InputAdornment position="end">{LL.SECONDS()}</InputAdornment>
)
}}
label={LL.DEFAULT(0)}
fullWidth

View File

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

View File

@@ -99,7 +99,12 @@ const NetworkSettings: FC = () => {
}
}, [initialized, setInitialized, data, selectedNetwork]);
const updateFormValue = updateValueDirty(origData, dirtyFlags, setDirtyFlags, updateDataValue);
const updateFormValue = updateValueDirty(
origData,
dirtyFlags,
setDirtyFlags,
updateDataValue
);
const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>();
@@ -142,7 +147,9 @@ const NetworkSettings: FC = () => {
<List>
<ListItem>
<ListItemAvatar>
<Avatar>{isNetworkOpen(selectedNetwork) ? <LockOpenIcon /> : <LockIcon />}</Avatar>
<Avatar>
{isNetworkOpen(selectedNetwork) ? <LockOpenIcon /> : <LockIcon />}
</Avatar>
</ListItemAvatar>
<ListItemText
primary={selectedNetwork.ssid}
@@ -220,11 +227,23 @@ const NetworkSettings: FC = () => {
<MenuItem value={8}>2 dBm</MenuItem>
</TextField>
<BlockFormControlLabel
control={<Checkbox name="nosleep" checked={data.nosleep} onChange={updateFormValue} />}
control={
<Checkbox
name="nosleep"
checked={data.nosleep}
onChange={updateFormValue}
/>
}
label={LL.NETWORK_DISABLE_SLEEP()}
/>
<BlockFormControlLabel
control={<Checkbox name="bandwidth20" checked={data.bandwidth20} onChange={updateFormValue} />}
control={
<Checkbox
name="bandwidth20"
checked={data.bandwidth20}
onChange={updateFormValue}
/>
}
label={LL.NETWORK_LOW_BAND()}
/>
<Typography sx={{ pt: 2 }} variant="h6" color="primary">
@@ -241,11 +260,23 @@ const NetworkSettings: FC = () => {
margin="normal"
/>
<BlockFormControlLabel
control={<Checkbox name="enableMDNS" checked={data.enableMDNS} onChange={updateFormValue} />}
control={
<Checkbox
name="enableMDNS"
checked={data.enableMDNS}
onChange={updateFormValue}
/>
}
label={LL.NETWORK_USE_DNS()}
/>
<BlockFormControlLabel
control={<Checkbox name="enableCORS" checked={data.enableCORS} onChange={updateFormValue} />}
control={
<Checkbox
name="enableCORS"
checked={data.enableCORS}
onChange={updateFormValue}
/>
}
label={LL.NETWORK_ENABLE_CORS()}
/>
{data.enableCORS && (
@@ -261,12 +292,24 @@ const NetworkSettings: FC = () => {
)}
{data.enableIPv6 !== undefined && (
<BlockFormControlLabel
control={<Checkbox name="enableIPv6" checked={data.enableIPv6} onChange={updateFormValue} />}
control={
<Checkbox
name="enableIPv6"
checked={data.enableIPv6}
onChange={updateFormValue}
/>
}
label={LL.NETWORK_ENABLE_IPV6()}
/>
)}
<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()}
/>
{data.static_ip_config && (
@@ -325,13 +368,19 @@ const NetworkSettings: FC = () => {
)}
{restartNeeded && (
<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()}
</Button>
</MessageBox>
)}
{!restartNeeded && (selectedNetwork || (dirtyFlags && dirtyFlags.length !== 0)) && (
{!restartNeeded &&
(selectedNetwork || (dirtyFlags && dirtyFlags.length !== 0)) && (
<ButtonRow>
<Button
startIcon={<CancelIcon />}

View File

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

View File

@@ -9,4 +9,6 @@ export interface 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 [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 {
data: networkList,
send: getNetworkList,
@@ -51,7 +53,9 @@ const WiFiNetworkScanner: FC = () => {
const renderNetworkScanner = () => {
if (!networkList) {
return <FormLoader message={LL.SCANNING() + '...'} errorMessage={errorMessage} />;
return (
<FormLoader message={LL.SCANNING() + '...'} errorMessage={errorMessage} />
);
}
return <WiFiNetworkSelector networkList={networkList} />;
};

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -14,9 +14,23 @@ import { Box, Button, IconButton } from '@mui/material';
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 { BlockNavigation, ButtonRow, FormLoader, MessageBox, SectionContent } from 'components';
import {
BlockNavigation,
ButtonRow,
FormLoader,
MessageBox,
SectionContent
} from 'components';
import { AuthenticatedContext } from 'contexts/authentication';
import { useI18nContext } from 'i18n/i18n-react';
import type { SecuritySettingsType, UserType } from 'types';
@@ -27,7 +41,8 @@ import GenerateToken from './GenerateToken';
import User from './User';
const ManageUsers: FC = () => {
const { loadData, saveData, saving, data, updateDataValue, errorMessage } = useRest<SecuritySettingsType>({
const { loadData, saveData, saving, data, updateDataValue, errorMessage } =
useRest<SecuritySettingsType>({
read: SecurityApi.readSecuritySettings,
update: SecurityApi.updateSecuritySettings
});
@@ -114,7 +129,12 @@ const ManageUsers: FC = () => {
const doneEditingUser = () => {
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 });
setUser(undefined);
setChanged(changed + 1);
@@ -148,11 +168,18 @@ const ManageUsers: FC = () => {
}
// 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 (
<>
<Table data={{ nodes: user_table }} theme={table_theme} layout={{ custom: true }}>
<Table
data={{ nodes: user_table }}
theme={table_theme}
layout={{ custom: true }}
>
{(tableList: UserType2[]) => (
<>
<Header>
@@ -189,7 +216,9 @@ const ManageUsers: FC = () => {
)}
</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 flexGrow={1} sx={{ '& button': { mt: 2 } }}>
@@ -221,7 +250,12 @@ const ManageUsers: FC = () => {
<Box flexWrap="nowrap" whiteSpace="nowrap">
<ButtonRow>
<Button startIcon={<PersonAddIcon />} variant="outlined" color="secondary" onClick={createUser}>
<Button
startIcon={<PersonAddIcon />}
variant="outlined"
color="secondary"
onClick={createUser}
>
{LL.ADD(0)}
</Button>
</ButtonRow>

View File

@@ -8,7 +8,14 @@ import { Button } from '@mui/material';
import * as SecurityApi from 'api/security';
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 { useI18nContext } from 'i18n/i18n-react';
import type { SecuritySettingsType } from 'types';
@@ -37,7 +44,12 @@ const SecuritySettings: FC = () => {
const authenticatedContext = useContext(AuthenticatedContext);
const updateFormValue = updateValueDirty(origData, dirtyFlags, setDirtyFlags, updateDataValue);
const updateFormValue = updateValueDirty(
origData,
dirtyFlags,
setDirtyFlags,
updateDataValue
);
const content = () => {
if (!data) {

View File

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

View File

@@ -8,7 +8,16 @@ import MemoryIcon from '@mui/icons-material/Memory';
import RefreshIcon from '@mui/icons-material/Refresh';
import SdCardAlertIcon from '@mui/icons-material/SdCardAlert';
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';
@@ -25,7 +34,11 @@ const ESPSystemStatus: FC = () => {
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 = () => {
if (!data) {
@@ -41,7 +54,10 @@ const ESPSystemStatus: FC = () => {
<DevicesIcon />
</Avatar>
</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>
<Divider variant="inset" component="li" />
<ListItem>
@@ -75,7 +91,12 @@ const ESPSystemStatus: FC = () => {
</ListItemAvatar>
<ListItemText
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>
{data.psram_size !== undefined && data.free_psram !== undefined && (
@@ -89,7 +110,12 @@ const ESPSystemStatus: FC = () => {
</ListItemAvatar>
<ListItemText
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>
</>
@@ -104,7 +130,10 @@ const ESPSystemStatus: FC = () => {
<ListItemText
primary={LL.FLASH()}
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>
@@ -118,7 +147,12 @@ const ESPSystemStatus: FC = () => {
<ListItemText
primary={LL.APPSIZE()}
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>
@@ -131,7 +165,12 @@ const ESPSystemStatus: FC = () => {
</ListItemAvatar>
<ListItemText
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>
<Divider variant="inset" component="li" />
@@ -139,7 +178,12 @@ const ESPSystemStatus: FC = () => {
<Box display="flex" flexWrap="wrap">
<Box flexGrow={1} sx={{ '& button': { mt: 2 } }}>
<ButtonRow>
<Button startIcon={<RefreshIcon />} variant="outlined" color="secondary" onClick={loadData}>
<Button
startIcon={<RefreshIcon />}
variant="outlined"
color="secondary"
onClick={loadData}
>
{LL.REFRESH()}
</Button>
</ButtonRow>

View File

@@ -36,7 +36,12 @@ const RestartMonitor: FC = () => {
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;

View File

@@ -24,7 +24,11 @@ const System: FC = () => {
<RouterTabs value={routerTab}>
<Tab value="status" label={LL.STATUS_OF('')} />
<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>
<Routes>
<Route path="status" element={<SystemStatus />} />

View File

@@ -4,14 +4,28 @@ import { toast } from 'react-toastify';
import DownloadIcon from '@mui/icons-material/GetApp';
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 { fetchLogES } from 'api/system';
import { useSSE } from '@alova/scene-react';
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 type { LogEntry, LogSettings } from 'types';
import { LogLevel } from 'types';
@@ -25,8 +39,10 @@ const LogEntryLine = styled('div')(() => ({
whiteSpace: 'nowrap'
}));
const topOffset = () => document.getElementById('log-window')?.getBoundingClientRect().bottom || 0;
const leftOffset = () => document.getElementById('log-window')?.getBoundingClientRect().left || 0;
const topOffset = () =>
document.getElementById('log-window')?.getBoundingClientRect().bottom || 0;
const leftOffset = () =>
document.getElementById('log-window')?.getBoundingClientRect().left || 0;
const levelLabel = (level: LogLevel) => {
switch (level) {
@@ -50,8 +66,17 @@ const SystemLog: FC = () => {
useLayoutTitle(LL.LOG_OF(''));
const { loadData, data, updateDataValue, origData, dirtyFlags, setDirtyFlags, blocker, saveData, errorMessage } =
useRest<LogSettings>({
const {
loadData,
data,
updateDataValue,
origData,
dirtyFlags,
setDirtyFlags,
blocker,
saveData,
errorMessage
} = useRest<LogSettings>({
read: SystemApi.readLogSettings,
update: SystemApi.updateLogSettings
});
@@ -59,7 +84,12 @@ const SystemLog: FC = () => {
const [logEntries, setLogEntries] = useState<LogEntry[]>([]);
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
const { onMessage, onError } = useSSE(fetchLogES, {
@@ -102,10 +132,14 @@ const SystemLog: FC = () => {
const onDownload = () => {
let result = '';
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');
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');
document.body.appendChild(a);
a.click();
@@ -134,7 +168,13 @@ const SystemLog: FC = () => {
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}>
<TextField
name="level"
@@ -173,7 +213,13 @@ const SystemLog: FC = () => {
</Grid>
<Grid item>
<BlockFormControlLabel
control={<Checkbox checked={data.compact} onChange={updateFormValue} name="compact" />}
control={
<Checkbox
checked={data.compact}
onChange={updateFormValue}
name="compact"
/>
}
label={LL.COMPACT()}
/>
</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()}
</Button>
{dirtyFlags && dirtyFlags.length !== 0 && (

View File

@@ -49,7 +49,11 @@ const SystemStatus: FC = () => {
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, {
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 () => {
await scanDevices()
@@ -148,14 +153,28 @@ const SystemStatus: FC = () => {
};
const renderScanDialog = () => (
<Dialog sx={dialogStyle} open={confirmScan} onClose={() => setConfirmScan(false)}>
<Dialog
sx={dialogStyle}
open={confirmScan}
onClose={() => setConfirmScan(false)}
>
<DialogTitle>{LL.SCAN_DEVICES()}</DialogTitle>
<DialogContent dividers>{LL.EMS_SCAN()}</DialogContent>
<DialogActions>
<Button startIcon={<CancelIcon />} variant="outlined" onClick={() => setConfirmScan(false)} color="secondary">
<Button
startIcon={<CancelIcon />}
variant="outlined"
onClick={() => setConfirmScan(false)}
color="secondary"
>
{LL.CANCEL()}
</Button>
<Button startIcon={<PermScanWifiIcon />} variant="outlined" onClick={scan} color="primary">
<Button
startIcon={<PermScanWifiIcon />}
variant="outlined"
onClick={scan}
color="primary"
>
{LL.SCAN()}
</Button>
</DialogActions>
@@ -282,7 +301,12 @@ const SystemStatus: FC = () => {
{renderScanDialog()}
<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()}
</Button>
</Box>

View File

@@ -8,7 +8,12 @@ import * as SystemApi from 'api/system';
import * as EMSESP from 'project/api';
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 type { APIcall } from 'project/types';
@@ -19,23 +24,40 @@ const UploadDownload: FC = () => {
const [restarting, setRestarting] = useState<boolean>();
const [md5, setMd5] = useState<string>();
const { send: getSettings, onSuccess: onSuccessGetSettings } = useRequest(EMSESP.getSettings(), {
const { send: getSettings, onSuccess: onSuccessGetSettings } = useRequest(
EMSESP.getSettings(),
{
immediate: false
}
);
const { send: getCustomizations, onSuccess: onSuccessGetCustomizations } =
useRequest(EMSESP.getCustomizations(), {
immediate: false
});
const { send: getCustomizations, onSuccess: onSuccessGetCustomizations } = useRequest(EMSESP.getCustomizations(), {
const { send: getEntities, onSuccess: onSuccessGetEntities } = useRequest(
EMSESP.getEntities(),
{
immediate: false
});
const { send: getEntities, onSuccess: onSuccessGetEntities } = useRequest(EMSESP.getEntities(), {
}
);
const { send: getSchedule, onSuccess: onSuccessGetSchedule } = useRequest(
EMSESP.getSchedule(),
{
immediate: false
});
const { send: getSchedule, onSuccess: onSuccessGetSchedule } = useRequest(EMSESP.getSchedule(), {
}
);
const { send: getAPI, onSuccess: onGetAPI } = useRequest(
(data: APIcall) => EMSESP.API(data),
{
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, {
immediate: true,
@@ -50,11 +72,17 @@ const UploadDownload: FC = () => {
const STABLE_URL = 'https://github.com/emsesp/EMS-ESP32/releases/download/';
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 DEV_RELNOTES_URL = 'https://github.com/emsesp/EMS-ESP32/blob/dev/CHANGELOG_LATEST.md';
const STABLE_RELNOTES_URL =
'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) =>
'EMS-ESP-' + v.replaceAll('.', '_') + '-' + data.esp_platform.replaceAll('-', '_') + '.bin';
'EMS-ESP-' +
v.replaceAll('.', '_') +
'-' +
data.esp_platform.replaceAll('-', '_') +
'.bin';
const {
loading: isUploading,
@@ -115,8 +143,11 @@ const UploadDownload: FC = () => {
saveFile(event.data, 'schedule.json');
});
onGetAPI((event) => {
saveFile(
event.data,
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
saveFile(event.data, event.sendArgs[0].device + '_' + event.sendArgs[0].entity + '.txt');
event.sendArgs[0].device + '_' + event.sendArgs[0].entity + '.txt'
);
});
const downloadSettings = async () => {
@@ -170,7 +201,8 @@ const UploadDownload: FC = () => {
<b>{data.emsesp_version}</b>&nbsp;({data.esp_platform})
{latestVersion && (
<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;(
<Link target="_blank" href={STABLE_RELNOTES_URL} color="primary">
{LL.RELEASE_NOTES()}
@@ -178,7 +210,13 @@ const UploadDownload: FC = () => {
)&nbsp;(
<Link
target="_blank"
href={STABLE_URL + 'v' + latestVersion + '/' + getBinURL(latestVersion as string)}
href={
STABLE_URL +
'v' +
latestVersion +
'/' +
getBinURL(latestVersion as string)
}
color="primary"
>
{LL.DOWNLOAD(1)}
@@ -188,14 +226,19 @@ const UploadDownload: FC = () => {
)}
{latestDevVersion && (
<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>
&nbsp;(
<Link target="_blank" href={DEV_RELNOTES_URL} color="primary">
{LL.RELEASE_NOTES()}
</Link>
)&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)}
</Link>
)
@@ -219,7 +262,12 @@ const UploadDownload: FC = () => {
<Typography variant="body2">{'MD5: ' + md5}</Typography>
</Box>
)}
<SingleUpload onDrop={startUpload} onCancel={cancelUpload} isUploading={isUploading} progress={progress} />
<SingleUpload
onDrop={startUpload}
onCancel={cancelUpload}
isUploading={isUploading}
progress={progress}
/>
{!isUploading && (
<>
<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;

View File

@@ -1,7 +1,5 @@
import type { Translation } from '../i18n-types';
/* prettier-ignore */
const de: Translation = {
LANGUAGE: 'Sprache',
RETRY: 'Neuer Versuch',
@@ -208,7 +206,8 @@ const de: Translation = {
USER_WARNING: 'Sie müssen mindestens einen Admin-Nutzer konfigurieren',
ADD: 'Hinzufügen',
ACCESS_TOKEN_FOR: 'Zugangs-Token für',
ACCESS_TOKEN_TEXT: 'Dieses Token ist für REST API Aufrufe bestimmt, die eine Authentifizierung benötigen. Es kann entweder als Bearer Token im `Authorization-Header` oder in der Access_Token URL verwendet werden.',
ACCESS_TOKEN_TEXT:
'Dieses Token ist für REST API Aufrufe bestimmt, die eine Authentifizierung benötigen. Es kann entweder als Bearer Token im `Authorization-Header` oder in der Access_Token URL verwendet werden.',
GENERATING_TOKEN: 'Erzeuge Token',
USER: 'Nutzer',
MODIFY: 'Ändern',

View File

@@ -1,7 +1,5 @@
import type { Translation } from '../i18n-types';
/* prettier-ignore */
const en: Translation = {
LANGUAGE: 'Language',
RETRY: 'Retry',
@@ -208,7 +206,8 @@ const en: Translation = {
USER_WARNING: 'You must have at least one admin user configured',
ADD: 'Add',
ACCESS_TOKEN_FOR: 'Access Token for',
ACCESS_TOKEN_TEXT: 'The token below is used with REST API calls that require authorization. It can be passed either as a Bearer token in the Authorization header or in the access_token URL query parameter.',
ACCESS_TOKEN_TEXT:
'The token below is used with REST API calls that require authorization. It can be passed either as a Bearer token in the Authorization header or in the access_token URL query parameter.',
GENERATING_TOKEN: 'Generating token',
USER: 'User',
MODIFY: 'Modify',

View File

@@ -1,7 +1,5 @@
import type { Translation } from '../i18n-types';
/* prettier-ignore */
const fr: Translation = {
LANGUAGE: 'Langue',
RETRY: 'Réessayer',
@@ -9,7 +7,7 @@ const fr: Translation = {
IS_REQUIRED: '{0} est requis',
SIGN_IN: 'Se connecter',
SIGN_OUT: 'Se déconnecter',
USERNAME: 'Nom d\'utilisateur',
USERNAME: "Nom d'utilisateur",
PASSWORD: 'Mot de passe',
SU_PASSWORD: 'Mot de passe su',
SETTINGS_OF: 'Paramètres {0}',
@@ -28,13 +26,13 @@ const fr: Translation = {
ENTITIES: 'Entités',
REFRESH: 'Rafraîchir',
EXPORT: 'Exporter',
DEVICE_DETAILS: 'Détails de l\'appareil',
DEVICE_DETAILS: "Détails de l'appareil",
ID_OF: 'ID {0}',
DEVICE: 'Appareil',
PRODUCT: 'Produit',
VERSION: 'Version',
BRAND: 'Marque',
ENTITY_NAME: 'Nom de l\'entité',
ENTITY_NAME: "Nom de l'entité",
VALUE: 'Valeur',
DEVICES: 'Appareils',
SENSORS: 'Capteurs',
@@ -88,7 +86,7 @@ const fr: Translation = {
'Lectures capteurs de température',
'Lectures capteurs analogiques',
'Publications MQTT',
'Appels à l\'API',
"Appels à l'API",
'Messages Syslog'
],
NUM_DEVICES: '{num} Appareil{{s}}',
@@ -98,11 +96,11 @@ const fr: Translation = {
NUM_SECONDS: '{num} seconde{{s}}',
NUM_HOURS: '{num} heure{{s}}',
NUM_MINUTES: '{num} minute{{s}}',
APPLICATION_SETTINGS: 'Paramètres de l\'application',
APPLICATION_SETTINGS: "Paramètres de l'application",
CUSTOMIZATIONS: 'Personnalisation',
APPLICATION_RESTARTING: 'EMS-ESP redémarre',
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',
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: 'Profil de carte',
CUSTOM: 'Personnalisé',
GPIO_OF: 'GPIO {0}',
@@ -119,14 +117,14 @@ const fr: Translation = {
ENABLE_TELNET: 'Activer la console Telnet',
ENABLE_ANALOG: 'Activer les capteurs analogiques',
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)',
UNDERCLOCK_CPU: 'Underclock du CPU',
HEATINGOFF: 'Start boiler with forced heating off', // TODO translate
ENABLE_SHOWER_TIMER: 'Activer la minuterie de la douche',
ENABLE_SHOWER_ALERT: 'Activer les alertes de durée de douche',
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',
BOOLEAN_FORMAT_DASHBOARD: 'Tableau de bord du format booléen',
BOOLEAN_FORMAT_API: 'Format booléen API/MQTT',
@@ -150,8 +148,8 @@ const fr: Translation = {
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_2: 'marquer comme favori',
CUSTOMIZATIONS_HELP_3: 'désactiver l\'action d\'écriture',
CUSTOMIZATIONS_HELP_4: 'exclure de MQTT et de l\'API',
CUSTOMIZATIONS_HELP_3: "désactiver l'action d'écriture",
CUSTOMIZATIONS_HELP_4: "exclure de MQTT et de l'API",
CUSTOMIZATIONS_HELP_5: 'cacher du Tableau de bord',
CUSTOMIZATIONS_HELP_6: 'remove from memory', // TODO translate
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_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_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 !',
UPLOAD: 'Upload',
DOWNLOAD: '{{D|d|d}}ownload',
@@ -178,8 +176,8 @@ const fr: Translation = {
CLOSE: 'Fermer',
USE: 'Utiliser',
FACTORY_RESET: 'Réinitialisation',
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: "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 ?",
THE_LATEST: 'La dernière',
OFFICIAL: 'officielle',
DEVELOPMENT: 'développement',
@@ -195,10 +193,12 @@ const fr: Translation = {
BUFFER_SIZE: 'Max taille du buffer',
COMPACT: 'Compact',
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_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.',
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)',
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.",
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',
UPLOAD_DROP_TEXT: 'Déposer le fichier ou cliquer ici',
ERROR: 'Erreur inattendue, veuillez réessayer',
@@ -207,12 +207,13 @@ const fr: Translation = {
IS_ADMIN: 'admin',
USER_WARNING: 'Vous devez avoir au moins un utilisateur admin configuré',
ADD: 'Ajouter',
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_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.",
GENERATING_TOKEN: 'Génération de jeton',
USER: 'Utilisateur',
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é',
ERRORS_OF: 'Erreurs {0}',
DISCONNECT_REASON: 'Raison de la déconnexion',
@@ -240,7 +241,7 @@ const fr: Translation = {
MQTT_QUEUE: 'Queue MQTT',
DEFAULT: 'Défaut',
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_2: 'Multiple instances, short name', // TODO translate
MQTT_CLEAN_SESSION: 'Flag Clean Session',
@@ -248,15 +249,15 @@ const fr: Translation = {
INACTIVE: 'Inactif',
ACTIVE: 'Actif',
UNKNOWN: 'Inconnu',
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: "Définir l'heure",
SET_TIME_TEXT: "Entrer la date et l'heure locale ci-dessous pour régler l'heure",
LOCAL_TIME: 'Heure locale',
UTC_TIME: 'Heure UTC',
ENABLE_NTP: 'Activer le NTP',
NTP_SERVER: 'Serveur NTP',
TIME_ZONE: 'Fuseau horaire',
ACCESS_POINT: 'Point d\'accès',
AP_PROVIDE: 'Activer le Point d\'Accès',
ACCESS_POINT: "Point d'accès",
AP_PROVIDE: "Activer le Point d'Accès",
AP_PROVIDE_TEXT_1: 'toujours',
AP_PROVIDE_TEXT_2: 'quand le WiFi est déconnecté',
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_BSSID: 'leave blank to use only SSID', // TODO translate
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_LOW_BAND: 'Utiliser une bande passante WiFi plus faible',
NETWORK_USE_DNS: 'Activer le service mDNS',
NETWORK_ENABLE_CORS: 'Activer 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_GATEWAY: 'Passerelle',
NETWORK_SUBNET: 'Masque de sous-réseau',

View File

@@ -1,7 +1,5 @@
import type { Translation } from '../i18n-types';
/* prettier-ignore */
const it: Translation = {
LANGUAGE: 'Lingua',
RETRY: 'Riprovare',
@@ -102,7 +100,8 @@ const it: Translation = {
CUSTOMIZATIONS: 'Personalizzazione',
APPLICATION_RESTARTING: 'EMS-ESP sta riavviando',
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',
CUSTOM: 'Personalizzazione',
GPIO_OF: 'GPIO {0}',
@@ -197,8 +196,10 @@ const it: Translation = {
ENABLE_OTA: 'Abilita aggiornamenti OTA',
DOWNLOAD_CUSTOMIZATION_TEXT: 'Scarica personalizzazioni entità',
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',
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" ',
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',
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',
UPLOAD_DROP_TEXT: 'Trascina il file o clicca qui',
ERROR: 'Errore Inaspettato, prego tenta ancora',
@@ -208,7 +209,8 @@ const it: Translation = {
USER_WARNING: 'Devi avere configurato almeno un utente amministratore',
ADD: 'Aggiungi',
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',
USER: 'Utente',
MODIFY: 'Modifica',

View File

@@ -1,7 +1,5 @@
import type { Translation } from '../i18n-types';
/* prettier-ignore */
const nl: Translation = {
LANGUAGE: 'Taal',
RETRY: 'Opnieuw proberen',
@@ -208,7 +206,8 @@ const nl: Translation = {
USER_WARNING: 'U dient tenminste 1 admin gebruiker te configureren',
ADD: 'Toevoegen',
ACCESS_TOKEN_FOR: 'Access Token voor',
ACCESS_TOKEN_TEXT: 'Het token hieronder wordt gebruikt voor de REST API calls die authorisatie nodig hebben. Het kan zowel als Bearer token in de Authorization header of in acccess_token URL query parameter gebruikt worden',
ACCESS_TOKEN_TEXT:
'Het token hieronder wordt gebruikt voor de REST API calls die authorisatie nodig hebben. Het kan zowel als Bearer token in de Authorization header of in acccess_token URL query parameter gebruikt worden',
GENERATING_TOKEN: 'Token aan het genereren',
USER: 'Gebruiker',
MODIFY: 'Aanpassen',

View File

@@ -1,7 +1,5 @@
import type { Translation } from '../i18n-types';
/* prettier-ignore */
const no: Translation = {
LANGUAGE: 'Språk',
RETRY: 'Forsøk igjen',
@@ -208,7 +206,8 @@ const no: Translation = {
USER_WARNING: 'Du må ha minst en admin bruker konfigurert',
ADD: 'Legg til',
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',
USER: 'Bruker',
MODIFY: 'Endre',

View File

@@ -1,7 +1,5 @@
import type { BaseTranslation } from '../i18n-types';
/* prettier-ignore */
const pl: BaseTranslation = {
LANGUAGE: 'Język',
RETRY: 'Ponów',

View File

@@ -1,7 +1,5 @@
import type { Translation } from '../i18n-types';
/* prettier-ignore */
const sk: Translation = {
LANGUAGE: 'Jazyk',
RETRY: 'Opakovať',
@@ -209,7 +207,8 @@ const sk: Translation = {
USER_WARNING: 'Musíte mať nakonfigurovaného aspoň jedného používateľa administrátora',
ADD: 'Pridať',
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',
USER: 'Užívateľ',
MODIFY: 'Upraviť',

View File

@@ -1,7 +1,5 @@
import type { Translation } from '../i18n-types';
/* prettier-ignore */
const sv: Translation = {
LANGUAGE: 'Språk',
RETRY: 'Försök igen',
@@ -208,7 +206,8 @@ const sv: Translation = {
USER_WARNING: 'Du måste ha minst en admin konfigurerad',
ADD: 'Lägg till',
ACCESS_TOKEN_FOR: 'Access Token för',
ACCESS_TOKEN_TEXT: 'Nedan Token används med REST API-anrop som kräver auktorisering. Den kan skickas med antingen som en Bearer token i Authorization-headern eller i access_token URL query-parametern.',
ACCESS_TOKEN_TEXT:
'Nedan Token används med REST API-anrop som kräver auktorisering. Den kan skickas med antingen som en Bearer token i Authorization-headern eller i access_token URL query-parametern.',
GENERATING_TOKEN: 'Genererar token',
USER: 'Användare',
MODIFY: 'Ändra',

View File

@@ -1,7 +1,5 @@
import type { Translation } from '../i18n-types';
/* prettier-ignore */
const tr: Translation = {
LANGUAGE: 'Dil',
RETRY: 'Tekrar Dene',
@@ -208,7 +206,8 @@ const tr: Translation = {
USER_WARNING: 'En az bir yönetici kullanıcısı ayarlamanız gerekmektedir',
ADD: 'Ekle',
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',
USER: 'Kullanıcı',
MODIFY: 'Düzenle',

View File

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

View File

@@ -5,7 +5,17 @@ import { toast } from 'react-toastify';
import CancelIcon from '@mui/icons-material/Cancel';
import PowerSettingsNewIcon from '@mui/icons-material/PowerSettingsNew';
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';
@@ -61,7 +71,12 @@ const ApplicationSettings: FC = () => {
const { LL } = useI18nContext();
const updateFormValue = updateValueDirty(origData, dirtyFlags, setDirtyFlags, updateDataValue);
const updateFormValue = updateValueDirty(
origData,
dirtyFlags,
setDirtyFlags,
updateDataValue
);
const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>();
@@ -220,7 +235,9 @@ const ApplicationSettings: FC = () => {
<ValidatedTextField
fieldErrors={fieldErrors}
name="dallas_gpio"
label={LL.GPIO_OF(LL.TEMPERATURE()) + ' (0=' + LL.DISABLED(1) + ')'}
label={
LL.GPIO_OF(LL.TEMPERATURE()) + ' (0=' + LL.DISABLED(1) + ')'
}
fullWidth
variant="outlined"
value={numberValue(data.dallas_gpio)}
@@ -322,7 +339,13 @@ const ApplicationSettings: FC = () => {
<Typography sx={{ pt: 2 }} variant="h6" color="primary">
{LL.SETTINGS_OF(LL.EMS_BUS(0))}
</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}>
<TextField
name="tx_mode"
@@ -396,54 +419,120 @@ const ApplicationSettings: FC = () => {
</Grid>
{data.led_gpio !== 0 && (
<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()}
disabled={saving}
/>
)}
<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()}
disabled={saving}
/>
<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()}
disabled={saving}
/>
<BlockFormControlLabel
control={<Checkbox checked={data.fahrenheit} onChange={updateFormValue} name="fahrenheit" />}
control={
<Checkbox
checked={data.fahrenheit}
onChange={updateFormValue}
name="fahrenheit"
/>
}
label={LL.CONVERT_FAHRENHEIT()}
disabled={saving}
/>
<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()}
disabled={saving}
/>
<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()}
disabled={saving}
/>
<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()}
disabled={saving}
/>
<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()}
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
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()}
disabled={saving}
/>
<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()}
disabled={!data.shower_timer}
/>
@@ -465,7 +554,9 @@ const ApplicationSettings: FC = () => {
name="shower_alert_trigger"
label={LL.TRIGGER_TIME()}
InputProps={{
endAdornment: <InputAdornment position="end">{LL.MINUTES()}</InputAdornment>
endAdornment: (
<InputAdornment position="end">{LL.MINUTES()}</InputAdornment>
)
}}
variant="outlined"
value={numberValue(data.shower_alert_trigger)}
@@ -481,7 +572,9 @@ const ApplicationSettings: FC = () => {
name="shower_alert_coldshot"
label={LL.COLD_SHOT_DURATION()}
InputProps={{
endAdornment: <InputAdornment position="end">{LL.SECONDS()}</InputAdornment>
endAdornment: (
<InputAdornment position="end">{LL.SECONDS()}</InputAdornment>
)
}}
variant="outlined"
value={numberValue(data.shower_alert_coldshot)}
@@ -497,7 +590,13 @@ const ApplicationSettings: FC = () => {
<Typography sx={{ pt: 3 }} variant="h6" color="primary">
{LL.FORMATTING_OPTIONS()}
</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}>
<TextField
name="bool_dashboard"
@@ -556,7 +655,13 @@ const ApplicationSettings: FC = () => {
{LL.TEMP_SENSORS()}
</Typography>
<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()}
disabled={saving}
/>
@@ -566,7 +671,13 @@ const ApplicationSettings: FC = () => {
{LL.LOGGING()}
</Typography>
<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()}
disabled={saving}
/>
@@ -582,7 +693,13 @@ const ApplicationSettings: FC = () => {
label={LL.ENABLE_SYSLOG()}
/>
{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}>
<ValidatedTextField
fieldErrors={fieldErrors}
@@ -636,7 +753,9 @@ const ApplicationSettings: FC = () => {
name="syslog_mark_interval"
label={LL.MARK_INTERVAL()}
InputProps={{
endAdornment: <InputAdornment position="end">{LL.SECONDS()}</InputAdornment>
endAdornment: (
<InputAdornment position="end">{LL.SECONDS()}</InputAdornment>
)
}}
fullWidth
variant="outlined"
@@ -651,7 +770,12 @@ const ApplicationSettings: FC = () => {
)}
{restartNeeded && (
<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()}
</Button>
</MessageBox>

View File

@@ -10,10 +10,24 @@ import RefreshIcon from '@mui/icons-material/Refresh';
import WarningIcon from '@mui/icons-material/Warning';
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 { 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 * as EMSESP from './api';
@@ -171,8 +185,13 @@ const CustomEntities: FC = () => {
updateState('entities', (data: EntityItem[]) => {
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);
return new_data;
});
@@ -201,7 +220,8 @@ const CustomEntities: FC = () => {
return value === undefined
? ''
: 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);
}
@@ -215,7 +235,11 @@ const CustomEntities: FC = () => {
}
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[]) => (
<>
<Header>
@@ -233,12 +257,18 @@ const CustomEntities: FC = () => {
<Row key={ei.name} item={ei} onClick={() => editEntityItem(ei)}>
<Cell>
{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>{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 ? '' : 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>
</Row>
))}
@@ -273,7 +303,12 @@ const CustomEntities: FC = () => {
<Box flexGrow={1}>
{numChanges > 0 && (
<ButtonRow>
<Button startIcon={<CancelIcon />} variant="outlined" onClick={onDialogCancel} color="secondary">
<Button
startIcon={<CancelIcon />}
variant="outlined"
onClick={onDialogCancel}
color="secondary"
>
{LL.CANCEL()}
</Button>
<Button
@@ -289,10 +324,20 @@ const CustomEntities: FC = () => {
</Box>
<Box flexWrap="nowrap" whiteSpace="nowrap">
<ButtonRow>
<Button startIcon={<RefreshIcon />} variant="outlined" color="secondary" onClick={fetchEntities}>
<Button
startIcon={<RefreshIcon />}
variant="outlined"
color="secondary"
onClick={fetchEntities}
>
{LL.REFRESH()}
</Button>
<Button startIcon={<AddIcon />} variant="outlined" color="primary" onClick={addEntityItem}>
<Button
startIcon={<AddIcon />}
variant="outlined"
color="primary"
onClick={addEntityItem}
>
{LL.ADD(0)}
</Button>
</ButtonRow>

View File

@@ -142,7 +142,13 @@ const CustomEntitiesDialog = ({
<>
<Grid item xs={4} mt={3}>
<BlockFormControlLabel
control={<Checkbox checked={editItem.writeable} onChange={updateFormValue} name="writeable" />}
control={
<Checkbox
checked={editItem.writeable}
onChange={updateFormValue}
name="writeable"
/>
}
label={LL.WRITEABLE()}
/>
</Grid>
@@ -157,7 +163,11 @@ const CustomEntitiesDialog = ({
value={editItem.device_id as string}
onChange={updateFormValue}
inputProps={{ style: { textTransform: 'uppercase' } }}
InputProps={{ startAdornment: <InputAdornment position="start">0x</InputAdornment> }}
InputProps={{
startAdornment: (
<InputAdornment position="start">0x</InputAdornment>
)
}}
/>
</Grid>
<Grid item xs={4}>
@@ -170,7 +180,11 @@ const CustomEntitiesDialog = ({
value={editItem.type_id}
onChange={updateFormValue}
inputProps={{ style: { textTransform: 'uppercase' } }}
InputProps={{ startAdornment: <InputAdornment position="start">0x</InputAdornment> }}
InputProps={{
startAdornment: (
<InputAdornment position="start">0x</InputAdornment>
)
}}
/>
</Grid>
<Grid item xs={4}>
@@ -207,7 +221,8 @@ const CustomEntitiesDialog = ({
</TextField>
</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
@@ -241,7 +256,8 @@ const CustomEntitiesDialog = ({
</Grid>
</>
)}
{editItem.value_type === DeviceValueType.STRING && editItem.device_id !== '0' && (
{editItem.value_type === DeviceValueType.STRING &&
editItem.device_id !== '0' && (
<Grid item xs={4}>
<TextField
name="factor"
@@ -264,15 +280,30 @@ const CustomEntitiesDialog = ({
<DialogActions>
{!creating && (
<Box flexGrow={1}>
<Button startIcon={<RemoveIcon />} variant="outlined" color="warning" onClick={remove}>
<Button
startIcon={<RemoveIcon />}
variant="outlined"
color="warning"
onClick={remove}
>
{LL.REMOVE()}
</Button>
</Box>
)}
<Button startIcon={<CancelIcon />} variant="outlined" onClick={close} color="secondary">
<Button
startIcon={<CancelIcon />}
variant="outlined"
onClick={close}
color="secondary"
>
{LL.CANCEL()}
</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()}
</Button>
</DialogActions>

View File

@@ -27,11 +27,25 @@ import {
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 { dialogStyle } from 'CustomTheme';
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 { useI18nContext } from 'i18n/i18n-react';
@@ -63,7 +77,9 @@ const Customization: FC = () => {
// fetch devices first
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 { send: resetCustomizations } = useRequest(EMSESP.resetCustomizations(), {
@@ -71,7 +87,8 @@ const Customization: FC = () => {
});
const { send: writeCustomizationEntities } = useRequest(
(data: { id: number; entity_ids: string[] }) => EMSESP.writeCustomizationEntities(data),
(data: { id: number; entity_ids: string[] }) =>
EMSESP.writeCustomizationEntities(data),
{
immediate: false
}
@@ -86,7 +103,15 @@ const Customization: FC = () => {
);
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) => {
@@ -166,7 +191,12 @@ const Customization: FC = () => {
});
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(() => {
@@ -221,8 +251,11 @@ const Customization: FC = () => {
}
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) +
(withShortname ? ' ' + de.id : '');
(de.n && de.n[0] === '!'
? LL.COMMAND(1) + ': ' + de.n.slice(1)
: de.cn && de.cn !== ''
? de.cn
: de.n) + (withShortname ? ' ' + de.id : '');
const getMaskNumber = (newMask: string[]) => {
let new_mask = 0;
@@ -253,7 +286,8 @@ const Customization: FC = () => {
};
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) => {
setDeviceEntities(
@@ -262,8 +296,14 @@ const Customization: FC = () => {
return {
...de,
m: set
? de.m | (DeviceEntityMask.DV_API_MQTT_EXCLUDE | DeviceEntityMask.DV_WEB_EXCLUDE)
: de.m & ~(DeviceEntityMask.DV_API_MQTT_EXCLUDE | DeviceEntityMask.DV_WEB_EXCLUDE)
? de.m |
(DeviceEntityMask.DV_API_MQTT_EXCLUDE |
DeviceEntityMask.DV_WEB_EXCLUDE)
: de.m &
~(
DeviceEntityMask.DV_API_MQTT_EXCLUDE |
DeviceEntityMask.DV_WEB_EXCLUDE
)
};
} else {
return de;
@@ -288,7 +328,11 @@ const Customization: FC = () => {
};
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) => {
@@ -330,7 +374,10 @@ const Customization: FC = () => {
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') {
setRestartNeeded(true);
} else {
@@ -376,14 +423,26 @@ const Customization: FC = () => {
<>
<Box color="warning.main">
<Typography variant="body2" mt={1}>
<OptionIcon type="favorite" isSet={true} />={LL.CUSTOMIZATIONS_HELP_2()}&nbsp;&nbsp;
<OptionIcon type="readonly" isSet={true} />={LL.CUSTOMIZATIONS_HELP_3()}&nbsp;&nbsp;
<OptionIcon type="api_mqtt_exclude" isSet={true} />={LL.CUSTOMIZATIONS_HELP_4()}&nbsp;&nbsp;
<OptionIcon type="web_exclude" isSet={true} />={LL.CUSTOMIZATIONS_HELP_5()}&nbsp;&nbsp;
<OptionIcon type="favorite" isSet={true} />={LL.CUSTOMIZATIONS_HELP_2()}
&nbsp;&nbsp;
<OptionIcon type="readonly" isSet={true} />={LL.CUSTOMIZATIONS_HELP_3()}
&nbsp;&nbsp;
<OptionIcon type="api_mqtt_exclude" isSet={true} />=
{LL.CUSTOMIZATIONS_HELP_4()}&nbsp;&nbsp;
<OptionIcon type="web_exclude" isSet={true} />=
{LL.CUSTOMIZATIONS_HELP_5()}&nbsp;&nbsp;
<OptionIcon type="deleted" isSet={true} />={LL.CUSTOMIZATIONS_HELP_6()}
</Typography>
</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}>
<TextField
size="small"
@@ -455,11 +514,16 @@ const Customization: FC = () => {
</Grid>
<Grid item>
<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>
</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[]) => (
<>
<Header>
@@ -479,13 +543,20 @@ const Customization: FC = () => {
</Cell>
<Cell>
{formatName(de, false)}&nbsp;(
<Link target="_blank" href={APIURL + selectedDeviceName + '/' + de.id}>
<Link
target="_blank"
href={APIURL + selectedDeviceName + '/' + de.id}
>
{de.id}
</Link>
)
</Cell>
<Cell>{!(de.m & DeviceEntityMask.DV_READONLY) && formatValue(de.mi)}</Cell>
<Cell>{!(de.m & DeviceEntityMask.DV_READONLY) && formatValue(de.ma)}</Cell>
<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>
</Row>
))}
@@ -498,14 +569,28 @@ const Customization: FC = () => {
};
const renderResetDialog = () => (
<Dialog sx={dialogStyle} open={confirmReset} onClose={() => setConfirmReset(false)}>
<Dialog
sx={dialogStyle}
open={confirmReset}
onClose={() => setConfirmReset(false)}
>
<DialogTitle>{LL.RESET(1)}</DialogTitle>
<DialogContent dividers>{LL.CUSTOMIZATIONS_RESET()}</DialogContent>
<DialogActions>
<Button startIcon={<CancelIcon />} variant="outlined" onClick={() => setConfirmReset(false)} color="secondary">
<Button
startIcon={<CancelIcon />}
variant="outlined"
onClick={() => setConfirmReset(false)}
color="secondary"
>
{LL.CANCEL()}
</Button>
<Button startIcon={<SettingsBackupRestoreIcon />} variant="outlined" onClick={resetCustomization} color="error">
<Button
startIcon={<SettingsBackupRestoreIcon />}
variant="outlined"
onClick={resetCustomization}
color="error"
>
{LL.RESET(0)}
</Button>
</DialogActions>
@@ -518,7 +603,12 @@ const Customization: FC = () => {
{deviceEntities && renderDeviceData()}
{restartNeeded && (
<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()}
</Button>
</MessageBox>

View File

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

View File

@@ -3,7 +3,12 @@ import { AiOutlineAlert, AiOutlineControl, AiOutlineGateway } from 'react-icons/
import { CgSmartHomeBoiler } from 'react-icons/cg';
import { FaSolarPanel } from 'react-icons/fa';
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 { VscVmConnect } from 'react-icons/vsc';
@@ -47,7 +52,11 @@ const DeviceIcon: FC<DeviceIconProps> = ({ type_id }) => {
case DeviceType.POOL:
return <MdOutlinePool />;
case DeviceType.CUSTOM:
return <PlaylistAddIcon sx={{ color: 'lightblue', fontSize: 22, verticalAlign: 'middle' }} />;
return (
<PlaylistAddIcon
sx={{ color: 'lightblue', fontSize: 22, verticalAlign: 'middle' }}
/>
);
default:
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 { IconContext } from 'react-icons';
import { useNavigate } from 'react-router-dom';
@@ -35,7 +41,15 @@ import {
import { useRowSelect } from '@table-library/react-table-library/select';
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 type { Action, State } from '@table-library/react-table-library/types/common';
import { dialogStyle } from 'CustomTheme';
@@ -67,19 +81,25 @@ const Devices: FC = () => {
useLayoutTitle(LL.DEVICES());
const { data: coreData, send: readCoreData } = useRequest(() => EMSESP.readCoreData(), {
const { data: coreData, send: readCoreData } = useRequest(
() => EMSESP.readCoreData(),
{
initialData: {
connected: true,
devices: []
}
});
}
);
const { data: deviceData, send: readDeviceData } = useRequest((id: number) => EMSESP.readDeviceData(id), {
const { data: deviceData, send: readDeviceData } = useRequest(
(id: number) => EMSESP.readDeviceData(id),
{
initialData: {
data: []
},
immediate: false
});
}
);
const { loading: submitting, send: writeDeviceValue } = useRequest(
(data: { id: number; c: string; v: unknown }) => EMSESP.writeDeviceValue(data),
@@ -235,9 +255,14 @@ const Devices: FC = () => {
},
sortToggleType: SortToggleType.AlternateWithReset,
sortFns: {
NAME: (array) => array.sort((a, b) => a.id.toString().slice(2).localeCompare(b.id.toString().slice(2))),
// eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call
VALUE: (array) => array.sort((a, b) => a.v.toString().localeCompare(b.v.toString()))
NAME: (array) =>
array.sort((a, b) =>
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 === '""') {
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;
};
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 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) {
return;
}
const filename = coreData.devices[deviceIndex].tn + '_' + coreData.devices[deviceIndex].n;
const filename =
coreData.devices[deviceIndex].tn + '_' + coreData.devices[deviceIndex].n;
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)
},
{ 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()
},
{
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'
}
];
@@ -341,10 +390,13 @@ const Devices: FC = () => {
(csvString: string, rowItem: DeviceValue) =>
csvString +
columns
.map(({ accessor }: { accessor: (dv: DeviceValue) => unknown }) => escapeCsvCell(accessor(rowItem) as string))
.map(({ accessor }: { accessor: (dv: DeviceValue) => unknown }) =>
escapeCsvCell(accessor(rowItem) as string)
)
.join(';') +
'\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' });
@@ -381,45 +433,76 @@ const Devices: FC = () => {
const renderDeviceDetails = () => {
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) {
return;
}
return (
<Dialog sx={dialogStyle} open={showDeviceInfo} onClose={() => setShowDeviceInfo(false)}>
<Dialog
sx={dialogStyle}
open={showDeviceInfo}
onClose={() => setShowDeviceInfo(false)}
>
<DialogTitle>{LL.DEVICE_DETAILS()}</DialogTitle>
<DialogContent dividers>
<List dense={true}>
<ListItem>
<ListItemText primary={LL.TYPE(0)} secondary={coreData.devices[deviceIndex].tn} />
<ListItemText
primary={LL.TYPE(0)}
secondary={coreData.devices[deviceIndex].tn}
/>
</ListItem>
<ListItem>
<ListItemText primary={LL.NAME(0)} secondary={coreData.devices[deviceIndex].n} />
<ListItemText
primary={LL.NAME(0)}
secondary={coreData.devices[deviceIndex].n}
/>
</ListItem>
{coreData.devices[deviceIndex].t !== DeviceType.CUSTOM && (
<>
<ListItem>
<ListItemText primary={LL.BRAND()} secondary={coreData.devices[deviceIndex].b} />
<ListItemText
primary={LL.BRAND()}
secondary={coreData.devices[deviceIndex].b}
/>
</ListItem>
<ListItem>
<ListItemText
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>
<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>
<ListItemText primary={LL.VERSION()} secondary={coreData.devices[deviceIndex].v} />
<ListItemText
primary={LL.VERSION()}
secondary={coreData.devices[deviceIndex].v}
/>
</ListItem>
</>
)}
</List>
</DialogContent>
<DialogActions>
<Button variant="outlined" onClick={() => setShowDeviceInfo(false)} color="secondary">
<Button
variant="outlined"
onClick={() => setShowDeviceInfo(false)}
color="secondary"
>
{LL.CLOSE()}
</Button>
</DialogActions>
@@ -429,11 +512,24 @@ const Devices: FC = () => {
};
const renderCoreData = () => (
<IconContext.Provider value={{ color: 'lightblue', size: '18', style: { verticalAlign: 'middle' } }}>
{!coreData.connected && <MessageBox my={2} level="error" message={LL.EMS_BUS_WARNING()} />}
<IconContext.Provider
value={{
color: 'lightblue',
size: '18',
style: { verticalAlign: 'middle' }
}}
>
{!coreData.connected && (
<MessageBox my={2} level="error" message={LL.EMS_BUS_WARNING()} />
)}
{coreData.connected && (
<Table data={{ nodes: coreData.devices }} select={device_select} theme={device_theme} layout={{ custom: true }}>
<Table
data={{ nodes: coreData.devices }}
select={device_select}
theme={device_theme}
layout={{ custom: true }}
>
{(tableList: Device[]) => (
<>
<Header>
@@ -451,7 +547,9 @@ const Devices: FC = () => {
</Cell>
<Cell>
{device.n}
<span style={{ color: 'lightblue' }}>&nbsp;&nbsp;({device.e})</span>
<span style={{ color: 'lightblue' }}>
&nbsp;&nbsp;({device.e})
</span>
</Cell>
<Cell stiff>{device.tn}</Cell>
</Row>
@@ -481,8 +579,12 @@ const Devices: FC = () => {
const renderNameCell = (dv: DeviceValue) => (
<>
{dv.id.slice(2)}&nbsp;
{hasMask(dv.id, DeviceEntityMask.DV_FAVORITE) && <StarIcon color="primary" sx={{ fontSize: 12 }} />}
{hasMask(dv.id, DeviceEntityMask.DV_READONLY) && <EditOffOutlinedIcon color="primary" sx={{ fontSize: 12 }} />}
{hasMask(dv.id, DeviceEntityMask.DV_FAVORITE) && (
<StarIcon color="primary" sx={{ fontSize: 12 }} />
)}
{hasMask(dv.id, DeviceEntityMask.DV_READONLY) && (
<EditOffOutlinedIcon color="primary" sx={{ fontSize: 12 }} />
)}
{hasMask(dv.id, DeviceEntityMask.DV_API_MQTT_EXCLUDE) && (
<CommentsDisabledOutlinedIcon color="primary" sx={{ fontSize: 12 }} />
)}
@@ -493,7 +595,9 @@ const Devices: FC = () => {
? deviceData.data.filter((dv) => hasMask(dv.id, DeviceEntityMask.DV_FAVORITE))
: 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) {
return;
}
@@ -514,7 +618,8 @@ const Devices: FC = () => {
>
<Box sx={{ border: '1px solid #177ac9' }}>
<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>
<Grid container justifyContent="space-between">
@@ -527,30 +632,50 @@ const Devices: FC = () => {
' ' +
LL.ENTITIES(shown_data.length)}
<IconButton onClick={() => setShowDeviceInfo(true)}>
<InfoOutlinedIcon color="primary" sx={{ fontSize: 18, verticalAlign: 'middle' }} />
<InfoOutlinedIcon
color="primary"
sx={{ fontSize: 18, verticalAlign: 'middle' }}
/>
</IconButton>
{me.admin && (
<IconButton onClick={customize}>
<FormatListNumberedIcon sx={{ fontSize: 18, verticalAlign: 'middle' }} />
<FormatListNumberedIcon
sx={{ fontSize: 18, verticalAlign: 'middle' }}
/>
</IconButton>
)}
<IconButton onClick={handleDownloadCsv}>
<DownloadIcon color="primary" sx={{ fontSize: 18, verticalAlign: 'middle' }} />
<DownloadIcon
color="primary"
sx={{ fontSize: 18, verticalAlign: 'middle' }}
/>
</IconButton>
<IconButton onClick={() => setOnlyFav(!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 onClick={refreshData}>
<RefreshIcon color="primary" sx={{ fontSize: 18, verticalAlign: 'middle' }} />
<RefreshIcon
color="primary"
sx={{ fontSize: 18, verticalAlign: 'middle' }}
/>
</IconButton>
</Typography>
<Grid item zeroMinWidth justifyContent="flex-end">
<IconButton onClick={resetDeviceSelect}>
<HighlightOffIcon color="primary" sx={{ fontSize: 18, verticalAlign: 'middle' }} />
<HighlightOffIcon
color="primary"
sx={{ fontSize: 18, verticalAlign: 'middle' }}
/>
</IconButton>
</Grid>
</Grid>
@@ -595,8 +720,13 @@ const Devices: FC = () => {
<Cell>{renderNameCell(dv)}</Cell>
<Cell>{formatValue(LL, dv.v, dv.u)}</Cell>
<Cell stiff>
{me.admin && dv.c && !hasMask(dv.id, DeviceEntityMask.DV_READONLY) && (
<IconButton size="small" onClick={() => showDeviceValue(dv)}>
{me.admin &&
dv.c &&
!hasMask(dv.id, DeviceEntityMask.DV_READONLY) && (
<IconButton
size="small"
onClick={() => showDeviceValue(dv)}
>
{dv.v === '' && dv.c ? (
<PlayArrowIcon color="primary" sx={{ fontSize: 16 }} />
) : (
@@ -627,14 +757,20 @@ const Devices: FC = () => {
onSave={deviceValueDialogSave}
selectedItem={selectedDeviceValue}
writeable={
selectedDeviceValue.c !== undefined && !hasMask(selectedDeviceValue.id, DeviceEntityMask.DV_READONLY)
selectedDeviceValue.c !== undefined &&
!hasMask(selectedDeviceValue.id, DeviceEntityMask.DV_READONLY)
}
validator={deviceValueItemValidation(selectedDeviceValue)}
progress={submitting}
/>
)}
<ButtonRow mt={1}>
<Button startIcon={<RefreshIcon />} variant="outlined" color="secondary" onClick={refreshData}>
<Button
startIcon={<RefreshIcon />}
variant="outlined"
color="secondary"
onClick={refreshData}
>
{LL.REFRESH()}
</Button>
</ButtonRow>

View File

@@ -102,7 +102,11 @@ const DevicesDialog = ({
return (
<Dialog sx={dialogStyle} open={open} onClose={close}>
<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>
<DialogContent dividers>
<Box color="warning.main" p={0} pl={0} pr={0} mt={0} mb={2}>
@@ -138,9 +142,17 @@ const DevicesDialog = ({
type="number"
sx={{ width: '30ch' }}
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={{
startAdornment: <InputAdornment position="start">{setUom(editItem.u)}</InputAdornment>
startAdornment: (
<InputAdornment position="start">
{setUom(editItem.u)}
</InputAdornment>
)
}}
/>
) : (
@@ -175,10 +187,20 @@ const DevicesDialog = ({
position: 'relative'
}}
>
<Button startIcon={<CancelIcon />} variant="outlined" onClick={close} color="secondary">
<Button
startIcon={<CancelIcon />}
variant="outlined"
onClick={close}
color="secondary"
>
{LL.CANCEL()}
</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()}
</Button>
{progress && (

View File

@@ -55,25 +55,46 @@ const EntityMaskToggle = ({ onUpdate, de }: EntityMaskToggleProps) => {
}}
>
<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 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 value="2" disabled={de.n === '' || (de.m & 0x80) !== 0}>
<OptionIcon
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 value="1" disabled={de.n === undefined || (de.m & 0x80) !== 0}>
<OptionIcon
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 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>
</ToggleButtonGroup>
);

View File

@@ -29,9 +29,12 @@ const Help: FC = () => {
const { LL } = useI18nContext();
useLayoutTitle(LL.HELP_OF(''));
const { send: getAPI, onSuccess: onGetAPI } = useRequest((data: APIcall) => EMSESP.API(data), {
const { send: getAPI, onSuccess: onGetAPI } = useRequest(
(data: APIcall) => EMSESP.API(data),
{
immediate: false
});
}
);
onGetAPI((event) => {
const anchor = document.createElement('a');
@@ -40,8 +43,10 @@ const Help: FC = () => {
type: 'text/plain'
})
);
anchor.download =
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
anchor.download = 'emsesp_' + event.sendArgs[0].device + '_' + event.sendArgs[0].entity + '.txt';
'emsesp_' + event.sendArgs[0].device + '_' + event.sendArgs[0].entity + '.txt';
anchor.click();
URL.revokeObjectURL(anchor.href);
toast.info(LL.DOWNLOAD_SUCCESSFUL());
@@ -79,7 +84,10 @@ const Help: FC = () => {
</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>
<Avatar sx={{ bgcolor: '#72caf9' }}>
<GitHubIcon />
@@ -119,7 +127,11 @@ const Help: FC = () => {
<b>{LL.HELP_INFORMATION_5()}</b>
</Typography>
<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'}
</Link>
</Typography>

View File

@@ -12,9 +12,19 @@ import VisibilityOffOutlinedIcon from '@mui/icons-material/VisibilityOffOutlined
import VisibilityOutlinedIcon from '@mui/icons-material/VisibilityOutlined';
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],
readonly: [EditOffOutlinedIcon, EditOutlinedIcon],
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 { 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 { 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 * as EMSESP from './api';
@@ -39,9 +53,12 @@ const Scheduler: FC = () => {
force: true
});
const { send: writeSchedule } = useRequest((data: Schedule) => EMSESP.writeSchedule(data), {
const { send: writeSchedule } = useRequest(
(data: Schedule) => EMSESP.writeSchedule(data),
{
immediate: false
});
}
);
function hasScheduleChanged(si: ScheduleItem) {
return (
@@ -57,7 +74,10 @@ const Scheduler: FC = () => {
}
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 dd = day < 10 ? `0${day}` : day;
return new Date(`2017-01-${dd}T00:00:00+00:00`);
@@ -157,8 +177,13 @@ const Scheduler: FC = () => {
updateState('schedule', (data: ScheduleItem[]) => {
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);
@@ -189,8 +214,13 @@ const Scheduler: FC = () => {
const dayBox = (si: ScheduleItem, flag: number) => (
<>
<Box>
<Typography 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
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>
</Box>
<Divider orientation="vertical" flexItem />
@@ -201,7 +231,11 @@ const Scheduler: FC = () => {
return (
<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}
layout={{ custom: true }}
>
@@ -222,9 +256,15 @@ const Scheduler: FC = () => {
<Row key={si.id} item={si} onClick={() => editScheduleItem(si)}>
<Cell stiff>
{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 stiff>
@@ -277,7 +317,12 @@ const Scheduler: FC = () => {
<Box flexGrow={1}>
{numChanges !== 0 && (
<ButtonRow>
<Button startIcon={<CancelIcon />} variant="outlined" onClick={onDialogCancel} color="secondary">
<Button
startIcon={<CancelIcon />}
variant="outlined"
onClick={onDialogCancel}
color="secondary"
>
{LL.CANCEL()}
</Button>
<Button
@@ -293,7 +338,12 @@ const Scheduler: FC = () => {
</Box>
<Box flexWrap="nowrap" whiteSpace="nowrap">
<ButtonRow>
<Button startIcon={<AddIcon />} variant="outlined" color="secondary" onClick={addScheduleItem}>
<Button
startIcon={<AddIcon />}
variant="outlined"
color="secondary"
onClick={addScheduleItem}
>
{LL.ADD(0)}
</Button>
</ButtonRow>

View File

@@ -40,7 +40,15 @@ interface SchedulerDialogProps {
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 [editItem, setEditItem] = useState<ScheduleItem>(selectedItem);
const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>();
@@ -111,8 +119,14 @@ const SchedulerDialog = ({ open, creating, onClose, onSave, selectedItem, valida
};
const showFlag = (si: ScheduleItem, flag: number) => (
<Typography 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
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>
);
@@ -121,7 +135,8 @@ const SchedulerDialog = ({ open, creating, onClose, onSave, selectedItem, valida
return (
<Dialog sx={dialogStyle} open={open} onClose={close}>
<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>
<DialogContent dividers>
<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 });
}}
>
<ToggleButton value="2">{showFlag(editItem, ScheduleFlag.SCHEDULE_MON)}</ToggleButton>
<ToggleButton value="4">{showFlag(editItem, ScheduleFlag.SCHEDULE_TUE)}</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>
<ToggleButton value="2">
{showFlag(editItem, ScheduleFlag.SCHEDULE_MON)}
</ToggleButton>
<ToggleButton value="4">
{showFlag(editItem, ScheduleFlag.SCHEDULE_TUE)}
</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>
</Box>
<Box flexWrap="nowrap" whiteSpace="nowrap">
@@ -160,7 +189,10 @@ const SchedulerDialog = ({ open, creating, onClose, onSave, selectedItem, valida
size="large"
variant="outlined"
onClick={() => {
setEditItem({ ...editItem, flags: ScheduleFlag.SCHEDULE_TIMER });
setEditItem({
...editItem,
flags: ScheduleFlag.SCHEDULE_TIMER
});
}}
>
{showFlag(editItem, ScheduleFlag.SCHEDULE_TIMER)}
@@ -170,7 +202,13 @@ const SchedulerDialog = ({ open, creating, onClose, onSave, selectedItem, valida
</Box>
<Grid container>
<BlockFormControlLabel
control={<Checkbox checked={editItem.active} onChange={updateFormValue} name="active" />}
control={
<Checkbox
checked={editItem.active}
onChange={updateFormValue}
name="active"
/>
}
label={LL.ACTIVE()}
/>
</Grid>
@@ -220,15 +258,30 @@ const SchedulerDialog = ({ open, creating, onClose, onSave, selectedItem, valida
<DialogActions>
{!creating && (
<Box flexGrow={1}>
<Button startIcon={<RemoveIcon />} variant="outlined" color="warning" onClick={remove}>
<Button
startIcon={<RemoveIcon />}
variant="outlined"
color="warning"
onClick={remove}
>
{LL.REMOVE()}
</Button>
</Box>
)}
<Button startIcon={<CancelIcon />} variant="outlined" onClick={close} color="secondary">
<Button
startIcon={<CancelIcon />}
variant="outlined"
onClick={close}
color="secondary"
>
{LL.CANCEL()}
</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()}
</Button>
</DialogActions>

View File

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

View File

@@ -79,7 +79,8 @@ const SensorsAnalogDialog = ({
return (
<Dialog sx={dialogStyle} open={open} onClose={close}>
<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>
<DialogContent dividers>
<Grid container spacing={2}>
@@ -113,7 +114,14 @@ const SensorsAnalogDialog = ({
/>
</Grid>
<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) => (
<MenuItem key={i} value={i}>
{val}
@@ -123,7 +131,14 @@ const SensorsAnalogDialog = ({
</Grid>
{editItem.t >= AnalogType.COUNTER && editItem.t <= AnalogType.RATE && (
<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) => (
<MenuItem key={i} value={i}>
{val}
@@ -144,7 +159,9 @@ const SensorsAnalogDialog = ({
onChange={updateFormValue}
inputProps={{ min: '0', max: '3300', step: '1' }}
InputProps={{
startAdornment: <InputAdornment position="start">mV</InputAdornment>
startAdornment: (
<InputAdornment position="start">mV</InputAdornment>
)
}}
/>
</Grid>
@@ -177,7 +194,8 @@ const SensorsAnalogDialog = ({
/>
</Grid>
)}
{editItem.t === AnalogType.DIGITAL_OUT && (editItem.g === 25 || editItem.g === 26) && (
{editItem.t === AnalogType.DIGITAL_OUT &&
(editItem.g === 25 || editItem.g === 26) && (
<Grid item xs={4}>
<TextField
name="o"
@@ -191,7 +209,9 @@ const SensorsAnalogDialog = ({
/>
</Grid>
)}
{editItem.t === AnalogType.DIGITAL_OUT && editItem.g !== 25 && editItem.g !== 26 && (
{editItem.t === AnalogType.DIGITAL_OUT &&
editItem.g !== 25 &&
editItem.g !== 26 && (
<>
<Grid item xs={4}>
<TextField
@@ -240,7 +260,9 @@ const SensorsAnalogDialog = ({
</Grid>
</>
)}
{(editItem.t === AnalogType.PWM_0 || editItem.t === AnalogType.PWM_1 || editItem.t === AnalogType.PWM_2) && (
{(editItem.t === AnalogType.PWM_0 ||
editItem.t === AnalogType.PWM_1 ||
editItem.t === AnalogType.PWM_2) && (
<>
<Grid item xs={4}>
<TextField
@@ -253,7 +275,9 @@ const SensorsAnalogDialog = ({
onChange={updateFormValue}
inputProps={{ min: '1', max: '5000', step: '1' }}
InputProps={{
startAdornment: <InputAdornment position="start">Hz</InputAdornment>
startAdornment: (
<InputAdornment position="start">Hz</InputAdornment>
)
}}
/>
</Grid>
@@ -268,7 +292,9 @@ const SensorsAnalogDialog = ({
onChange={updateFormValue}
inputProps={{ min: '0', max: '100', step: '0.1' }}
InputProps={{
startAdornment: <InputAdornment position="start">%</InputAdornment>
startAdornment: (
<InputAdornment position="start">%</InputAdornment>
)
}}
/>
</Grid>
@@ -279,15 +305,30 @@ const SensorsAnalogDialog = ({
<DialogActions>
{!creating && (
<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()}
</Button>
</Box>
)}
<Button startIcon={<CancelIcon />} variant="outlined" onClick={close} color="secondary">
<Button
startIcon={<CancelIcon />}
variant="outlined"
onClick={close}
color="secondary"
>
{LL.CANCEL()}
</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()}
</Button>
</DialogActions>

View File

@@ -107,10 +107,20 @@ const SensorsTemperatureDialog = ({
</Grid>
</DialogContent>
<DialogActions>
<Button startIcon={<CancelIcon />} variant="outlined" onClick={close} color="secondary">
<Button
startIcon={<CancelIcon />}
variant="outlined"
onClick={close}
color="secondary"
>
{LL.CANCEL()}
</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()}
</Button>
</DialogActions>

View File

@@ -4,7 +4,15 @@ import type { FC } from 'react';
import RefreshIcon from '@mui/icons-material/Refresh';
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 { useRequest } from 'alova';
import { ButtonRow, FormLoader, SectionContent, useLayoutTitle } from 'components';
@@ -93,7 +101,11 @@ const SystemActivity: FC = () => {
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[]) => (
<>
<Header>
@@ -118,7 +130,12 @@ const SystemActivity: FC = () => {
)}
</Table>
<ButtonRow mt={1}>
<Button startIcon={<RefreshIcon />} variant="outlined" color="secondary" onClick={loadData}>
<Button
startIcon={<RefreshIcon />}
variant="outlined"
color="secondary"
onClick={loadData}
>
{LL.REFRESH()}
</Button>
</ButtonRow>

View File

@@ -30,17 +30,20 @@ export const writeDeviceValue = (data: { id: number; c: string; v: unknown }) =>
// Application 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) =>
alovaInstance.Get('/rest/boardProfile', {
params: { boardProfile }
});
// Sensors
export const readSensorData = () => alovaInstance.Get<SensorData>('/rest/sensorData');
export const readSensorData = () =>
alovaInstance.Get<SensorData>('/rest/sensorData');
export const writeTemperatureSensor = (ts: WriteTemperatureSensor) =>
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
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 resetCustomizations = () => alovaInstance.Post('/rest/resetCustomizations');
export const writeCustomizationEntities = (data: { id: number; entity_ids: string[] }) =>
alovaInstance.Post('/rest/customizationEntities', data);
export const resetCustomizations = () =>
alovaInstance.Post('/rest/resetCustomizations');
export const writeCustomizationEntities = (data: {
id: number;
entity_ids: string[];
}) => alovaInstance.Post('/rest/customizationEntities', data);
// SettingsScheduler
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
export const readCustomEntities = () =>

View File

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

View File

@@ -5,7 +5,11 @@ import { IP_OR_HOSTNAME_VALIDATOR } from 'validators/shared';
import type { AnalogSensor, DeviceValue, ScheduleItem, Settings } from './types';
export const GPIO_VALIDATOR = {
validator(rule: InternalRuleItem, value: number, callback: (error?: string) => void) {
validator(
rule: InternalRuleItem,
value: number,
callback: (error?: string) => void
) {
if (
value &&
(value === 1 ||
@@ -24,7 +28,11 @@ export const GPIO_VALIDATOR = {
};
export const GPIO_VALIDATORR = {
validator(rule: InternalRuleItem, value: number, callback: (error?: string) => void) {
validator(
rule: InternalRuleItem,
value: number,
callback: (error?: string) => void
) {
if (
value &&
(value === 1 ||
@@ -44,7 +52,11 @@ export const GPIO_VALIDATORR = {
};
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)) {
callback('Must be an valid GPIO port');
} else {
@@ -54,8 +66,18 @@ export const GPIO_VALIDATORC3 = {
};
export const GPIO_VALIDATORS2 = {
validator(rule: InternalRuleItem, value: number, callback: (error?: string) => void) {
if (value && ((value >= 19 && value <= 20) || (value >= 22 && value <= 32) || value > 40 || value < 0)) {
validator(
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');
} else {
callback();
@@ -64,7 +86,11 @@ export const GPIO_VALIDATORS2 = {
};
export const GPIO_VALIDATORS3 = {
validator(rule: InternalRuleItem, value: number, callback: (error?: string) => void) {
validator(
rule: InternalRuleItem,
value: number,
callback: (error?: string) => void
) {
if (
value &&
((value >= 19 && value <= 20) ||
@@ -84,46 +110,121 @@ export const createSettingsValidator = (settings: Settings) =>
new Schema({
...(settings.board_profile === 'CUSTOM' &&
settings.platform === 'ESP32' && {
led_gpio: [{ required: true, message: 'LED 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],
led_gpio: [
{ required: true, message: 'LED 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]
}),
...(settings.board_profile === 'CUSTOM' &&
settings.platform === 'ESP32R' && {
led_gpio: [{ required: true, message: 'LED 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]
led_gpio: [
{ required: true, message: 'LED 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.platform === 'ESP32-C3' && {
led_gpio: [{ required: true, message: 'LED 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]
led_gpio: [
{ required: true, message: 'LED 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.platform === 'ESP32-S2' && {
led_gpio: [{ required: true, message: 'LED 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]
led_gpio: [
{ required: true, message: 'LED 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.platform === 'ESP32-S3' && {
led_gpio: [{ required: true, message: 'LED 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]
led_gpio: [
{ required: true, message: 'LED 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 && {
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: [
{ required: true, message: 'Port is required' },
{ type: 'number', min: 0, max: 65535, message: 'Invalid Port' }
@@ -134,14 +235,35 @@ export const createSettingsValidator = (settings: Settings) =>
]
}),
...(settings.shower_alert && {
shower_alert_trigger: [{ 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' }]
shower_alert_trigger: [
{
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) => ({
validator(rule: InternalRuleItem, name: string, callback: (error?: string) => void) {
if ((o_name === undefined || o_name !== name) && schedule.find((si) => si.name === name)) {
validator(
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');
} else {
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({
name: [
{
@@ -162,7 +287,12 @@ export const schedulerItemValidation = (schedule: ScheduleItem[], scheduleItem:
],
cmd: [
{ 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: [
{
validator(rule: InternalRuleItem, value: string, callback: (error?: string) => void) {
validator(
rule: InternalRuleItem,
value: string,
callback: (error?: string) => void
) {
if (isNaN(parseInt(value, 16))) {
callback('Is required and must be in hex format');
}
@@ -188,7 +322,11 @@ export const entityItemValidation = () =>
],
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))) {
callback('Is required and must be in hex format');
}
@@ -208,7 +346,11 @@ export const temperatureSensorItemValidation = () =>
});
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)) {
callback('GPIO already in use');
} 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({
n: [{ required: true, message: 'Name is required' }],
g: [
@@ -240,8 +386,17 @@ export const deviceValueItemValidation = (dv: DeviceValue) =>
v: [
{ required: true, message: 'Value is required' },
{
validator(rule: InternalRuleItem, value: unknown, callback: (error?: string) => void) {
if (typeof value === 'number' && dv.m && dv.x && (value < dv.m || value > dv.x)) {
validator(
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();

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
});
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) =>
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 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 {
loading: saving,

View File

@@ -6,15 +6,27 @@ import { IP_ADDRESS_VALIDATOR } from './shared';
export const createAPSettingsValidator = (apSettings: APSettingsType) =>
new Schema({
provision_mode: { required: true, message: 'Please provide a provision mode' },
provision_mode: {
required: true,
message: 'Please provide a provision mode'
},
...(isAPEnabled(apSettings) && {
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: [
{ 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: [
{ required: true, message: 'Please provide a network channel' },
@@ -22,10 +34,24 @@ export const createAPSettingsValidator = (apSettings: APSettingsType) =>
],
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],
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]
local_ip: [
{ required: true, message: 'Local IP address is required' },
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) =>
new Schema({
...(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' },
port: [
{ required: true, message: 'Port is required' },
@@ -14,11 +17,21 @@ export const createMqttSettingsValidator = (mqttSettings: MqttSettingsType) =>
],
keep_alive: [
{ 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: [
{ 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';
export const createNetworkSettingsValidator = (networkSettings: NetworkSettingsType) =>
export const createNetworkSettingsValidator = (
networkSettings: NetworkSettingsType
) =>
new Schema({
ssid: [{ type: 'string', max: 32, message: 'SSID must be 32 characters or less' }],
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],
ssid: [
{ type: 'string', max: 32, message: 'SSID must be 32 characters or less' }
],
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 && {
local_ip: [{ required: true, message: 'Local IP is required' }, 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],
local_ip: [
{ required: true, message: 'Local IP is required' },
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_2: IP_ADDRESS_VALIDATOR
})

View File

@@ -3,7 +3,10 @@ import Schema from 'async-validator';
import { IP_OR_HOSTNAME_VALIDATOR } from './shared';
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: {
required: true,
message: 'Time zone is required'

View File

@@ -5,12 +5,21 @@ import type { UserType } from 'types';
export const SECURITY_SETTINGS_VALIDATOR = new Schema({
jwt_secret: [
{ 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[]) => ({
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)) {
callback('Username already in use');
} else {
@@ -32,6 +41,11 @@ export const createUserValidator = (users: UserType[], creating: boolean) =>
],
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
): Promise<T> =>
new Promise((resolve, reject) => {
void validator.validate(source, options ? options : {}, (errors, fieldErrors) => {
void validator.validate(
source,
options ? options : {},
(errors, fieldErrors) => {
if (errors) {
reject(fieldErrors);
} else {
resolve(source as T);
}
});
}
);
});
// updated to support both IPv4 and IPv6
@@ -23,7 +27,11 @@ const IP_ADDRESS_REGEXP =
const isValidIpAddress = (value: string) => IP_ADDRESS_REGEXP.test(value);
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)) {
callback('Must be an IP address');
} else {
@@ -36,10 +44,15 @@ const HOSTNAME_LENGTH_REGEXP = /^.{0,200}$/;
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])$/;
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 = {
validator(rule: InternalRuleItem, value: string, callback: (error?: string) => void) {
validator(
rule: InternalRuleItem,
value: string,
callback: (error?: string) => void
) {
if (value && !isValidHostname(value)) {
callback('Must be a valid hostname');
} else {
@@ -49,7 +62,11 @@ export const 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))) {
callback('Must be a valid IP address or hostname');
} else {

View File

@@ -3,10 +3,20 @@ import Schema from 'async-validator';
export const OTA_SETTINGS_VALIDATOR = new Schema({
port: [
{ 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: [
{ 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) {
if (id.includes('node_modules')) {
// 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 'vendor';