This commit is contained in:
MichaelDvP
2024-03-24 08:38:07 +01:00
114 changed files with 3073 additions and 2856 deletions

View File

@@ -20,7 +20,8 @@ import {
ValidatedTextField,
ButtonRow,
MessageBox,
BlockNavigation
BlockNavigation,
useLayoutTitle
} from 'components';
import RestartMonitor from 'framework/system/RestartMonitor';
@@ -36,7 +37,7 @@ export function boardProfileSelectItems() {
));
}
const SettingsApplication: FC = () => {
const ApplicationSettings: FC = () => {
const {
loadData,
saveData,
@@ -97,6 +98,8 @@ const SettingsApplication: FC = () => {
});
};
useLayoutTitle(LL.APPLICATION_SETTINGS());
const content = () => {
if (!data) {
return <FormLoader onRetry={loadData} errorMessage={errorMessage} />;
@@ -136,7 +139,7 @@ const SettingsApplication: FC = () => {
return (
<>
<Typography sx={{ pt: 2 }} variant="h6" color="primary">
<Typography sx={{ pb: 1 }} variant="h6" color="primary">
{LL.INTERFACE_BOARD_PROFILE()}
</Typography>
<Box color="warning.main">
@@ -680,11 +683,11 @@ const SettingsApplication: FC = () => {
};
return (
<SectionContent title={LL.APPLICATION_SETTINGS()} titleGutter>
<SectionContent>
{blocker ? <BlockNavigation blocker={blocker} /> : null}
{restarting ? <RestartMonitor /> : content()}
</SectionContent>
);
};
export default SettingsApplication;
export default ApplicationSettings;

View File

@@ -13,17 +13,17 @@ import { useBlocker } from 'react-router-dom';
import { toast } from 'react-toastify';
import SettingsCustomEntitiesDialog from './SettingsCustomEntitiesDialog';
import SettingsCustomEntitiesDialog from './CustomEntitiesDialog';
import * as EMSESP from './api';
import { DeviceValueTypeNames, DeviceValueUOM_s } from './types';
import { entityItemValidation } from './validators';
import type { EntityItem } from './types';
import type { FC } from 'react';
import { ButtonRow, FormLoader, SectionContent, BlockNavigation } from 'components';
import { ButtonRow, FormLoader, SectionContent, BlockNavigation, useLayoutTitle } from 'components';
import { useI18nContext } from 'i18n/i18n-react';
const SettingsCustomEntities: FC = () => {
const CustomEntities: FC = () => {
const { LL } = useI18nContext();
const [numChanges, setNumChanges] = useState<number>(0);
const blocker = useBlocker(numChanges !== 0);
@@ -31,6 +31,8 @@ const SettingsCustomEntities: FC = () => {
const [creating, setCreating] = useState<boolean>(false);
const [dialogOpen, setDialogOpen] = useState<boolean>(false);
useLayoutTitle(LL.CUSTOM_ENTITIES(0));
const {
data: entities,
send: fetchEntities,
@@ -246,7 +248,7 @@ const SettingsCustomEntities: FC = () => {
};
return (
<SectionContent title={LL.CUSTOM_ENTITIES(0)} titleGutter>
<SectionContent>
{blocker ? <BlockNavigation blocker={blocker} /> : null}
<Box mb={2} color="warning.main">
<Typography variant="body2">{LL.ENTITIES_HELP_1()}</Typography>
@@ -265,7 +267,7 @@ const SettingsCustomEntities: FC = () => {
/>
)}
<Box display="flex" flexWrap="wrap">
<Box mt={1} display="flex" flexWrap="wrap">
<Box flexGrow={1}>
{numChanges > 0 && (
<ButtonRow>
@@ -298,4 +300,4 @@ const SettingsCustomEntities: FC = () => {
);
};
export default SettingsCustomEntities;
export default CustomEntities;

View File

@@ -30,7 +30,7 @@ import { useI18nContext } from 'i18n/i18n-react';
import { numberValue, updateValue } from 'utils';
import { validate } from 'validators';
type SettingsCustomEntitiesDialogProps = {
type CustomEntitiesDialogProps = {
open: boolean;
creating: boolean;
onClose: () => void;
@@ -39,14 +39,14 @@ type SettingsCustomEntitiesDialogProps = {
validator: Schema;
};
const SettingsCustomEntitiesDialog = ({
const CustomEntitiesDialog = ({
open,
creating,
onClose,
onSave,
selectedItem,
validator
}: SettingsCustomEntitiesDialogProps) => {
}: CustomEntitiesDialogProps) => {
const { LL } = useI18nContext();
const [editItem, setEditItem] = useState<EntityItem>(selectedItem);
const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>();
@@ -281,4 +281,4 @@ const SettingsCustomEntitiesDialog = ({
);
};
export default SettingsCustomEntitiesDialog;
export default CustomEntitiesDialog;

View File

@@ -26,9 +26,9 @@ import { useState, useEffect, useCallback } from 'react';
import { useBlocker, useLocation } from 'react-router-dom';
import { toast } from 'react-toastify';
import SettingsCustomizationDialog from './CustomizationDialog';
import EntityMaskToggle from './EntityMaskToggle';
import OptionIcon from './OptionIcon';
import SettingsCustomizationDialog from './SettingsCustomizationDialog';
import * as EMSESP from './api';
@@ -37,14 +37,14 @@ import type { DeviceShort, DeviceEntity } from './types';
import type { FC } from 'react';
import { dialogStyle } from 'CustomTheme';
import * as SystemApi from 'api/system';
import { ButtonRow, SectionContent, MessageBox, BlockNavigation } from 'components';
import { ButtonRow, SectionContent, MessageBox, BlockNavigation, useLayoutTitle } from 'components';
import RestartMonitor from 'framework/system/RestartMonitor';
import { useI18nContext } from 'i18n/i18n-react';
export const APIURL = window.location.origin + '/api/';
const SettingsCustomization: FC = () => {
const Customization: FC = () => {
const { LL } = useI18nContext();
const [numChanges, setNumChanges] = useState<number>(0);
const blocker = useBlocker(numChanges !== 0);
@@ -58,6 +58,8 @@ const SettingsCustomization: FC = () => {
const [selectedDeviceEntity, setSelectedDeviceEntity] = useState<DeviceEntity>();
const [dialogOpen, setDialogOpen] = useState<boolean>(false);
useLayoutTitle(LL.CUSTOMIZATIONS());
// fetch devices first
const { data: devices } = useRequest(EMSESP.readDevices);
@@ -508,9 +510,6 @@ const SettingsCustomization: FC = () => {
const renderContent = () => (
<>
<Typography sx={{ pt: 2, pb: 2 }} variant="h6" color="primary">
{LL.DEVICE_ENTITIES()}
</Typography>
{devices && renderDeviceList()}
{deviceEntities && renderDeviceData()}
{restartNeeded && (
@@ -544,7 +543,7 @@ const SettingsCustomization: FC = () => {
</ButtonRow>
)}
</Box>
<ButtonRow>
<ButtonRow mt={1}>
<Button
startIcon={<SettingsBackupRestoreIcon />}
variant="outlined"
@@ -561,7 +560,7 @@ const SettingsCustomization: FC = () => {
);
return (
<SectionContent title={LL.CUSTOMIZATIONS()} titleGutter>
<SectionContent>
{blocker ? <BlockNavigation blocker={blocker} /> : null}
{restarting ? <RestartMonitor /> : renderContent()}
{selectedDeviceEntity && (
@@ -576,4 +575,4 @@ const SettingsCustomization: FC = () => {
);
};
export default SettingsCustomization;
export default Customization;

View File

@@ -31,7 +31,7 @@ type SettingsCustomizationDialogProps = {
selectedItem: DeviceEntity;
};
const SettingsCustomizationDialog = ({ 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);
@@ -152,4 +152,4 @@ const SettingsCustomizationDialog = ({ open, onClose, onSave, selectedItem }: Se
);
};
export default SettingsCustomizationDialog;
export default CustomizationDialog;

View File

@@ -1,37 +0,0 @@
import { Tab } from '@mui/material';
import { Navigate, Route, Routes } from 'react-router-dom';
import DashboardDevices from './DashboardDevices';
import DashboardSensors from './DashboardSensors';
import DashboardStatus from './DashboardStatus';
import type { FC } from 'react';
import { RouterTabs, useRouterTab, useLayoutTitle } from 'components';
import { useI18nContext } from 'i18n/i18n-react';
const Dashboard: FC = () => {
const { routerTab } = useRouterTab();
const { LL } = useI18nContext();
useLayoutTitle(LL.DASHBOARD());
return (
<>
<RouterTabs value={routerTab}>
<Tab value="/dashboard/devices" label={LL.DEVICES()} />
<Tab value="/dashboard/sensors" label={LL.SENSORS()} />
<Tab value="/dashboard/status" label="Status" />
</RouterTabs>
<Routes>
<Route path="devices" element={<DashboardDevices />} />
<Route path="sensors" element={<DashboardSensors />} />
<Route path="status" element={<DashboardStatus />} />
<Route path="*" element={<Navigate replace to="/dashboard/devices" />} />
</Routes>
</>
);
};
export default Dashboard;

View File

@@ -1,282 +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 DashboardStatus: 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>
<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>
<Box flexWrap="nowrap" whiteSpace="nowrap">
<ButtonRow>
<Button
startIcon={<PermScanWifiIcon />}
variant="outlined"
color="primary"
disabled={!me.admin}
onClick={() => setConfirmScan(true)}
>
{LL.SCAN_DEVICES()}
</Button>
</ButtonRow>
</Box>
</Box>
</>
);
};
return (
<SectionContent title={LL.EMS_BUS_STATUS_TITLE()} titleGutter>
{content()}
</SectionContent>
);
};
export default DashboardStatus;

View File

@@ -1,12 +1,13 @@
import PlaylistAddIcon from '@mui/icons-material/PlaylistAdd';
import { AiOutlineControl, AiOutlineGateway, AiOutlineAlert } from 'react-icons/ai';
import { CgSmartHomeBoiler } from 'react-icons/cg';
import { FaSolarPanel } from 'react-icons/fa';
import { GiHeatHaze, GiTap } from 'react-icons/gi';
import { MdThermostatAuto, MdOutlineSensors, MdOutlineExtension, MdOutlineDevices, MdOutlinePool } from 'react-icons/md';
import { MdThermostatAuto, MdOutlineSensors, MdOutlineDevices, MdOutlinePool } from 'react-icons/md';
import { TiFlowSwitch } from 'react-icons/ti';
import { VscVmConnect } from 'react-icons/vsc';
import { DeviceType } from './types';
import type { FC } from 'react';
interface DeviceIconProps {
@@ -45,7 +46,7 @@ const DeviceIcon: FC<DeviceIconProps> = ({ type_id }) => {
case DeviceType.POOL:
return <MdOutlinePool />;
case DeviceType.CUSTOM:
return <MdOutlineExtension />;
return <PlaylistAddIcon sx={{ color: 'lightblue', fontSize: 22, verticalAlign: 'middle' }} />;
default:
return null;
}

View File

@@ -33,13 +33,13 @@ import { useSort, SortToggleType } from '@table-library/react-table-library/sort
import { Table, Header, HeaderRow, HeaderCell, Body, Row, Cell } from '@table-library/react-table-library/table';
import { useTheme } from '@table-library/react-table-library/theme';
import { useRequest } from 'alova';
import { useState, useContext, useEffect, useCallback, useLayoutEffect } from 'react';
import { useState, useEffect, useCallback, useLayoutEffect, useContext } from 'react';
import { IconContext } from 'react-icons';
import { useNavigate } from 'react-router-dom';
import { toast } from 'react-toastify';
import DashboardDevicesDialog from './DashboardDevicesDialog';
import DeviceIcon from './DeviceIcon';
import DashboardDevicesDialog from './DevicesDialog';
import * as EMSESP from './api';
import { formatValue } from './deviceValue';
@@ -49,14 +49,15 @@ import { deviceValueItemValidation } from './validators';
import type { Device, DeviceValue } from './types';
import type { FC } from 'react';
import { dialogStyle } from 'CustomTheme';
import { ButtonRow, SectionContent, MessageBox } from 'components';
import { AuthenticatedContext } from 'contexts/authentication';
import { ButtonRow, SectionContent, MessageBox, useLayoutTitle } from 'components';
import { AuthenticatedContext } from 'contexts/authentication';
import { useI18nContext } from 'i18n/i18n-react';
const DashboardDevices: FC = () => {
const { me } = useContext(AuthenticatedContext);
const Devices: FC = () => {
const { LL } = useI18nContext();
const { me } = useContext(AuthenticatedContext);
const [size, setSize] = useState([0, 0]);
const [selectedDeviceValue, setSelectedDeviceValue] = useState<DeviceValue>();
const [onlyFav, setOnlyFav] = useState(false);
@@ -66,6 +67,8 @@ const DashboardDevices: FC = () => {
const navigate = useNavigate();
useLayoutTitle(LL.DEVICES());
const { data: coreData, send: readCoreData } = useRequest(() => EMSESP.readCoreData(), {
initialData: {
connected: true,
@@ -281,9 +284,9 @@ const DashboardDevices: FC = () => {
const customize = () => {
if (selectedDevice == 99) {
navigate('/settings/customentities');
navigate('/customentities');
} else {
navigate('/settings/customization', { state: selectedDevice });
navigate('/customizations', { state: selectedDevice });
}
};
@@ -420,11 +423,8 @@ const DashboardDevices: FC = () => {
};
const renderCoreData = () => (
<IconContext.Provider value={{ color: 'lightblue', size: '24', style: { verticalAlign: 'middle' } }}>
<IconContext.Provider value={{ color: 'lightblue', size: '18', style: { verticalAlign: 'middle' } }}>
{!coreData.connected && <MessageBox my={2} level="error" message={LL.EMS_BUS_WARNING()} />}
{/* {coreData.connected && coreData.devices.length === 0 && (
<MessageBox my={2} level="warning" message={LL.EMS_BUS_SCANNING()} />
)} */}
{coreData.connected && (
<Table data={{ nodes: coreData.devices }} select={device_select} theme={device_theme} layout={{ custom: true }}>
@@ -523,9 +523,11 @@ const DashboardDevices: FC = () => {
<IconButton onClick={() => setShowDeviceInfo(true)}>
<InfoOutlinedIcon color="primary" sx={{ fontSize: 18, verticalAlign: 'middle' }} />
</IconButton>
<IconButton onClick={customize}>
<FormatListNumberedIcon color="primary" sx={{ fontSize: 18, verticalAlign: 'middle' }} />
</IconButton>
{me.admin && (
<IconButton onClick={customize}>
<FormatListNumberedIcon sx={{ fontSize: 18, verticalAlign: 'middle' }} />
</IconButton>
)}
<IconButton onClick={handleDownloadCsv}>
<DownloadIcon color="primary" sx={{ fontSize: 18, verticalAlign: 'middle' }} />
</IconButton>
@@ -587,7 +589,7 @@ const DashboardDevices: FC = () => {
<Cell>{renderNameCell(dv)}</Cell>
<Cell>{formatValue(LL, dv.v, dv.u)}</Cell>
<Cell stiff>
{dv.c && me.admin && !hasMask(dv.id, DeviceEntityMask.DV_READONLY) && (
{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 }} />
@@ -608,7 +610,7 @@ const DashboardDevices: FC = () => {
};
return (
<SectionContent title={LL.DEVICE_DATA()} titleGutter id="devices-window">
<SectionContent id="devices-window">
{renderCoreData()}
{renderDeviceData()}
{renderDeviceDetails()}
@@ -619,15 +621,13 @@ const DashboardDevices: FC = () => {
onSave={deviceValueDialogSave}
selectedItem={selectedDeviceValue}
writeable={
me.admin &&
selectedDeviceValue.c !== undefined &&
!hasMask(selectedDeviceValue.id, DeviceEntityMask.DV_READONLY)
selectedDeviceValue.c !== undefined && !hasMask(selectedDeviceValue.id, DeviceEntityMask.DV_READONLY)
}
validator={deviceValueItemValidation(selectedDeviceValue)}
progress={submitting}
/>
)}
<ButtonRow>
<ButtonRow mt={1}>
<Button startIcon={<RefreshIcon />} variant="outlined" color="secondary" onClick={refreshData}>
{LL.REFRESH()}
</Button>
@@ -636,4 +636,4 @@ const DashboardDevices: FC = () => {
);
};
export default DashboardDevices;
export default Devices;

View File

@@ -40,7 +40,7 @@ type DashboardDevicesDialogProps = {
progress: boolean;
};
const DashboardDevicesDialog = ({
const DevicesDialog = ({
open,
onClose,
onSave,
@@ -204,4 +204,4 @@ const DashboardDevicesDialog = ({
);
};
export default DashboardDevicesDialog;
export default DevicesDialog;

View File

@@ -1,9 +1,19 @@
import CommentIcon from '@mui/icons-material/CommentTwoTone';
import EastIcon from '@mui/icons-material/East';
import DownloadIcon from '@mui/icons-material/GetApp';
import GitHubIcon from '@mui/icons-material/GitHub';
import MenuBookIcon from '@mui/icons-material/MenuBookTwoTone';
import { Box, List, ListItem, ListItemAvatar, ListItemText, Link, Typography, Button } from '@mui/material';
import {
Box,
List,
ListItem,
ListItemAvatar,
ListItemText,
Link,
Typography,
Button,
ListItemButton,
Avatar
} from '@mui/material';
import { useRequest } from 'alova';
import { toast } from 'react-toastify';
import type { FC } from 'react';
@@ -39,59 +49,56 @@ const Help: FC = () => {
};
return (
<SectionContent title={LL.SUPPORT_INFORMATION(0)} titleGutter>
<List>
<SectionContent>
<List sx={{ borderRadius: 3, border: '2px solid grey' }}>
<ListItem>
<ListItemAvatar>
<MenuBookIcon style={{ fontSize: 24, color: 'lightblue', verticalAlign: 'middle' }} />
</ListItemAvatar>
<ListItemText>
{LL.HELP_INFORMATION_1()}&nbsp;
<EastIcon style={{ fontSize: 24, color: 'lightblue', verticalAlign: 'middle' }} />
&nbsp;
<Link target="_blank" href="https://emsesp.github.io/docs" color="primary">
{LL.CLICK_HERE()}
</Link>
</ListItemText>
<ListItemButton component="a" href="https://emsesp.github.io/docs">
<ListItemAvatar>
<Avatar sx={{ bgcolor: '#72caf9' }}>
<MenuBookIcon />
</Avatar>
</ListItemAvatar>
<ListItemText primary={LL.HELP_INFORMATION_1()} />
</ListItemButton>
</ListItem>
<ListItem>
<ListItemAvatar>
<CommentIcon style={{ fontSize: 24, color: 'lightblue', verticalAlign: 'middle' }} />
</ListItemAvatar>
<ListItemText>
{LL.HELP_INFORMATION_2()}&nbsp;
<EastIcon style={{ fontSize: 24, color: 'lightblue', verticalAlign: 'middle' }} />
&nbsp;
<Link target="_blank" href="https://discord.gg/3J3GgnzpyT" color="primary">
{LL.CLICK_HERE()}
</Link>
</ListItemText>
<ListItemButton component="a" href="https://discord.gg/3J3GgnzpyT">
<ListItemAvatar>
<Avatar sx={{ bgcolor: '#72caf9' }}>
<CommentIcon />
</Avatar>
</ListItemAvatar>
<ListItemText primary={LL.HELP_INFORMATION_2()} />
</ListItemButton>
</ListItem>
<ListItem>
<ListItemAvatar>
<GitHubIcon style={{ fontSize: 24, color: 'lightblue', verticalAlign: 'middle' }} />
</ListItemAvatar>
<ListItemText>
{LL.HELP_INFORMATION_3()}&nbsp;
<EastIcon style={{ fontSize: 24, color: 'lightblue', verticalAlign: 'middle' }} />
<Link target="_blank" href="https://github.com/emsesp/EMS-ESP32/issues/new/choose" color="primary">
{LL.CLICK_HERE()}
</Link>
<br />
</ListItemText>
<ListItemButton component="a" href="https://github.com/emsesp/EMS-ESP32/issues/new/choose">
<ListItemAvatar>
<Avatar sx={{ bgcolor: '#72caf9' }}>
<GitHubIcon />
</Avatar>
</ListItemAvatar>
<ListItemText primary={LL.HELP_INFORMATION_3()} />
</ListItemButton>
</ListItem>
</List>
<Box color="warning.main">
<Box p={2} color="warning.main">
<Typography mb={1} variant="body2">
{LL.HELP_INFORMATION_4()}
</Typography>
<Button
startIcon={<DownloadIcon />}
variant="outlined"
color="primary"
onClick={() => callAPI('system', 'info')}
>
{LL.SUPPORT_INFORMATION(0)}
</Button>
</Box>
<Button startIcon={<DownloadIcon />} variant="outlined" color="primary" onClick={() => callAPI('system', 'info')}>
{LL.SUPPORT_INFORMATION(0)}
</Button>
<Button
sx={{ ml: 2 }}
startIcon={<DownloadIcon />}
@@ -102,7 +109,7 @@ const Help: FC = () => {
All Values
</Button>
<Box border={1} p={1} mt={4} color="orange">
<Box border={1} p={1} mt={4}>
<Typography align="center" variant="subtitle1" color="orange">
<b>{LL.HELP_INFORMATION_5()}</b>
</Typography>

View File

@@ -11,18 +11,18 @@ import { updateState, useRequest } from 'alova';
import { useState, useEffect, useCallback } from 'react';
import { useBlocker } from 'react-router-dom';
import { toast } from 'react-toastify';
import SettingsSchedulerDialog from './SettingsSchedulerDialog';
import SettingsSchedulerDialog from './SchedulerDialog';
import * as EMSESP from './api';
import { ScheduleFlag } from './types';
import { schedulerItemValidation } from './validators';
import type { ScheduleItem } from './types';
import type { FC } from 'react';
import { ButtonRow, FormLoader, SectionContent, BlockNavigation } from 'components';
import { ButtonRow, FormLoader, SectionContent, BlockNavigation, useLayoutTitle } from 'components';
import { useI18nContext } from 'i18n/i18n-react';
const SettingsScheduler: FC = () => {
const Scheduler: FC = () => {
const { LL, locale } = useI18nContext();
const [numChanges, setNumChanges] = useState<number>(0);
const blocker = useBlocker(numChanges !== 0);
@@ -194,6 +194,8 @@ const SettingsScheduler: FC = () => {
</>
);
useLayoutTitle(LL.SCHEDULER());
return (
<Table
data={{ nodes: schedule.filter((si) => !si.deleted).sort((a, b) => a.time.localeCompare(b.time)) }}
@@ -249,7 +251,7 @@ const SettingsScheduler: FC = () => {
};
return (
<SectionContent title={LL.SCHEDULER()} titleGutter>
<SectionContent>
{blocker ? <BlockNavigation blocker={blocker} /> : null}
<Box mb={2} color="warning.main">
<Typography variant="body2">{LL.SCHEDULER_HELP_1()}</Typography>
@@ -268,7 +270,7 @@ const SettingsScheduler: FC = () => {
/>
)}
<Box display="flex" flexWrap="wrap">
<Box mt={1} display="flex" flexWrap="wrap">
<Box flexGrow={1}>
{numChanges !== 0 && (
<ButtonRow>
@@ -298,4 +300,4 @@ const SettingsScheduler: FC = () => {
);
};
export default SettingsScheduler;
export default Scheduler;

View File

@@ -32,7 +32,7 @@ import { useI18nContext } from 'i18n/i18n-react';
import { updateValue } from 'utils';
import { validate } from 'validators';
type SettingsSchedulerDialogProps = {
type SchedulerDialogProps = {
open: boolean;
creating: boolean;
onClose: () => void;
@@ -42,15 +42,7 @@ type SettingsSchedulerDialogProps = {
dow: string[];
};
const SettingsSchedulerDialog = ({
open,
creating,
onClose,
onSave,
selectedItem,
validator,
dow
}: SettingsSchedulerDialogProps) => {
const SchedulerDialog = ({ open, creating, onClose, onSave, selectedItem, validator, dow }: SchedulerDialogProps) => {
const { LL } = useI18nContext();
const [editItem, setEditItem] = useState<ScheduleItem>(selectedItem);
const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>();
@@ -246,4 +238,4 @@ const SettingsSchedulerDialog = ({
);
};
export default SettingsSchedulerDialog;
export default SchedulerDialog;

View File

@@ -8,26 +8,27 @@ import { useSort, SortToggleType } from '@table-library/react-table-library/sort
import { Table, Header, HeaderRow, HeaderCell, Body, Row, Cell } from '@table-library/react-table-library/table';
import { useTheme } from '@table-library/react-table-library/theme';
import { useRequest } from 'alova';
import { useState, useContext, useEffect } from 'react';
import { useState, useEffect, useContext } from 'react';
import { toast } from 'react-toastify';
import DashboardSensorsAnalogDialog from './DashboardSensorsAnalogDialog';
import DashboardSensorsTemperatureDialog from './DashboardSensorsTemperatureDialog';
import DashboardSensorsAnalogDialog from './SensorsAnalogDialog';
import DashboardSensorsTemperatureDialog from './SensorsTemperatureDialog';
import * as EMSESP from './api';
import { DeviceValueUOM, DeviceValueUOM_s, AnalogTypeNames, AnalogType } from './types';
import { temperatureSensorItemValidation, analogSensorItemValidation } from './validators';
import type { TemperatureSensor, AnalogSensor } from './types';
import type { FC } from 'react';
import { ButtonRow, SectionContent } from 'components';
import { ButtonRow, SectionContent, useLayoutTitle } from 'components';
import { AuthenticatedContext } from 'contexts/authentication';
import { useI18nContext } from 'i18n/i18n-react';
const DashboardSensors: FC = () => {
const Sensors: FC = () => {
const { LL } = useI18nContext();
const { me } = useContext(AuthenticatedContext);
const [selectedTemperatureSensor, setSelectedTemperatureSensor] = useState<TemperatureSensor>();
const [selectedAnalogSensor, setSelectedAnalogSensor] = useState<AnalogSensor>();
const [temperatureDialogOpen, setTemperatureDialogOpen] = useState<boolean>(false);
@@ -51,8 +52,6 @@ const DashboardSensors: FC = () => {
immediate: false
});
const isAdmin = me.admin;
const common_theme = useTheme({
BaseRow: `
font-size: 14px;
@@ -170,6 +169,8 @@ const DashboardSensors: FC = () => {
};
});
useLayoutTitle(LL.SENSORS());
const formatDurationMin = (duration_min: number) => {
const days = Math.trunc((duration_min * 60000) / 86400000);
const hours = Math.trunc((duration_min * 60000) / 3600000) % 24;
@@ -220,7 +221,7 @@ const DashboardSensors: FC = () => {
}
const updateTemperatureSensor = (ts: TemperatureSensor) => {
if (isAdmin) {
if (me.admin) {
setSelectedTemperatureSensor(ts);
setTemperatureDialogOpen(true);
}
@@ -246,7 +247,7 @@ const DashboardSensors: FC = () => {
};
const updateAnalogSensor = (as: AnalogSensor) => {
if (isAdmin) {
if (me.admin) {
setCreating(false);
setSelectedAnalogSensor(as);
setAnalogDialogOpen(true);
@@ -406,25 +407,20 @@ const DashboardSensors: FC = () => {
);
return (
<SectionContent title={LL.SENSOR_DATA()} titleGutter>
{sensorData.ts.length > 0 && (
<>
<Typography sx={{ pt: 2, pb: 1 }} variant="h6" color="secondary">
{LL.TEMP_SENSORS()}
</Typography>
<RenderTemperatureSensors />
{selectedTemperatureSensor && (
<DashboardSensorsTemperatureDialog
open={temperatureDialogOpen}
onClose={onTemperatureDialogClose}
onSave={onTemperatureDialogSave}
selectedItem={selectedTemperatureSensor}
validator={temperatureSensorItemValidation()}
/>
)}
</>
<SectionContent>
<Typography sx={{ pb: 1 }} variant="h6" color="secondary">
{LL.TEMP_SENSORS()}
</Typography>
<RenderTemperatureSensors />
{selectedTemperatureSensor && (
<DashboardSensorsTemperatureDialog
open={temperatureDialogOpen}
onClose={onTemperatureDialogClose}
onSave={onTemperatureDialogSave}
selectedItem={selectedTemperatureSensor}
validator={temperatureSensorItemValidation()}
/>
)}
{sensorData?.analog_enabled === true && (
<>
<Typography sx={{ pt: 4, pb: 1 }} variant="h6" color="secondary">
@@ -443,15 +439,14 @@ const DashboardSensors: FC = () => {
)}
</>
)}
<ButtonRow>
<Box mt={2} display="flex" flexWrap="wrap">
<Box mt={1} display="flex" flexWrap="wrap">
<Box flexGrow={1}>
<Button startIcon={<RefreshIcon />} variant="outlined" color="secondary" onClick={fetchSensorData}>
{LL.REFRESH()}
</Button>
</Box>
{sensorData?.analog_enabled === true && (
{sensorData?.analog_enabled === true && me.admin && (
<Button
variant="outlined"
color="primary"
@@ -467,4 +462,4 @@ const DashboardSensors: FC = () => {
);
};
export default DashboardSensors;
export default Sensors;

View File

@@ -38,7 +38,7 @@ type DashboardSensorsAnalogDialogProps = {
validator: Schema;
};
const DashboardSensorsAnalogDialog = ({
const SensorsAnalogDialog = ({
open,
onClose,
onSave,
@@ -296,4 +296,4 @@ const DashboardSensorsAnalogDialog = ({
);
};
export default DashboardSensorsAnalogDialog;
export default SensorsAnalogDialog;

View File

@@ -26,7 +26,7 @@ import { numberValue, updateValue } from 'utils';
import { validate } from 'validators';
type DashboardSensorsTemperatureDialogProps = {
type SensorsTemperatureDialogProps = {
open: boolean;
onClose: () => void;
onSave: (ts: TemperatureSensor) => void;
@@ -34,13 +34,13 @@ type DashboardSensorsTemperatureDialogProps = {
validator: Schema;
};
const DashboardSensorsTemperatureDialog = ({
const SensorsTemperatureDialog = ({
open,
onClose,
onSave,
selectedItem,
validator
}: DashboardSensorsTemperatureDialogProps) => {
}: SensorsTemperatureDialogProps) => {
const { LL } = useI18nContext();
const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>();
const [editItem, setEditItem] = useState<TemperatureSensor>(selectedItem);
@@ -119,4 +119,4 @@ const DashboardSensorsTemperatureDialog = ({
);
};
export default DashboardSensorsTemperatureDialog;
export default SensorsTemperatureDialog;

View File

@@ -1,37 +0,0 @@
import { Tab } from '@mui/material';
import { Navigate, Route, Routes } from 'react-router-dom';
import SettingsApplication from './SettingsApplication';
import SettingsCustomEntities from './SettingsCustomEntities';
import SettingsCustomization from './SettingsCustomization';
import SettingsScheduler from './SettingsScheduler';
import type { FC } from 'react';
import { RouterTabs, useRouterTab, useLayoutTitle } from 'components';
import { useI18nContext } from 'i18n/i18n-react';
const Settings: FC = () => {
const { LL } = useI18nContext();
const { routerTab } = useRouterTab();
useLayoutTitle(LL.SETTINGS_OF(''));
return (
<>
<RouterTabs value={routerTab}>
<Tab value="/settings/application" label={LL.APPLICATION_SETTINGS()} />
<Tab value="/settings/customization" label={LL.CUSTOMIZATIONS()} />
<Tab value="/settings/scheduler" label={LL.SCHEDULER()} />
<Tab value="/settings/customentities" label={LL.CUSTOM_ENTITIES(0)} />
</RouterTabs>
<Routes>
<Route path="application" element={<SettingsApplication />} />
<Route path="customization" element={<SettingsCustomization />} />
<Route path="scheduler" element={<SettingsScheduler />} />
<Route path="customentities" element={<SettingsCustomEntities />} />
<Route path="*" element={<Navigate replace to="/settings/application" />} />
</Routes>
</>
);
};
export default Settings;

View File

@@ -0,0 +1,130 @@
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 { 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 { ButtonRow, FormLoader, SectionContent, useLayoutTitle } from 'components';
import { useI18nContext } from 'i18n/i18n-react';
const SystemActivity: FC = () => {
const { data: data, send: loadData, error } = useRequest(EMSESP.readActivity);
const { LL } = useI18nContext();
useLayoutTitle(LL.ACTIVITY());
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>
<ButtonRow mt={1}>
<Button startIcon={<RefreshIcon />} variant="outlined" color="secondary" onClick={loadData}>
{LL.REFRESH()}
</Button>
</ButtonRow>
</>
);
};
return <SectionContent>{content()}</SectionContent>;
};
export default SystemActivity;

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[];
}