status screen updates

This commit is contained in:
proddy
2024-03-19 23:25:31 +01:00
parent 217b424320
commit 863bc04c21
42 changed files with 773 additions and 583 deletions

View File

@@ -10,6 +10,7 @@ import NetworkConnection from 'framework/network/NetworkConnection';
import NetworkTime from 'framework/ntp/NetworkTime';
import OTASettingsForm from 'framework/ota/OTASettingsForm';
import Security from 'framework/security/Security';
import ESPSystemStatus from 'framework/system/ESPSystemStatus';
import Status from 'framework/system/Status';
import UploadDownload from 'framework/system/UploadDownload';
import ApplicationSettings from 'project/ApplicationSettings';
@@ -42,6 +43,7 @@ const AuthenticatedRouting: FC = () => {
<Route path="/settings/mqtt/*" element={<Mqtt />} />
<Route path="/settings/ota/*" element={<OTASettingsForm />} />
<Route path="/settings/security/*" element={<Security />} />
<Route path="/settings/espsystemstatus/*" element={<ESPSystemStatus />} />
<Route path="/settings/upload/*" element={<UploadDownload />} />
</>
)}

View File

@@ -1,7 +1,10 @@
import { alovaInstance, alovaInstanceGH } from './endpoints';
import type { OTASettings, SystemStatus, LogSettings } from 'types';
import type { OTASettings, SystemStatus, LogSettings, ESPSystemStatus } from 'types';
// SystemStatus - also used to ping in Restart monitor for pinging
// ESPSystemStatus - also used to ping in Restart monitor for pinging
export const readESPSystemStatus = () => alovaInstance.Get<ESPSystemStatus>('/rest/ESPSystemStatus');
// SystemStatus
export const readSystemStatus = () => alovaInstance.Get<SystemStatus>('/rest/systemStatus');
// commands

View File

@@ -4,6 +4,7 @@ import CastIcon from '@mui/icons-material/Cast';
import DeviceHubIcon from '@mui/icons-material/DeviceHub';
import ImportExportIcon from '@mui/icons-material/ImportExport';
import LockIcon from '@mui/icons-material/Lock';
import MemoryIcon from '@mui/icons-material/Memory';
import NavigateNextIcon from '@mui/icons-material/NavigateNext';
import PowerSettingsNewIcon from '@mui/icons-material/PowerSettingsNew';
import SettingsBackupRestoreIcon from '@mui/icons-material/SettingsBackupRestore';
@@ -24,7 +25,8 @@ import {
DialogActions,
DialogContent,
DialogTitle,
Box
Box,
Divider
} from '@mui/material';
import { useRequest } from 'alova';
import { useState, type FC } from 'react';
@@ -294,6 +296,26 @@ const Settings: FC = () => {
</ListItemButton>
</ListItem>
<Divider />
<ListItem
disablePadding
secondaryAction={
<ListItemIcon style={{ justifyContent: 'right', color: 'lightblue', verticalAlign: 'middle' }}>
<NavigateNextIcon />
</ListItemIcon>
}
>
<ListItemButton component={Link} to="espsystemstatus">
<ListItemAvatar>
<Avatar sx={{ color: 'white' }}>
<MemoryIcon />
</Avatar>
</ListItemAvatar>
<ListItemText primary={LL.STATUS_OF('ESP32')} secondary="ESP32 Information" />
</ListItemButton>
</ListItem>
<ListItem
disablePadding
secondaryAction={
@@ -312,8 +334,10 @@ const Settings: FC = () => {
</ListItemButton>
</ListItem>
</List>
{renderRestartDialog()}
{renderFactoryResetDialog()}
<Box display="flex" flexWrap="wrap">
<Box flexGrow={1} sx={{ '& button': { mt: 2 } }}>
<ButtonRow>

View File

@@ -205,7 +205,7 @@ const APSettingsForm: FC = () => {
};
return (
<SectionContent title={LL.SETTINGS_OF(LL.ACCESS_POINT(1))}>
<SectionContent>
{blocker ? <BlockNavigation blocker={blocker} /> : null}
{content()}
</SectionContent>

View File

@@ -99,7 +99,7 @@ const APStatusForm: FC = () => {
);
};
return <SectionContent title={LL.STATUS_OF(LL.ACCESS_POINT(1))}>{content()}</SectionContent>;
return <SectionContent>{content()}</SectionContent>;
};
export default APStatusForm;

View File

@@ -449,7 +449,7 @@ const MqttSettingsForm: FC = () => {
};
return (
<SectionContent title={LL.SETTINGS_OF('MQTT')}>
<SectionContent>
{blocker ? <BlockNavigation blocker={blocker} /> : null}
{content()}
</SectionContent>

View File

@@ -146,7 +146,7 @@ const MqttStatusForm: FC = () => {
);
};
return <SectionContent title={LL.STATUS_OF('MQTT')}>{content()}</SectionContent>;
return <SectionContent>{content()}</SectionContent>;
};
export default MqttStatusForm;

View File

@@ -360,7 +360,7 @@ const WiFiSettingsForm: FC = () => {
};
return (
<SectionContent title={LL.SETTINGS_OF(LL.NETWORK(1))}>
<SectionContent>
{blocker ? <BlockNavigation blocker={blocker} /> : null}
{restarting ? <RestartMonitor /> : content()}
</SectionContent>

View File

@@ -193,7 +193,7 @@ const NetworkStatusForm: FC = () => {
);
};
return <SectionContent title={LL.STATUS_OF(LL.NETWORK(1))}>{content()}</SectionContent>;
return <SectionContent>{content()}</SectionContent>;
};
export default NetworkStatusForm;

View File

@@ -56,7 +56,7 @@ const WiFiNetworkScanner: FC = () => {
};
return (
<SectionContent title={LL.NETWORK_SCANNER()}>
<SectionContent>
{renderNetworkScanner()}
<ButtonRow>
<Button

View File

@@ -130,7 +130,7 @@ const NTPSettingsForm: FC = () => {
};
return (
<SectionContent title={LL.SETTINGS_OF('NTP')}>
<SectionContent>
{blocker ? <BlockNavigation blocker={blocker} /> : null}
{content()}
</SectionContent>

View File

@@ -214,7 +214,7 @@ const NTPStatusForm: FC = () => {
);
};
return <SectionContent title={LL.STATUS_OF('NTP')}>{content()}</SectionContent>;
return <SectionContent>{content()}</SectionContent>;
};
export default NTPStatusForm;

View File

@@ -120,7 +120,7 @@ const OTASettingsForm: FC = () => {
};
return (
<SectionContent title={LL.SETTINGS_OF('OTA')}>
<SectionContent>
{blocker ? <BlockNavigation blocker={blocker} /> : null}
{content()}
</SectionContent>

View File

@@ -232,7 +232,7 @@ const ManageUsersForm: FC = () => {
};
return (
<SectionContent title={LL.MANAGE_USERS()}>
<SectionContent>
{blocker ? <BlockNavigation blocker={blocker} /> : null}
{content()}
</SectionContent>

View File

@@ -96,7 +96,7 @@ const SecuritySettingsForm: FC = () => {
};
return (
<SectionContent title={LL.SETTINGS_OF(LL.SECURITY(1))}>
<SectionContent>
{blocker ? <BlockNavigation blocker={blocker} /> : null}
{content()}
</SectionContent>

View File

@@ -1,5 +1,4 @@
import AppsIcon from '@mui/icons-material/Apps';
import BuildIcon from '@mui/icons-material/Build';
import DeveloperBoardIcon from '@mui/icons-material/DeveloperBoard';
import DevicesIcon from '@mui/icons-material/Devices';
import FolderIcon from '@mui/icons-material/Folder';
@@ -7,24 +6,25 @@ 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 TimerIcon from '@mui/icons-material/Timer';
import { Avatar, Box, Button, Divider, List, ListItem, ListItemAvatar, ListItemText } from '@mui/material';
import { useRequest } from 'alova';
import type { FC } from 'react';
import * as SystemApi from 'api/system';
import { ButtonRow, FormLoader, SectionContent } from 'components';
import { ButtonRow, FormLoader, SectionContent, useLayoutTitle } from 'components';
import { useI18nContext } from 'i18n/i18n-react';
function formatNumber(num: number) {
return new Intl.NumberFormat().format(num);
}
const SystemStatusForm: FC = () => {
const ESPSystemStatus: FC = () => {
const { LL } = useI18nContext();
const { data: data, send: loadData, error } = useRequest(SystemApi.readSystemStatus, { force: true });
useLayoutTitle(LL.STATUS_OF('ESP32'));
const { data: data, send: loadData, error } = useRequest(SystemApi.readESPSystemStatus, { force: true });
const content = () => {
if (!data) {
@@ -34,24 +34,6 @@ const SystemStatusForm: FC = () => {
return (
<>
<List>
<ListItem>
<ListItemAvatar>
<Avatar sx={{ bgcolor: '#5f9a5f', color: 'white' }}>
<BuildIcon />
</Avatar>
</ListItemAvatar>
<ListItemText primary={LL.EMS_ESP_VER()} secondary={data.emsesp_version} />
</ListItem>
<Divider variant="inset" component="li" />
<ListItem>
<ListItemAvatar>
<Avatar sx={{ bgcolor: '#5f9a5f', color: 'white' }}>
<TimerIcon />
</Avatar>
</ListItemAvatar>
<ListItemText primary={LL.UPTIME()} secondary={data.uptime} />
</ListItem>
<Divider variant="inset" component="li" />
<ListItem>
<ListItemAvatar>
<Avatar sx={{ bgcolor: '#5f9a5f', color: 'white' }}>
@@ -151,7 +133,6 @@ const SystemStatusForm: FC = () => {
secondary={formatNumber(data.fs_used) + ' KB / ' + formatNumber(data.fs_free) + ' KB'}
/>
</ListItem>
<Divider variant="inset" component="li" />
</List>
<Box display="flex" flexWrap="wrap">
<Box flexGrow={1} sx={{ '& button': { mt: 2 } }}>
@@ -166,7 +147,7 @@ const SystemStatusForm: FC = () => {
);
};
return <SectionContent title={LL.STATUS_OF(LL.SYSTEM(1))}>{content()}</SectionContent>;
return <SectionContent>{content()}</SectionContent>;
};
export default SystemStatusForm;
export default ESPSystemStatus;

View File

@@ -1,13 +1,13 @@
import { Tab } from '@mui/material';
import { Navigate, Routes, Route } from 'react-router-dom';
import SystemLog from './SystemLog';
import SystemStatusForm from './SystemStatusForm';
import SystemStatus from './SystemStatus';
import type { FC } from 'react';
import { useRouterTab, RouterTabs, useLayoutTitle } from 'components';
import { useI18nContext } from 'i18n/i18n-react';
import EMSStatus from 'project/EMSStatus';
import Activity from 'project/Activity';
const Status: FC = () => {
const { LL } = useI18nContext();
@@ -20,12 +20,12 @@ const Status: FC = () => {
<>
<RouterTabs value={routerTab}>
<Tab value="status" label={LL.SYSTEM(1)} />
<Tab value="emsesp-status" label="EMS-ESP" />
<Tab value="log" label={LL.LOG_OF(LL.SYSTEM(2))} />
<Tab value="activity" label={LL.ACTIVITY()} />
<Tab value="log" label={LL.LOG_OF('')} />
</RouterTabs>
<Routes>
<Route path="status" element={<SystemStatusForm />} />
<Route path="emsesp-status" element={<EMSStatus />} />
<Route path="status" element={<SystemStatus />} />
<Route path="activity" element={<Activity />} />
<Route path="log" element={<SystemLog />} />
<Route path="*" element={<Navigate replace to="status" />} />
</Routes>

View File

@@ -232,7 +232,7 @@ const SystemLog: FC = () => {
};
return (
<SectionContent title={LL.LOG_OF(LL.SYSTEM(2))} id="log-window">
<SectionContent id="log-window">
{blocker ? <BlockNavigation blocker={blocker} /> : null}
{content()}
</SectionContent>

View File

@@ -0,0 +1,212 @@
import BuildIcon from '@mui/icons-material/Build';
import CancelIcon from '@mui/icons-material/Cancel';
import DeviceHubIcon from '@mui/icons-material/DeviceHub';
import DirectionsBusIcon from '@mui/icons-material/DirectionsBus';
import PermScanWifiIcon from '@mui/icons-material/PermScanWifi';
import RefreshIcon from '@mui/icons-material/Refresh';
import TimerIcon from '@mui/icons-material/Timer';
import {
Avatar,
Box,
Button,
Dialog,
DialogActions,
DialogContent,
DialogTitle,
Divider,
List,
ListItem,
ListItemAvatar,
ListItemText,
useTheme
} from '@mui/material';
import { useRequest } from 'alova';
import { useContext, type FC, useState } from 'react';
import { toast } from 'react-toastify';
import { dialogStyle } from 'CustomTheme';
import * as SystemApi from 'api/system';
import { FormLoader, SectionContent, useLayoutTitle } from 'components';
import { AuthenticatedContext } from 'contexts/authentication';
import { useI18nContext } from 'i18n/i18n-react';
import * as EMSESP from 'project/api';
import { busConnectionStatus } from 'project/types';
const SystemStatus: FC = () => {
const { LL } = useI18nContext();
useLayoutTitle(LL.STATUS_OF(''));
const { me } = useContext(AuthenticatedContext);
const [confirmScan, setConfirmScan] = useState<boolean>(false);
const { data: data, send: loadData, error } = useRequest(SystemApi.readSystemStatus, { force: true });
const { send: scanDevices } = useRequest(EMSESP.scanDevices, {
immediate: false
});
const theme = useTheme();
const formatDurationSec = (duration_sec: number) => {
const days = Math.trunc((duration_sec * 1000) / 86400000);
const hours = Math.trunc((duration_sec * 1000) / 3600000) % 24;
const minutes = Math.trunc((duration_sec * 1000) / 60000) % 60;
const seconds = Math.trunc((duration_sec * 1000) / 1000) % 60;
let formatted = '';
if (days) {
formatted += LL.NUM_DAYS({ num: days }) + ' ';
}
if (hours) {
formatted += LL.NUM_HOURS({ num: hours }) + ' ';
}
if (minutes) {
formatted += LL.NUM_MINUTES({ num: minutes }) + ' ';
}
formatted += LL.NUM_SECONDS({ num: seconds });
return formatted;
};
const busStatus = () => {
if (data) {
switch (data.status) {
case busConnectionStatus.BUS_STATUS_CONNECTED:
return LL.CONNECTED(0);
case busConnectionStatus.BUS_STATUS_TX_ERRORS:
return LL.TX_ISSUES();
case busConnectionStatus.BUS_STATUS_OFFLINE:
return LL.DISCONNECTED();
}
}
return 'Unknown';
};
// export const isConnected = ({ status }: Status) => status !== busConnectionStatus.BUS_STATUS_OFFLINE;
const busStatusHighlight = () => {
switch (data.status) {
case busConnectionStatus.BUS_STATUS_TX_ERRORS:
return theme.palette.warning.main;
case busConnectionStatus.BUS_STATUS_CONNECTED:
return theme.palette.success.main;
case busConnectionStatus.BUS_STATUS_OFFLINE:
return theme.palette.error.main;
default:
return theme.palette.warning.main;
}
};
const scan = async () => {
await scanDevices()
.then(() => {
toast.info(LL.SCANNING() + '...');
})
.catch((err) => {
toast.error(err.message);
});
setConfirmScan(false);
};
const renderScanDialog = () => (
<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">
{LL.CANCEL()}
</Button>
<Button startIcon={<PermScanWifiIcon />} variant="outlined" onClick={scan} color="primary">
{LL.SCAN()}
</Button>
</DialogActions>
</Dialog>
);
const content = () => {
if (!data) {
return <FormLoader onRetry={loadData} errorMessage={error?.message} />;
}
return (
<>
<List>
<ListItem>
<ListItemAvatar>
<Avatar>
<BuildIcon />
</Avatar>
</ListItemAvatar>
<ListItemText primary={LL.EMS_ESP_VER()} secondary={data.emsesp_version} />
</ListItem>
<Divider variant="inset" component="li" />
<ListItem>
<ListItemAvatar>
<Avatar sx={{ bgcolor: '#5f9a5f', color: 'white' }}>
<TimerIcon />
</Avatar>
</ListItemAvatar>
<ListItemText primary={LL.UPTIME()} secondary={formatDurationSec(data.uptime)} />
</ListItem>
<Divider variant="inset" component="li" />
<ListItem>
<ListItemAvatar>
<Avatar sx={{ bgcolor: busStatusHighlight() }}>
<DirectionsBusIcon />
</Avatar>
</ListItemAvatar>
<ListItemText primary={LL.EMS_BUS_STATUS()} secondary={busStatus()} />
</ListItem>
<Divider variant="inset" component="li" />
<ListItem>
<ListItemAvatar>
<Avatar sx={{ bgcolor: theme.palette.success.main }}>
<DeviceHubIcon />
</Avatar>
</ListItemAvatar>
<ListItemText
primary={LL.ACTIVE_DEVICES()}
secondary={
LL.NUM_DEVICES({ num: data.num_devices }) +
', ' +
LL.NUM_TEMP_SENSORS({ num: data.num_sensors }) +
', ' +
LL.NUM_ANALOG_SENSORS({ num: data.num_analogs })
}
/>
{me.admin && (
<Button
startIcon={<PermScanWifiIcon />}
variant="outlined"
color="primary"
onClick={() => setConfirmScan(true)}
>
{LL.SCAN_DEVICES()}
</Button>
)}
</ListItem>
<Divider variant="inset" component="li" />
</List>
{renderScanDialog()}
<Box mt={2} display="flex" flexWrap="wrap">
<Button startIcon={<RefreshIcon />} variant="outlined" color="secondary" onClick={loadData}>
{LL.REFRESH()}
</Button>
</Box>
</>
);
};
return <SectionContent>{content()}</SectionContent>;
};
export default SystemStatus;

View File

@@ -32,12 +32,13 @@ const UploadDownload: FC = () => {
immediate: false
});
const { data: data, send: loadData, error } = useRequest(SystemApi.readSystemStatus, { force: true });
const { data: data, send: loadData, error } = useRequest(SystemApi.readESPSystemStatus, { force: true });
const { data: latestVersion } = useRequest(SystemApi.getStableVersion, {
immediate: true,
force: true
});
const { data: latestDevVersion } = useRequest(SystemApi.getDevVersion, {
immediate: true,
force: true

View File

@@ -80,7 +80,6 @@ const de: Translation = {
FAIL: 'FEHLER',
QUALITY: 'QUALITÄT',
SCAN_DEVICES: 'Nach neuen Geräten suchen',
EMS_BUS_STATUS_TITLE: 'EMS-Bus- und Aktivitätsstatus',
SCAN: 'Suche',
STATUS_NAMES: [
'EMS-Telegramme empfangen (Rx)',
@@ -322,7 +321,9 @@ const de: Translation = {
ACTIVEHIGH: 'Aktiv Positiv',
ACTIVELOW: 'Aktiv Negativ',
UNCHANGED: 'Unverändert',
ALWAYS: 'Immer'
ALWAYS: 'Immer',
ACTIVITY: 'Activity' // TODO translate
};
export default de;

View File

@@ -80,7 +80,6 @@ const en: Translation = {
FAIL: 'FAIL',
QUALITY: 'QUALITY',
SCAN_DEVICES: 'Scan for new devices',
EMS_BUS_STATUS_TITLE: 'EMS Bus & Activity Status',
SCAN: 'Scan',
STATUS_NAMES: [
'EMS Telegrams Received (Rx)',
@@ -164,7 +163,7 @@ const en: Translation = {
HELP_INFORMATION_1: 'Visit the online wiki to get instructions on how to configure EMS-ESP',
HELP_INFORMATION_2: 'For live community chat join our Discord server',
HELP_INFORMATION_3: 'To request a feature or report a bug',
HELP_INFORMATION_4: 'Remember to download and attach your support information for a faster response when reporting an issue',
HELP_INFORMATION_4: 'Download and attach your support information for a faster response when reporting an issue',
HELP_INFORMATION_5: 'EMS-ESP is a free and open-source project. Please support its future development by giving it a star on Github!',
UPLOAD: 'Upload',
DOWNLOAD: '{{D|d|d}}ownload',
@@ -187,7 +186,6 @@ const en: Translation = {
RELEASE_IS: 'release is',
RELEASE_NOTES: 'release notes',
EMS_ESP_VER: 'EMS-ESP Version',
PLATFORM: 'Device (Platform / SDK)',
UPTIME: 'System Uptime',
HEAP: 'Heap (Free / Max Alloc)',
PSRAM: 'PSRAM (Size / Free)',
@@ -323,7 +321,8 @@ const en: Translation = {
ACTIVEHIGH: 'Active High',
ACTIVELOW: 'Active Low',
UNCHANGED: 'Unchanged',
ALWAYS: 'Always'
ALWAYS: 'Always',
ACTIVITY: 'Activity'
};
export default en;

View File

@@ -80,7 +80,6 @@ const fr: Translation = {
FAIL: 'ÉCHEC',
QUALITY: 'QUALITÉ',
SCAN_DEVICES: 'Rechercher de nouveaux appareils',
EMS_BUS_STATUS_TITLE: 'Statut du bus et de l\'activité EMS',
SCAN: 'Scan',
STATUS_NAMES: [
'Télégrammes EMS reçus (Rx)',
@@ -322,7 +321,8 @@ const fr: Translation = {
ACTIVEHIGH: 'Active High', // TODO translate
ACTIVELOW: 'Active Low', // TODO translate
UNCHANGED: 'Unchanged', // TODO translate
ALWAYS: 'Always' // TODO translate
ALWAYS: 'Always', // TODO translate
ACTIVITY: 'Activity' // TODO translate
};
export default fr;

View File

@@ -48,7 +48,6 @@ const it: Translation = {
REMOVE: 'Elimina',
PROBLEM_UPDATING: 'Problema aggiornamento',
PROBLEM_LOADING: 'Problema caricamento',
ACCESS_DENIED: 'Accesso Negato',
ANALOG_SENSOR: 'Sensore Analogico',
ANALOG_SENSORS: 'Sensori Analogici',
SETTINGS: 'Settings',
@@ -68,7 +67,6 @@ const it: Translation = {
TEMP_SENSOR: 'Sensore Temperatura',
TEMP_SENSORS: 'Sensori Temperatura',
WRITE_CMD_SENT: 'Scrittura comando inviata',
WRITE_CMD_FAILED: 'Scittura comando fallita',
EMS_BUS_WARNING: 'EMS bus disconnesso. Se questo avvertimento persiste dopo alcuni secondi prego verificare impostazioni scheda',
EMS_BUS_SCANNING: 'Scansione dispositivi EMS ...',
CONNECTED: 'Connesso',
@@ -82,7 +80,6 @@ const it: Translation = {
FAIL: 'FALLITO',
QUALITY: 'QUALITÂ',
SCAN_DEVICES: 'Scansione per nuovi dispositivi',
EMS_BUS_STATUS_TITLE: 'Bus EMS & Stato Attività',
SCAN: 'Scansione',
STATUS_NAMES: [
'Telegrammi EMS Ricevuti (Rx)',
@@ -324,7 +321,8 @@ const it: Translation = {
ACTIVEHIGH: 'Active High', // TODO translate
ACTIVELOW: 'Active Low', // TODO translate
UNCHANGED: 'Unchanged', // TODO translate
ALWAYS: 'Always' // TODO translate
ALWAYS: 'Always', // TODO translate
ACTIVITY: 'Activity' // TODO translate
};
export default it;

View File

@@ -80,7 +80,6 @@ const nl: Translation = {
FAIL: 'MISLUKT',
QUALITY: 'QUALITEIT',
SCAN_DEVICES: 'Scannen naar nieuwe apparaten',
EMS_BUS_STATUS_TITLE: 'EMS Bus & Activiteitenstatus',
SCAN: 'Scan',
STATUS_NAMES: [
'EMS Telegrammen ontvangen (Rx)',
@@ -322,7 +321,8 @@ const nl: Translation = {
ACTIVEHIGH: 'Active High', // TODO translate
ACTIVELOW: 'Active Low', // TODO translate
UNCHANGED: 'Unchanged', // TODO translate
ALWAYS: 'Always' // TODO translate
ALWAYS: 'Always', // TODO translate
ACTIVITY: 'Activity' // TODO translate
};
export default nl;

View File

@@ -80,7 +80,6 @@ const no: Translation = {
FAIL: 'MISLYKKET',
QUALITY: 'KVALITET',
SCAN_DEVICES: 'Søk etter nye enheter',
EMS_BUS_STATUS_TITLE: 'EMS Buss & Aktivitet Status',
SCAN: 'Søk',
STATUS_NAMES: [
'EMS Telegrammer Mottatt (Rx)',
@@ -322,7 +321,8 @@ const no: Translation = {
ACTIVEHIGH: 'Active High', // TODO translate
ACTIVELOW: 'Active Low', // TODO translate
UNCHANGED: 'Unchanged', // TODO translate
ALWAYS: 'Always' // TODO translate
ALWAYS: 'Always', // TODO translate
ACTIVITY: 'Activity' // TODO translate
};
export default no;

View File

@@ -80,7 +80,6 @@ const pl: BaseTranslation = {
FAIL: 'Nieudane',
QUALITY: 'Jakość',
SCAN_DEVICES: 'Wyszukiwanie nowych urządzeń',
EMS_BUS_STATUS_TITLE: 'Aktywność',
SCAN: 'Skanuj',
STATUS_NAMES: [
'EMS, telegramy odebrane (Rx)',
@@ -322,7 +321,8 @@ const pl: BaseTranslation = {
ACTIVEHIGH: 'Wyzwalany stanem wysokim',
ACTIVELOW: 'Wyzwalany stanem niskim',
UNCHANGED: 'Zachowaj stan',
ALWAYS: 'Zawsze'
ALWAYS: 'Zawsze',
ACTIVITY: 'Activity' // TODO translate
};
export default pl;

View File

@@ -80,7 +80,6 @@ const sk: Translation = {
FAIL: 'ZLYHANIE',
QUALITY: 'KVALITA',
SCAN_DEVICES: 'Scan pre nové zariadenia',
EMS_BUS_STATUS_TITLE: 'EMS zbernica & stav aktivity',
SCAN: 'Scan',
STATUS_NAMES: [
'EMS Telegramy prijaté (Rx)',
@@ -322,7 +321,8 @@ const sk: Translation = {
ACTIVEHIGH: 'Aktívny Vysoký',
ACTIVELOW: 'Aktívny Nízky',
UNCHANGED: 'Nezmenené',
ALWAYS: 'Vždy'
ALWAYS: 'Vždy',
ACTIVITY: 'Activity' // TODO translate
};
export default sk;

View File

@@ -80,7 +80,6 @@ const sv: Translation = {
FAIL: 'Misslyckades',
QUALITY: 'Kvalitet',
SCAN_DEVICES: 'Sök efter nya enheter',
EMS_BUS_STATUS_TITLE: 'EMS-buss & aktivitetsstatus',
SCAN: 'Sök',
STATUS_NAMES: [
'EMS-telegram (Rx)',
@@ -322,7 +321,8 @@ const sv: Translation = {
ACTIVEHIGH: 'Active High', // TODO translate
ACTIVELOW: 'Active Low', // TODO translate
UNCHANGED: 'Unchanged', // TODO translate
ALWAYS: 'Always' // TODO translate
ALWAYS: 'Always', // TODO translate
ACTIVITY: 'Activity' // TODO translate
};
export default sv;

View File

@@ -80,7 +80,6 @@ const tr: Translation = {
FAIL: 'HATA',
QUALITY: 'KALİTE',
SCAN_DEVICES: 'Yeni cihaz taraması',
EMS_BUS_STATUS_TITLE: 'EMS Hattı ve Aktivite Durumu',
SCAN: 'Tara',
STATUS_NAMES: [
'EMS Telegramlar Alındı (Rx)',
@@ -322,7 +321,8 @@ const tr: Translation = {
ACTIVEHIGH: 'Active High', // TODO translate
ACTIVELOW: 'Active Low', // TODO translate
UNCHANGED: 'Unchanged', // TODO translate
ALWAYS: 'Always' // TODO translate
ALWAYS: 'Always', // TODO translate
ACTIVITY: 'Activity' // TODO translate
};
export default tr;

View File

@@ -0,0 +1,128 @@
import RefreshIcon from '@mui/icons-material/Refresh';
import { Box, Button } from '@mui/material';
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 { useEffect } from 'react';
import * as EMSESP from './api';
import type { Stat } from './types';
import type { Translation } from 'i18n/i18n-types';
import type { FC } from 'react';
import { FormLoader, SectionContent } from 'components';
import { useI18nContext } from 'i18n/i18n-react';
const Activity: FC = () => {
const { data: data, send: loadData, error } = useRequest(EMSESP.readActivity);
const { LL } = useI18nContext();
const stats_theme = tableTheme({
Table: `
--data-table-library_grid-template-columns: repeat(1, minmax(0, 1fr)) 90px 90px 80px;
`,
BaseRow: `
font-size: 14px;
`,
HeaderRow: `
text-transform: uppercase;
background-color: black;
color: #90CAF9;
.th {
height: 36px;
border-bottom: 1px solid #565656;
}
`,
Row: `
.td {
padding: 8px;
border-top: 1px solid #565656;
border-bottom: 1px solid #565656;
}
&:nth-of-type(odd) .td {
background-color: #303030;
}
&:nth-of-type(even) .td {
background-color: #1e1e1e;
}
`,
BaseCell: `
&:not(:first-of-type) {
text-align: center;
}
`
});
useEffect(() => {
const timer = setInterval(() => loadData(), 30000);
return () => {
clearInterval(timer);
};
});
const showName = (id: any) => {
const name: keyof Translation['STATUS_NAMES'] = id;
return LL.STATUS_NAMES[name]();
};
const showQuality = (stat: Stat) => {
if (stat.q === 0 || stat.s + stat.f === 0) {
return;
}
if (stat.q === 100) {
return <div style={{ color: '#00FF7F' }}>{stat.q}%</div>;
}
if (stat.q >= 95) {
return <div style={{ color: 'orange' }}>{stat.q}%</div>;
} else {
return <div style={{ color: 'red' }}>{stat.q}%</div>;
}
};
const content = () => {
if (!data) {
return <FormLoader onRetry={loadData} errorMessage={error?.message} />;
}
return (
<>
<Table data={{ nodes: data.stats }} theme={stats_theme} layout={{ custom: true }}>
{(tableList: any) => (
<>
<Header>
<HeaderRow>
<HeaderCell resize />
<HeaderCell stiff>{LL.SUCCESS()}</HeaderCell>
<HeaderCell stiff>{LL.FAIL()}</HeaderCell>
<HeaderCell stiff>{LL.QUALITY()}</HeaderCell>
</HeaderRow>
</Header>
<Body>
{tableList.map((stat: Stat) => (
<Row key={stat.id} item={stat}>
<Cell>{showName(stat.id)}</Cell>
<Cell stiff>{Intl.NumberFormat().format(stat.s)}</Cell>
<Cell stiff>{Intl.NumberFormat().format(stat.f)}</Cell>
<Cell stiff>{showQuality(stat)}</Cell>
</Row>
))}
</Body>
</>
)}
</Table>
<Box flexGrow={1} sx={{ '& button': { mt: 2 } }}>
<Button startIcon={<RefreshIcon />} variant="outlined" color="secondary" onClick={loadData}>
{LL.REFRESH()}
</Button>
</Box>
</>
);
};
return <SectionContent>{content()}</SectionContent>;
};
export default Activity;

View File

@@ -1,279 +0,0 @@
import CancelIcon from '@mui/icons-material/Cancel';
import DeviceHubIcon from '@mui/icons-material/DeviceHub';
import DirectionsBusIcon from '@mui/icons-material/DirectionsBus';
import PermScanWifiIcon from '@mui/icons-material/PermScanWifi';
import RefreshIcon from '@mui/icons-material/Refresh';
import {
Avatar,
Box,
Button,
Dialog,
DialogActions,
DialogContent,
DialogTitle,
List,
ListItem,
ListItemAvatar,
ListItemText,
useTheme
} from '@mui/material';
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 { useContext, useEffect, useState } from 'react';
import { toast } from 'react-toastify';
import * as EMSESP from './api';
import { busConnectionStatus } from './types';
import type { Stat, Status } from './types';
import type { Theme } from '@mui/material';
import type { Translation } from 'i18n/i18n-types';
import type { FC } from 'react';
import { dialogStyle } from 'CustomTheme';
import { ButtonRow, FormLoader, SectionContent } from 'components';
import { AuthenticatedContext } from 'contexts/authentication';
import { useI18nContext } from 'i18n/i18n-react';
export const isConnected = ({ status }: Status) => status !== busConnectionStatus.BUS_STATUS_OFFLINE;
const busStatusHighlight = ({ status }: Status, theme: Theme) => {
switch (status) {
case busConnectionStatus.BUS_STATUS_TX_ERRORS:
return theme.palette.warning.main;
case busConnectionStatus.BUS_STATUS_CONNECTED:
return theme.palette.success.main;
case busConnectionStatus.BUS_STATUS_OFFLINE:
return theme.palette.error.main;
default:
return theme.palette.warning.main;
}
};
const showQuality = (stat: Stat) => {
if (stat.q === 0 || stat.s + stat.f === 0) {
return;
}
if (stat.q === 100) {
return <div style={{ color: '#00FF7F' }}>{stat.q}%</div>;
}
if (stat.q >= 95) {
return <div style={{ color: 'orange' }}>{stat.q}%</div>;
} else {
return <div style={{ color: 'red' }}>{stat.q}%</div>;
}
};
const EMSStatus: FC = () => {
const { data: data, send: loadData, error } = useRequest(EMSESP.readStatus);
const { LL } = useI18nContext();
const theme = useTheme();
const [confirmScan, setConfirmScan] = useState<boolean>(false);
const { me } = useContext(AuthenticatedContext);
const { send: scanDevices } = useRequest(EMSESP.scanDevices, {
immediate: false
});
const stats_theme = tableTheme({
Table: `
--data-table-library_grid-template-columns: repeat(1, minmax(0, 1fr)) 90px 90px 80px;
`,
BaseRow: `
font-size: 14px;
`,
HeaderRow: `
text-transform: uppercase;
background-color: black;
color: #90CAF9;
.th {
height: 36px;
border-bottom: 1px solid #565656;
}
`,
Row: `
.td {
padding: 8px;
border-top: 1px solid #565656;
border-bottom: 1px solid #565656;
}
&:nth-of-type(odd) .td {
background-color: #303030;
}
&:nth-of-type(even) .td {
background-color: #1e1e1e;
}
`,
BaseCell: `
&:not(:first-of-type) {
text-align: center;
}
`
});
useEffect(() => {
const timer = setInterval(() => loadData(), 30000);
return () => {
clearInterval(timer);
};
});
const showName = (id: any) => {
const name: keyof Translation['STATUS_NAMES'] = id;
return LL.STATUS_NAMES[name]();
};
const formatDurationSec = (duration_sec: number) => {
const days = Math.trunc((duration_sec * 1000) / 86400000);
const hours = Math.trunc((duration_sec * 1000) / 3600000) % 24;
const minutes = Math.trunc((duration_sec * 1000) / 60000) % 60;
const seconds = Math.trunc((duration_sec * 1000) / 1000) % 60;
let formatted = '';
if (days) {
formatted += LL.NUM_DAYS({ num: days }) + ' ';
}
if (hours) {
formatted += LL.NUM_HOURS({ num: hours }) + ' ';
}
if (minutes) {
formatted += LL.NUM_MINUTES({ num: minutes }) + ' ';
}
formatted += LL.NUM_SECONDS({ num: seconds });
return formatted;
};
const busStatus = () => {
if (data) {
switch (data.status) {
case busConnectionStatus.BUS_STATUS_CONNECTED:
return LL.CONNECTED(0) + ' (' + formatDurationSec(data.uptime) + ')';
case busConnectionStatus.BUS_STATUS_TX_ERRORS:
return LL.TX_ISSUES();
case busConnectionStatus.BUS_STATUS_OFFLINE:
return LL.DISCONNECTED();
}
}
return 'Unknown';
};
const scan = async () => {
await scanDevices()
.then(() => {
toast.info(LL.SCANNING() + '...');
})
.catch((err) => {
toast.error(err.message);
});
setConfirmScan(false);
};
const renderScanDialog = () => (
<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">
{LL.CANCEL()}
</Button>
<Button startIcon={<PermScanWifiIcon />} variant="outlined" onClick={scan} color="primary">
{LL.SCAN()}
</Button>
</DialogActions>
</Dialog>
);
const content = () => {
if (!data) {
return <FormLoader onRetry={loadData} errorMessage={error?.message} />;
}
return (
<>
<List>
<ListItem>
<ListItemAvatar>
<Avatar sx={{ bgcolor: busStatusHighlight(data, theme) }}>
<DirectionsBusIcon />
</Avatar>
</ListItemAvatar>
<ListItemText primary={LL.EMS_BUS_STATUS()} secondary={busStatus()} />
</ListItem>
<ListItem>
<ListItemAvatar>
<Avatar sx={{ bgcolor: theme.palette.success.main }}>
<DeviceHubIcon />
</Avatar>
</ListItemAvatar>
<ListItemText
primary={LL.ACTIVE_DEVICES()}
secondary={
LL.NUM_DEVICES({ num: data.num_devices }) +
', ' +
LL.NUM_TEMP_SENSORS({ num: data.num_sensors }) +
', ' +
LL.NUM_ANALOG_SENSORS({ num: data.num_analogs })
}
/>
</ListItem>
<Box m={3} />
<Table data={{ nodes: data.stats }} theme={stats_theme} layout={{ custom: true }}>
{(tableList: any) => (
<>
<Header>
<HeaderRow>
<HeaderCell resize />
<HeaderCell stiff>{LL.SUCCESS()}</HeaderCell>
<HeaderCell stiff>{LL.FAIL()}</HeaderCell>
<HeaderCell stiff>{LL.QUALITY()}</HeaderCell>
</HeaderRow>
</Header>
<Body>
{tableList.map((stat: Stat) => (
<Row key={stat.id} item={stat}>
<Cell>{showName(stat.id)}</Cell>
<Cell stiff>{Intl.NumberFormat().format(stat.s)}</Cell>
<Cell stiff>{Intl.NumberFormat().format(stat.f)}</Cell>
<Cell stiff>{showQuality(stat)}</Cell>
</Row>
))}
</Body>
</>
)}
</Table>
</List>
{renderScanDialog()}
<Box display="flex" flexWrap="wrap">
<Box flexGrow={1} sx={{ '& button': { mt: 2 } }}>
<Button startIcon={<RefreshIcon />} variant="outlined" color="secondary" onClick={loadData}>
{LL.REFRESH()}
</Button>
</Box>
{me.admin && (
<Box flexWrap="nowrap" whiteSpace="nowrap">
<ButtonRow>
<Button
startIcon={<PermScanWifiIcon />}
variant="outlined"
color="primary"
onClick={() => setConfirmScan(true)}
>
{LL.SCAN_DEVICES()}
</Button>
</ButtonRow>
</Box>
)}
</Box>
</>
);
};
return <SectionContent title={LL.EMS_BUS_STATUS_TITLE()}>{content()}</SectionContent>;
};
export default EMSStatus;

View File

@@ -1,7 +1,7 @@
import type {
APIcall,
Settings,
Status,
Activity,
CoreData,
Devices,
DeviceEntity,
@@ -25,7 +25,7 @@ export const readDeviceData = (id: number) =>
});
export const writeDeviceValue = (data: any) => alovaInstance.Post('/rest/writeDeviceValue', data);
// SettingsApplication
// Application Settings
export const readSettings = () => alovaInstance.Get<Settings>('/rest/settings');
export const writeSettings = (data: any) => alovaInstance.Post('/rest/settings', data);
export const getBoardProfile = (boardProfile: string) =>
@@ -33,17 +33,18 @@ export const getBoardProfile = (boardProfile: string) =>
params: { boardProfile }
});
// DashboardSensors
// Sensors
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);
// DashboardStatus
export const readStatus = () => alovaInstance.Get<Status>('/rest/status');
// Activity
export const readActivity = () => alovaInstance.Get<Activity>('/rest/activity');
export const scanDevices = () => alovaInstance.Post('/rest/scanDevices');
// HelpInformation
// API, used in HelpInformation
export const API = (apiCall: APIcall) => alovaInstance.Post('/api', apiCall);
// UploadFileForm

View File

@@ -50,13 +50,7 @@ export interface Stat {
q: number; // quality
}
export interface Status {
status: busConnectionStatus;
tx_mode: number;
uptime: number;
num_devices: number;
num_sensors: number;
num_analogs: number;
export interface Activity {
stats: Stat[];
}

View File

@@ -1,5 +1,6 @@
export interface SystemStatus {
// TODO fix this next
import type { busConnectionStatus } from 'project/types';
export interface ESPSystemStatus {
emsesp_version: string;
esp_platform: string;
max_alloc_heap: number;
@@ -17,13 +18,23 @@ export interface SystemStatus {
app_free: number;
fs_used: number;
fs_free: number;
uptime: string;
free_mem: number;
psram_size?: number;
free_psram?: number;
has_loader: boolean;
}
export interface SystemStatus {
emsesp_version: string;
esp_platform: string;
status: busConnectionStatus;
tx_mode: number;
uptime: number;
num_devices: number;
num_sensors: number;
num_analogs: number;
}
export interface OTASettings {
enabled: boolean;
port: number;