mirror of
https://github.com/emsesp/EMS-ESP32.git
synced 2025-12-07 08:19:52 +03:00
Merge branch 'dev' of https://github.com/emsesp/EMS-ESP32 into dev
This commit is contained in:
@@ -25,7 +25,7 @@ const App = () => {
|
||||
<CustomTheme>
|
||||
<AppRouting />
|
||||
<ToastContainer
|
||||
position="bottom-right"
|
||||
position="bottom-left"
|
||||
autoClose={3000}
|
||||
hideProgressBar={false}
|
||||
newestOnTop={false}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { Navigate, Route, Routes } from 'react-router-dom';
|
||||
|
||||
import CustomEntities from 'app/main/CustomEntities';
|
||||
import Customizations from 'app/main/Customizations';
|
||||
import Dashboard from 'app/main/Dashboard';
|
||||
import Devices from 'app/main/Devices';
|
||||
import Help from 'app/main/Help';
|
||||
import Modules from 'app/main/Modules';
|
||||
@@ -32,6 +33,7 @@ const AuthenticatedRouting = () => {
|
||||
return (
|
||||
<Layout>
|
||||
<Routes>
|
||||
<Route path="/dashboard/*" element={<Dashboard />} />
|
||||
<Route path="/devices/*" element={<Devices />} />
|
||||
<Route path="/sensors/*" element={<Sensors />} />
|
||||
<Route path="/status/*" element={<Status />} />
|
||||
|
||||
@@ -5,6 +5,7 @@ import type {
|
||||
Action,
|
||||
Activity,
|
||||
CoreData,
|
||||
DashboardItem,
|
||||
DeviceData,
|
||||
DeviceEntity,
|
||||
Entities,
|
||||
@@ -19,7 +20,13 @@ import type {
|
||||
WriteTemperatureSensor
|
||||
} from '../app/main/types';
|
||||
|
||||
// DashboardDevices
|
||||
// Dashboard
|
||||
export const readDashboard = () =>
|
||||
alovaInstance.Get<DashboardItem[]>('/rest/dashboardData', {
|
||||
responseType: 'arraybuffer' // uses msgpack
|
||||
});
|
||||
|
||||
// Devices
|
||||
export const readCoreData = () => alovaInstance.Get<CoreData>(`/rest/coreData`);
|
||||
export const readDeviceData = (id: number) =>
|
||||
alovaInstance.Get<DeviceData>('/rest/deviceData', {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { useCallback, useState } from 'react';
|
||||
import { useBlocker } from 'react-router-dom';
|
||||
import { toast } from 'react-toastify';
|
||||
|
||||
@@ -27,6 +27,7 @@ import {
|
||||
useLayoutTitle
|
||||
} from 'components';
|
||||
import { useI18nContext } from 'i18n/i18n-react';
|
||||
import { useInterval } from 'utils';
|
||||
|
||||
import { readCustomEntities, writeCustomEntities } from '../../api/app';
|
||||
import SettingsCustomEntitiesDialog from './CustomEntitiesDialog';
|
||||
@@ -52,17 +53,11 @@ const CustomEntities = () => {
|
||||
initialData: []
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const timer = setInterval(async () => {
|
||||
if (dialogOpen || numChanges > 0) {
|
||||
return;
|
||||
}
|
||||
await fetchEntities();
|
||||
}, 2000);
|
||||
return () => {
|
||||
clearInterval(timer);
|
||||
};
|
||||
});
|
||||
useInterval(() => {
|
||||
if (!dialogOpen && !numChanges) {
|
||||
void fetchEntities();
|
||||
}
|
||||
}, 3000);
|
||||
|
||||
const { send: writeEntities } = useRequest(
|
||||
(data: Entities) => writeCustomEntities(data),
|
||||
@@ -130,15 +125,10 @@ const CustomEntities = () => {
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
.td {
|
||||
border-top: 1px solid #565656;
|
||||
border-bottom: 1px solid #565656;
|
||||
}
|
||||
&:hover .td {
|
||||
border-top: 1px solid #177ac9;
|
||||
border-bottom: 1px solid #177ac9;
|
||||
}
|
||||
&:nth-of-type(odd) .td {
|
||||
background-color: #303030;
|
||||
background-color: #177ac9;
|
||||
}
|
||||
`
|
||||
});
|
||||
@@ -295,7 +285,7 @@ const CustomEntities = () => {
|
||||
<SectionContent>
|
||||
{blocker ? <BlockNavigation blocker={blocker} /> : null}
|
||||
<Box mb={2} color="warning.main">
|
||||
<Typography variant="body2">{LL.ENTITIES_HELP_1()}</Typography>
|
||||
<Typography variant="body1">{LL.ENTITIES_HELP_1()}</Typography>
|
||||
</Box>
|
||||
|
||||
{renderEntity()}
|
||||
|
||||
@@ -190,10 +190,7 @@ const Customizations = () => {
|
||||
}
|
||||
&:hover .td {
|
||||
border-top: 1px solid #177ac9;
|
||||
border-bottom: 1px solid #177ac9;
|
||||
}
|
||||
&:nth-of-type(odd) .td {
|
||||
background-color: #303030;
|
||||
background-color: #177ac9;
|
||||
}
|
||||
`,
|
||||
Cell: `
|
||||
@@ -427,7 +424,7 @@ const Customizations = () => {
|
||||
const renderDeviceList = () => (
|
||||
<>
|
||||
<Box mb={1} color="warning.main">
|
||||
<Typography variant="body2">{LL.CUSTOMIZATIONS_HELP_1()}.</Typography>
|
||||
<Typography variant="body1">{LL.CUSTOMIZATIONS_HELP_1()}.</Typography>
|
||||
</Box>
|
||||
<Box display="flex" flexWrap="wrap" alignItems="center" gap={2}>
|
||||
{rename ? (
|
||||
|
||||
363
interface/src/app/main/Dashboard.tsx
Normal file
363
interface/src/app/main/Dashboard.tsx
Normal file
@@ -0,0 +1,363 @@
|
||||
import { useContext, useEffect, useState } from 'react';
|
||||
import { IconContext } from 'react-icons/lib';
|
||||
import { toast } from 'react-toastify';
|
||||
|
||||
import ChevronRightIcon from '@mui/icons-material/ChevronRight';
|
||||
import EditIcon from '@mui/icons-material/Edit';
|
||||
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
|
||||
import UnfoldLessIcon from '@mui/icons-material/UnfoldLess';
|
||||
import UnfoldMoreIcon from '@mui/icons-material/UnfoldMore';
|
||||
import {
|
||||
Box,
|
||||
IconButton,
|
||||
ToggleButton,
|
||||
ToggleButtonGroup,
|
||||
Tooltip,
|
||||
Typography
|
||||
} from '@mui/material';
|
||||
import Grid from '@mui/material/Grid2';
|
||||
|
||||
import { Body, Cell, Row, Table } from '@table-library/react-table-library/table';
|
||||
import { useTheme } from '@table-library/react-table-library/theme';
|
||||
import { CellTree, useTree } from '@table-library/react-table-library/tree';
|
||||
import { useRequest } from 'alova/client';
|
||||
import { FormLoader, SectionContent, useLayoutTitle } from 'components';
|
||||
import { AuthenticatedContext } from 'contexts/authentication';
|
||||
import { useI18nContext } from 'i18n/i18n-react';
|
||||
import { useInterval, usePersistState } from 'utils';
|
||||
|
||||
import { readDashboard, writeDeviceValue } from '../../api/app';
|
||||
import DeviceIcon from './DeviceIcon';
|
||||
import DevicesDialog from './DevicesDialog';
|
||||
import { formatValue } from './deviceValue';
|
||||
import {
|
||||
type DashboardItem,
|
||||
DeviceEntityMask,
|
||||
DeviceType,
|
||||
type DeviceValue
|
||||
} from './types';
|
||||
import { deviceValueItemValidation } from './validators';
|
||||
|
||||
const Dashboard = () => {
|
||||
const { LL } = useI18nContext();
|
||||
const { me } = useContext(AuthenticatedContext);
|
||||
|
||||
useLayoutTitle(LL.DASHBOARD());
|
||||
|
||||
const [showAll, setShowAll] = usePersistState(true, 'showAll');
|
||||
|
||||
const [deviceValueDialogOpen, setDeviceValueDialogOpen] = useState<boolean>(false);
|
||||
const [parentNodes, setParentNodes] = useState<number>(0);
|
||||
const [selectedDashboardItem, setSelectedDashboardItem] =
|
||||
useState<DashboardItem>();
|
||||
|
||||
const {
|
||||
data,
|
||||
send: fetchDashboard,
|
||||
error,
|
||||
loading
|
||||
} = useRequest(readDashboard, {
|
||||
initialData: []
|
||||
}).onSuccess((event) => {
|
||||
if (event.data.length !== parentNodes) {
|
||||
setParentNodes(event.data.length); // count number of parents/devices
|
||||
}
|
||||
});
|
||||
|
||||
const { loading: submitting, send: sendDeviceValue } = useRequest(
|
||||
(data: { id: number; c: string; v: unknown }) => writeDeviceValue(data),
|
||||
{
|
||||
immediate: false
|
||||
}
|
||||
);
|
||||
|
||||
const deviceValueDialogSave = async (devicevalue: DeviceValue) => {
|
||||
if (!selectedDashboardItem) {
|
||||
return;
|
||||
}
|
||||
const id = selectedDashboardItem.parentNode.id; // this is the parent ID
|
||||
await sendDeviceValue({ id, c: devicevalue.c ?? '', v: devicevalue.v })
|
||||
.then(() => {
|
||||
toast.success(LL.WRITE_CMD_SENT());
|
||||
})
|
||||
.catch((error: Error) => {
|
||||
toast.error(error.message);
|
||||
})
|
||||
.finally(() => {
|
||||
setDeviceValueDialogOpen(false);
|
||||
setSelectedDashboardItem(undefined);
|
||||
});
|
||||
};
|
||||
|
||||
const dashboard_theme = useTheme({
|
||||
Table: `
|
||||
--data-table-library_grid-template-columns: minmax(80px, auto) 120px 32px;
|
||||
`,
|
||||
BaseRow: `
|
||||
font-size: 14px;
|
||||
.td {
|
||||
height: 28px;
|
||||
}
|
||||
`,
|
||||
Row: `
|
||||
cursor: pointer;
|
||||
background-color: #1e1e1e;
|
||||
&:hover .td {
|
||||
background-color: #177ac9;
|
||||
}
|
||||
`,
|
||||
BaseCell: `
|
||||
&:nth-of-type(2) {
|
||||
text-align: right;
|
||||
}
|
||||
&:nth-of-type(3) {
|
||||
text-align: right;
|
||||
}
|
||||
`
|
||||
});
|
||||
|
||||
const tree = useTree(
|
||||
{ nodes: data },
|
||||
{
|
||||
onChange: undefined // not used but needed
|
||||
},
|
||||
{
|
||||
treeIcon: {
|
||||
margin: '4px',
|
||||
iconDefault: null,
|
||||
iconRight: (
|
||||
<ChevronRightIcon
|
||||
sx={{ fontSize: 16, verticalAlign: 'middle' }}
|
||||
color="info"
|
||||
/>
|
||||
),
|
||||
iconDown: (
|
||||
<ExpandMoreIcon
|
||||
sx={{ fontSize: 16, verticalAlign: 'middle' }}
|
||||
color="info"
|
||||
/>
|
||||
)
|
||||
},
|
||||
indentation: 45
|
||||
}
|
||||
);
|
||||
|
||||
useInterval(() => {
|
||||
if (!deviceValueDialogOpen) {
|
||||
void fetchDashboard();
|
||||
}
|
||||
}, 3000);
|
||||
|
||||
useEffect(() => {
|
||||
showAll
|
||||
? tree.fns.onAddAll(data.map((item: DashboardItem) => item.id)) // expand tree
|
||||
: tree.fns.onRemoveAll(); // collapse tree
|
||||
}, [parentNodes]);
|
||||
|
||||
const showType = (n?: string, t?: number) => {
|
||||
// if we have a name show it
|
||||
if (n) {
|
||||
return n;
|
||||
}
|
||||
if (t) {
|
||||
// otherwise pick translation based on type
|
||||
switch (t) {
|
||||
case DeviceType.CUSTOM:
|
||||
return LL.CUSTOM_ENTITIES(0);
|
||||
case DeviceType.ANALOGSENSOR:
|
||||
return LL.ANALOG_SENSORS();
|
||||
case DeviceType.TEMPERATURESENSOR:
|
||||
return LL.TEMP_SENSORS();
|
||||
case DeviceType.SCHEDULER:
|
||||
return LL.SCHEDULER();
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
return '';
|
||||
};
|
||||
|
||||
const showName = (di: DashboardItem) => {
|
||||
if (di.id < 100) {
|
||||
// if its a device (parent node) and has entities
|
||||
if (di.nodes?.length) {
|
||||
return (
|
||||
<>
|
||||
<span style="font-size: 14px">
|
||||
<DeviceIcon type_id={di.t ?? 0} />
|
||||
{showType(di.n, di.t)}
|
||||
</span>
|
||||
<span style={{ color: 'lightblue' }}> ({di.nodes?.length})</span>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
if (di.dv) {
|
||||
return <span style="color:lightgrey">{di.dv.id.slice(2)}</span>;
|
||||
}
|
||||
};
|
||||
|
||||
const hasMask = (id: string, mask: number) =>
|
||||
(parseInt(id.slice(0, 2), 16) & mask) === mask;
|
||||
|
||||
const editDashboardValue = (di: DashboardItem) => {
|
||||
if (me.admin && di.dv?.c) {
|
||||
setSelectedDashboardItem(di);
|
||||
setDeviceValueDialogOpen(true);
|
||||
}
|
||||
};
|
||||
|
||||
const handleShowAll = (
|
||||
event: React.MouseEvent<HTMLElement>,
|
||||
toggle: boolean | null
|
||||
) => {
|
||||
if (toggle !== null) {
|
||||
tree.fns.onToggleAll({});
|
||||
setShowAll(toggle);
|
||||
}
|
||||
};
|
||||
|
||||
const renderContent = () => {
|
||||
if (!data) {
|
||||
return <FormLoader onRetry={fetchDashboard} errorMessage={error?.message} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Box
|
||||
sx={{
|
||||
backgroundColor: 'black',
|
||||
pt: 1,
|
||||
pl: 2
|
||||
}}
|
||||
>
|
||||
<Grid container spacing={0} justifyContent="flex-start">
|
||||
<Grid size={11}>
|
||||
<Typography mb={2} variant="body1" color="warning">
|
||||
{LL.DASHBOARD_1()}
|
||||
</Typography>
|
||||
</Grid>
|
||||
|
||||
<Grid size={1} alignItems="end">
|
||||
<ToggleButtonGroup
|
||||
color="primary"
|
||||
size="small"
|
||||
value={showAll}
|
||||
exclusive
|
||||
onChange={handleShowAll}
|
||||
>
|
||||
<ToggleButton value={true}>
|
||||
<UnfoldMoreIcon sx={{ fontSize: 14 }} />
|
||||
</ToggleButton>
|
||||
<ToggleButton value={false}>
|
||||
<UnfoldLessIcon sx={{ fontSize: 14 }} />
|
||||
</ToggleButton>
|
||||
</ToggleButtonGroup>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Box>
|
||||
|
||||
<Box
|
||||
padding={1}
|
||||
justifyContent="center"
|
||||
flexDirection="column"
|
||||
sx={{
|
||||
borderRadius: 1,
|
||||
border: '1px solid grey'
|
||||
}}
|
||||
>
|
||||
<IconContext.Provider
|
||||
value={{
|
||||
color: 'lightblue',
|
||||
size: '16',
|
||||
style: { verticalAlign: 'middle' }
|
||||
}}
|
||||
>
|
||||
{!loading && data.length === 0 ? (
|
||||
<Typography variant="subtitle2" color="warning">
|
||||
{LL.NO_DATA()}
|
||||
</Typography>
|
||||
) : (
|
||||
<Table
|
||||
data={{ nodes: data }}
|
||||
theme={dashboard_theme}
|
||||
layout={{ custom: true }}
|
||||
tree={tree}
|
||||
>
|
||||
{(tableList: DashboardItem[]) => (
|
||||
<Body>
|
||||
{tableList.map((di: DashboardItem) => (
|
||||
<Row
|
||||
key={di.id}
|
||||
item={di}
|
||||
onClick={() => editDashboardValue(di)}
|
||||
>
|
||||
{di.id > 99 ? (
|
||||
<>
|
||||
<Cell>{showName(di)}</Cell>
|
||||
<Cell>
|
||||
<Tooltip
|
||||
placement="left"
|
||||
title={formatValue(LL, di.dv?.v, di.dv?.u)}
|
||||
arrow
|
||||
>
|
||||
<span style={{ color: 'lightgrey' }}>
|
||||
{formatValue(LL, di.dv?.v, di.dv?.u)}
|
||||
</span>
|
||||
</Tooltip>
|
||||
</Cell>
|
||||
|
||||
<Cell>
|
||||
{me.admin &&
|
||||
di.dv?.c &&
|
||||
!hasMask(di.dv.id, DeviceEntityMask.DV_READONLY) && (
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={() => editDashboardValue(di)}
|
||||
>
|
||||
<EditIcon
|
||||
color="primary"
|
||||
sx={{ fontSize: 16 }}
|
||||
/>
|
||||
</IconButton>
|
||||
)}
|
||||
</Cell>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<CellTree item={di}>{showName(di)}</CellTree>
|
||||
<Cell />
|
||||
<Cell />
|
||||
</>
|
||||
)}
|
||||
</Row>
|
||||
))}
|
||||
</Body>
|
||||
)}
|
||||
</Table>
|
||||
)}
|
||||
</IconContext.Provider>
|
||||
</Box>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<SectionContent>
|
||||
{renderContent()}
|
||||
{selectedDashboardItem && selectedDashboardItem.dv && (
|
||||
<DevicesDialog
|
||||
open={deviceValueDialogOpen}
|
||||
onClose={() => setDeviceValueDialogOpen(false)}
|
||||
onSave={deviceValueDialogSave}
|
||||
selectedItem={selectedDashboardItem.dv}
|
||||
writeable={true}
|
||||
validator={deviceValueItemValidation(selectedDashboardItem.dv)}
|
||||
progress={submitting}
|
||||
/>
|
||||
)}
|
||||
</SectionContent>
|
||||
);
|
||||
};
|
||||
|
||||
export default Dashboard;
|
||||
@@ -2,59 +2,52 @@ 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 { MdPlaylistAdd } from 'react-icons/md';
|
||||
import { MdMoreTime } from 'react-icons/md';
|
||||
import {
|
||||
MdOutlineDevices,
|
||||
MdOutlinePool,
|
||||
MdOutlineSensors,
|
||||
MdThermostatAuto
|
||||
} from 'react-icons/md';
|
||||
import { TiFlowSwitch } from 'react-icons/ti';
|
||||
import { PiFan, PiGauge } from 'react-icons/pi';
|
||||
import { TiFlowSwitch, TiThermometer } from 'react-icons/ti';
|
||||
import { VscVmConnect } from 'react-icons/vsc';
|
||||
|
||||
import PlaylistAddIcon from '@mui/icons-material/PlaylistAdd';
|
||||
import type { SvgIconProps } from '@mui/material';
|
||||
|
||||
import { DeviceType } from './types';
|
||||
|
||||
const deviceIconLookup: {
|
||||
[key in DeviceType]: React.ComponentType<SvgIconProps> | undefined;
|
||||
} = {
|
||||
[DeviceType.TEMPERATURESENSOR]: TiThermometer,
|
||||
[DeviceType.ANALOGSENSOR]: PiGauge,
|
||||
[DeviceType.BOILER]: CgSmartHomeBoiler,
|
||||
[DeviceType.HEATSOURCE]: CgSmartHomeBoiler,
|
||||
[DeviceType.THERMOSTAT]: MdThermostatAuto,
|
||||
[DeviceType.MIXER]: AiOutlineControl,
|
||||
[DeviceType.SOLAR]: FaSolarPanel,
|
||||
[DeviceType.HEATPUMP]: GiHeatHaze,
|
||||
[DeviceType.GATEWAY]: AiOutlineGateway,
|
||||
[DeviceType.SWITCH]: TiFlowSwitch,
|
||||
[DeviceType.CONTROLLER]: VscVmConnect,
|
||||
[DeviceType.CONNECT]: VscVmConnect,
|
||||
[DeviceType.ALERT]: AiOutlineAlert,
|
||||
[DeviceType.EXTENSION]: MdOutlineDevices,
|
||||
[DeviceType.WATER]: GiTap,
|
||||
[DeviceType.POOL]: MdOutlinePool,
|
||||
[DeviceType.CUSTOM]: MdPlaylistAdd,
|
||||
[DeviceType.UNKNOWN]: MdOutlineSensors,
|
||||
[DeviceType.SYSTEM]: undefined,
|
||||
[DeviceType.SCHEDULER]: MdMoreTime,
|
||||
[DeviceType.GENERIC]: MdOutlineSensors,
|
||||
[DeviceType.VENTILATION]: PiFan
|
||||
};
|
||||
|
||||
const DeviceIcon = ({ type_id }: { type_id: DeviceType }) => {
|
||||
switch (type_id) {
|
||||
case DeviceType.TEMPERATURESENSOR:
|
||||
case DeviceType.ANALOGSENSOR:
|
||||
return <MdOutlineSensors />;
|
||||
case DeviceType.BOILER:
|
||||
case DeviceType.HEATSOURCE:
|
||||
return <CgSmartHomeBoiler />;
|
||||
case DeviceType.THERMOSTAT:
|
||||
return <MdThermostatAuto />;
|
||||
case DeviceType.MIXER:
|
||||
return <AiOutlineControl />;
|
||||
case DeviceType.SOLAR:
|
||||
return <FaSolarPanel />;
|
||||
case DeviceType.HEATPUMP:
|
||||
return <GiHeatHaze />;
|
||||
case DeviceType.GATEWAY:
|
||||
return <AiOutlineGateway />;
|
||||
case DeviceType.SWITCH:
|
||||
return <TiFlowSwitch />;
|
||||
case DeviceType.CONTROLLER:
|
||||
case DeviceType.CONNECT:
|
||||
return <VscVmConnect />;
|
||||
case DeviceType.ALERT:
|
||||
return <AiOutlineAlert />;
|
||||
case DeviceType.EXTENSION:
|
||||
return <MdOutlineDevices />;
|
||||
case DeviceType.WATER:
|
||||
return <GiTap />;
|
||||
case DeviceType.POOL:
|
||||
return <MdOutlinePool />;
|
||||
case DeviceType.CUSTOM:
|
||||
return (
|
||||
<PlaylistAddIcon
|
||||
sx={{ color: 'lightblue', fontSize: 22, verticalAlign: 'middle' }}
|
||||
/>
|
||||
);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
const Icon = deviceIconLookup[type_id];
|
||||
return Icon ? <Icon /> : null;
|
||||
};
|
||||
|
||||
export default DeviceIcon;
|
||||
|
||||
@@ -60,10 +60,11 @@ import { useRequest } from 'alova/client';
|
||||
import { MessageBox, SectionContent, useLayoutTitle } from 'components';
|
||||
import { AuthenticatedContext } from 'contexts/authentication';
|
||||
import { useI18nContext } from 'i18n/i18n-react';
|
||||
import { useInterval } from 'utils';
|
||||
|
||||
import { readCoreData, readDeviceData, writeDeviceValue } from '../../api/app';
|
||||
import DeviceIcon from './DeviceIcon';
|
||||
import DashboardDevicesDialog from './DevicesDialog';
|
||||
import DevicesDialog from './DevicesDialog';
|
||||
import { formatValue } from './deviceValue';
|
||||
import { DeviceEntityMask, DeviceType, DeviceValueUOM_s } from './types';
|
||||
import type { Device, DeviceValue } from './types';
|
||||
@@ -77,7 +78,7 @@ const Devices = () => {
|
||||
const [selectedDeviceValue, setSelectedDeviceValue] = useState<DeviceValue>();
|
||||
const [onlyFav, setOnlyFav] = useState(false);
|
||||
const [deviceValueDialogOpen, setDeviceValueDialogOpen] = useState(false);
|
||||
const [showDeviceInfo, setShowDeviceInfo] = useState<boolean>(false);
|
||||
const [showDeviceInfo, setShowDeviceInfo] = useState(false);
|
||||
const [selectedDevice, setSelectedDevice] = useState<number>();
|
||||
|
||||
const navigate = useNavigate();
|
||||
@@ -95,7 +96,7 @@ const Devices = () => {
|
||||
(id: number) => readDeviceData(id),
|
||||
{
|
||||
initialData: {
|
||||
data: []
|
||||
nodes: []
|
||||
},
|
||||
immediate: false
|
||||
}
|
||||
@@ -147,22 +148,15 @@ const Devices = () => {
|
||||
}
|
||||
`,
|
||||
Row: `
|
||||
background-color: #1E1E1E;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
background-color: #1E1E1E;
|
||||
.td {
|
||||
padding: 8px;
|
||||
border-top: 1px solid #565656;
|
||||
border-bottom: 1px solid #565656;
|
||||
}
|
||||
&.tr.tr-body.row-select.row-select-single-selected {
|
||||
background-color: #3d4752;
|
||||
background-color: #177ac9;
|
||||
font-weight: normal;
|
||||
}
|
||||
&:hover .td {
|
||||
border-top: 1px solid #177ac9;
|
||||
border-bottom: 1px solid #177ac9;
|
||||
}
|
||||
`
|
||||
});
|
||||
|
||||
@@ -170,17 +164,21 @@ const Devices = () => {
|
||||
common_theme,
|
||||
{
|
||||
Table: `
|
||||
--data-table-library_grid-template-columns: 40px repeat(1, minmax(0, 1fr)) 130px;
|
||||
--data-table-library_grid-template-columns: repeat(1, minmax(0, 1fr)) 130px;
|
||||
`,
|
||||
BaseRow: `
|
||||
.td {
|
||||
height: 42px;
|
||||
}
|
||||
// .td {
|
||||
// height: 42px;
|
||||
// }
|
||||
`,
|
||||
HeaderRow: `
|
||||
.th {
|
||||
padding: 8px;
|
||||
height: 36px;
|
||||
`,
|
||||
Row: `
|
||||
&:hover .td {
|
||||
background-color: #177ac9;
|
||||
`
|
||||
}
|
||||
]);
|
||||
@@ -221,7 +219,10 @@ const Devices = () => {
|
||||
Row: `
|
||||
&:nth-of-type(odd) .td {
|
||||
background-color: #303030;
|
||||
}
|
||||
},
|
||||
&:hover .td {
|
||||
background-color: #177ac9;
|
||||
}
|
||||
`
|
||||
}
|
||||
]);
|
||||
@@ -251,7 +252,7 @@ const Devices = () => {
|
||||
};
|
||||
|
||||
const dv_sort = useSort(
|
||||
{ nodes: deviceData.data },
|
||||
{ nodes: deviceData.nodes },
|
||||
{},
|
||||
{
|
||||
sortIcon: {
|
||||
@@ -383,8 +384,8 @@ const Devices = () => {
|
||||
];
|
||||
|
||||
const data = onlyFav
|
||||
? deviceData.data.filter((dv) => hasMask(dv.id, DeviceEntityMask.DV_FAVORITE))
|
||||
: deviceData.data;
|
||||
? deviceData.nodes.filter((dv) => hasMask(dv.id, DeviceEntityMask.DV_FAVORITE))
|
||||
: deviceData.nodes;
|
||||
|
||||
const csvData = data.reduce(
|
||||
(csvString: string, rowItem: DeviceValue) =>
|
||||
@@ -418,17 +419,11 @@ const Devices = () => {
|
||||
downloadBlob(new Blob([csvData], { type: 'text/csv;charset:utf-8' }));
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const timer = setInterval(() => {
|
||||
if (deviceValueDialogOpen) {
|
||||
return;
|
||||
}
|
||||
useInterval(() => {
|
||||
if (!deviceValueDialogOpen) {
|
||||
selectedDevice ? void sendDeviceData(selectedDevice) : void sendCoreData();
|
||||
}, 2000);
|
||||
return () => {
|
||||
clearInterval(timer);
|
||||
};
|
||||
});
|
||||
}
|
||||
}, 3000);
|
||||
|
||||
const deviceValueDialogSave = async (devicevalue: DeviceValue) => {
|
||||
const id = Number(device_select.state.id);
|
||||
@@ -527,57 +522,57 @@ const Devices = () => {
|
||||
};
|
||||
|
||||
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 }}
|
||||
>
|
||||
{(tableList: Device[]) => (
|
||||
<>
|
||||
<Header>
|
||||
<HeaderRow>
|
||||
<HeaderCell stiff />
|
||||
<HeaderCell resize>{LL.DESCRIPTION()}</HeaderCell>
|
||||
<HeaderCell stiff>{LL.TYPE(0)}</HeaderCell>
|
||||
</HeaderRow>
|
||||
</Header>
|
||||
<Body>
|
||||
{tableList.length === 0 && (
|
||||
<CircularProgress sx={{ margin: 1 }} size={18} />
|
||||
)}
|
||||
{tableList.map((device: Device) => (
|
||||
<Row key={device.id} item={device}>
|
||||
<Cell stiff>
|
||||
<DeviceIcon type_id={device.t} />
|
||||
</Cell>
|
||||
<Cell>
|
||||
{device.n}
|
||||
<span style={{ color: 'lightblue' }}>
|
||||
({device.e})
|
||||
</span>
|
||||
</Cell>
|
||||
<Cell stiff>{device.tn}</Cell>
|
||||
</Row>
|
||||
))}
|
||||
</Body>
|
||||
</>
|
||||
)}
|
||||
</Table>
|
||||
)}
|
||||
</IconContext.Provider>
|
||||
{coreData.connected && (
|
||||
<Table
|
||||
data={{ nodes: coreData.devices }}
|
||||
select={device_select}
|
||||
theme={device_theme}
|
||||
layout={{ custom: true }}
|
||||
>
|
||||
{(tableList: Device[]) => (
|
||||
<>
|
||||
<Header>
|
||||
<HeaderRow>
|
||||
<HeaderCell resize>{LL.DESCRIPTION()}</HeaderCell>
|
||||
<HeaderCell stiff>{LL.TYPE(0)}</HeaderCell>
|
||||
</HeaderRow>
|
||||
</Header>
|
||||
<Body>
|
||||
{tableList.length === 0 && (
|
||||
<CircularProgress sx={{ margin: 1 }} size={18} />
|
||||
)}
|
||||
{tableList.map((device: Device) => (
|
||||
<Row key={device.id} item={device}>
|
||||
<Cell>
|
||||
<DeviceIcon type_id={device.t} />
|
||||
|
||||
{device.n}
|
||||
<span style={{ color: 'lightblue' }}>
|
||||
({device.e})
|
||||
</span>
|
||||
</Cell>
|
||||
<Cell stiff>{device.tn}</Cell>
|
||||
</Row>
|
||||
))}
|
||||
</Body>
|
||||
</>
|
||||
)}
|
||||
</Table>
|
||||
)}
|
||||
</IconContext.Provider>
|
||||
</>
|
||||
);
|
||||
|
||||
const deviceValueDialogClose = () => {
|
||||
@@ -611,8 +606,8 @@ const Devices = () => {
|
||||
);
|
||||
|
||||
const shown_data = onlyFav
|
||||
? deviceData.data.filter((dv) => hasMask(dv.id, DeviceEntityMask.DV_FAVORITE))
|
||||
: deviceData.data;
|
||||
? deviceData.nodes.filter((dv) => hasMask(dv.id, DeviceEntityMask.DV_FAVORITE))
|
||||
: deviceData.nodes;
|
||||
|
||||
const deviceIndex = coreData.devices.findIndex(
|
||||
(d) => d.id === device_select.state.id
|
||||
@@ -733,7 +728,7 @@ const Devices = () => {
|
||||
size="small"
|
||||
onClick={() => showDeviceValue(dv)}
|
||||
>
|
||||
{dv.v === '' && dv.c ? (
|
||||
{dv.v === '' ? (
|
||||
<PlayArrowIcon color="primary" sx={{ fontSize: 16 }} />
|
||||
) : (
|
||||
<EditIcon color="primary" sx={{ fontSize: 16 }} />
|
||||
@@ -757,7 +752,7 @@ const Devices = () => {
|
||||
{renderDeviceData()}
|
||||
{renderDeviceDetails()}
|
||||
{selectedDeviceValue && (
|
||||
<DashboardDevicesDialog
|
||||
<DevicesDialog
|
||||
open={deviceValueDialogOpen}
|
||||
onClose={deviceValueDialogClose}
|
||||
onSave={deviceValueDialogSave}
|
||||
|
||||
@@ -29,7 +29,7 @@ import { validate } from 'validators';
|
||||
import { DeviceValueUOM, DeviceValueUOM_s } from './types';
|
||||
import type { DeviceValue } from './types';
|
||||
|
||||
interface DashboardDevicesDialogProps {
|
||||
interface DevicesDialogProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
onSave: (as: DeviceValue) => void;
|
||||
@@ -47,7 +47,7 @@ const DevicesDialog = ({
|
||||
writeable,
|
||||
validator,
|
||||
progress
|
||||
}: DashboardDevicesDialogProps) => {
|
||||
}: DevicesDialogProps) => {
|
||||
const { LL } = useI18nContext();
|
||||
const [editItem, setEditItem] = useState<DeviceValue>(selectedItem);
|
||||
const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>();
|
||||
@@ -75,7 +75,10 @@ const DevicesDialog = ({
|
||||
}
|
||||
};
|
||||
|
||||
const setUom = (uom: DeviceValueUOM) => {
|
||||
const setUom = (uom?: DeviceValueUOM) => {
|
||||
if (uom === undefined) {
|
||||
return;
|
||||
}
|
||||
switch (uom) {
|
||||
case DeviceValueUOM.HOURS:
|
||||
return LL.HOURS();
|
||||
@@ -195,9 +198,9 @@ const DevicesDialog = ({
|
||||
</Button>
|
||||
<Button
|
||||
startIcon={<WarningIcon color="warning" />}
|
||||
variant="contained"
|
||||
variant="outlined"
|
||||
onClick={save}
|
||||
color="info"
|
||||
color="primary"
|
||||
>
|
||||
{selectedItem.v === '' && selectedItem.c ? LL.EXECUTE() : LL.UPDATE()}
|
||||
</Button>
|
||||
|
||||
@@ -24,7 +24,7 @@ import { useRequest } from 'alova/client';
|
||||
import { SectionContent, useLayoutTitle } from 'components';
|
||||
import { AuthenticatedContext } from 'contexts/authentication';
|
||||
import { useI18nContext } from 'i18n/i18n-react';
|
||||
import { saveFile } from 'utils/file';
|
||||
import { saveFile } from 'utils';
|
||||
|
||||
import { API, callAction } from '../../api/app';
|
||||
import type { APIcall } from './types';
|
||||
@@ -147,7 +147,7 @@ const Help = () => {
|
||||
)}
|
||||
|
||||
<Box p={2} color="warning.main">
|
||||
<Typography mb={1} variant="body2">
|
||||
<Typography mb={1} variant="body1">
|
||||
{LL.HELP_INFORMATION_4()}
|
||||
</Typography>
|
||||
<Button
|
||||
|
||||
@@ -175,7 +175,7 @@ const Modules = () => {
|
||||
return (
|
||||
<>
|
||||
<Box mb={2} color="warning.main">
|
||||
<Typography variant="body2">{LL.MODULES_DESCRIPTION()}</Typography>
|
||||
<Typography variant="body1">{LL.MODULES_DESCRIPTION()}</Typography>
|
||||
</Box>
|
||||
<Table
|
||||
data={{ nodes: modules }}
|
||||
|
||||
@@ -117,15 +117,10 @@ const Scheduler = () => {
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
.td {
|
||||
border-top: 1px solid #565656;
|
||||
border-bottom: 1px solid #565656;
|
||||
}
|
||||
&:hover .td {
|
||||
border-top: 1px solid #177ac9;
|
||||
border-bottom: 1px solid #177ac9;
|
||||
}
|
||||
&:nth-of-type(odd) .td {
|
||||
background-color: #303030;
|
||||
background-color: #177ac9;
|
||||
}
|
||||
`
|
||||
});
|
||||
@@ -318,7 +313,7 @@ const Scheduler = () => {
|
||||
<SectionContent>
|
||||
{blocker ? <BlockNavigation blocker={blocker} /> : null}
|
||||
<Box mb={2} color="warning.main">
|
||||
<Typography variant="body2">{LL.SCHEDULER_HELP_1()}</Typography>
|
||||
<Typography variant="body1">{LL.SCHEDULER_HELP_1()}</Typography>
|
||||
</Box>
|
||||
{renderSchedule()}
|
||||
|
||||
|
||||
@@ -19,10 +19,11 @@ import {
|
||||
} 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 { useAutoRequest, useRequest } from 'alova/client';
|
||||
import { useRequest } from 'alova/client';
|
||||
import { SectionContent, useLayoutTitle } from 'components';
|
||||
import { AuthenticatedContext } from 'contexts/authentication';
|
||||
import { useI18nContext } from 'i18n/i18n-react';
|
||||
import { useInterval } from 'utils';
|
||||
|
||||
import {
|
||||
readSensorData,
|
||||
@@ -59,7 +60,7 @@ const Sensors = () => {
|
||||
const [analogDialogOpen, setAnalogDialogOpen] = useState<boolean>(false);
|
||||
const [creating, setCreating] = useState<boolean>(false);
|
||||
|
||||
const { data: sensorData, send: fetchSensorData } = useAutoRequest(
|
||||
const { data: sensorData, send: fetchSensorData } = useRequest(
|
||||
() => readSensorData(),
|
||||
{
|
||||
initialData: {
|
||||
@@ -67,8 +68,7 @@ const Sensors = () => {
|
||||
as: [],
|
||||
analog_enabled: false,
|
||||
platform: 'ESP32'
|
||||
},
|
||||
pollingTime: 2000
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
@@ -86,6 +86,12 @@ const Sensors = () => {
|
||||
}
|
||||
);
|
||||
|
||||
useInterval(() => {
|
||||
if (!temperatureDialogOpen && !analogDialogOpen) {
|
||||
void fetchSensorData();
|
||||
}
|
||||
}, 3000);
|
||||
|
||||
const common_theme = useTheme({
|
||||
BaseRow: `
|
||||
font-size: 14px;
|
||||
@@ -110,19 +116,10 @@ const Sensors = () => {
|
||||
cursor: pointer;
|
||||
.td {
|
||||
padding: 8px;
|
||||
border-top: 1px solid #565656;
|
||||
border-bottom: 1px solid #565656;
|
||||
}
|
||||
&.tr.tr-body.row-select.row-select-single-selected {
|
||||
background-color: #3d4752;
|
||||
font-weight: normal;
|
||||
}
|
||||
&:hover .td {
|
||||
border-top: 1px solid #177ac9;
|
||||
border-bottom: 1px solid #177ac9;
|
||||
}
|
||||
&:nth-of-type(odd) .td {
|
||||
background-color: #303030;
|
||||
background-color: #177ac9;
|
||||
}
|
||||
`,
|
||||
Cell: `
|
||||
|
||||
@@ -322,7 +322,7 @@ const SensorsAnalogDialog = ({
|
||||
<Button
|
||||
startIcon={<RemoveIcon />}
|
||||
variant="outlined"
|
||||
color="error"
|
||||
color="warning"
|
||||
onClick={remove}
|
||||
>
|
||||
{LL.REMOVE()}
|
||||
@@ -339,9 +339,9 @@ const SensorsAnalogDialog = ({
|
||||
</Button>
|
||||
<Button
|
||||
startIcon={<WarningIcon color="warning" />}
|
||||
variant="contained"
|
||||
variant="outlined"
|
||||
onClick={save}
|
||||
color="info"
|
||||
color="primary"
|
||||
>
|
||||
{creating ? LL.ADD(0) : LL.UPDATE()}
|
||||
</Button>
|
||||
|
||||
@@ -122,9 +122,9 @@ const SensorsTemperatureDialog = ({
|
||||
</Button>
|
||||
<Button
|
||||
startIcon={<WarningIcon color="warning" />}
|
||||
variant="contained"
|
||||
variant="outlined"
|
||||
onClick={save}
|
||||
color="info"
|
||||
color="primary"
|
||||
>
|
||||
{LL.UPDATE()}
|
||||
</Button>
|
||||
|
||||
@@ -27,12 +27,16 @@ const formatDurationMin = (LL: TranslationFunctions, duration_min: number) => {
|
||||
|
||||
export function formatValue(
|
||||
LL: TranslationFunctions,
|
||||
value: unknown,
|
||||
uom: DeviceValueUOM
|
||||
value?: unknown,
|
||||
uom?: DeviceValueUOM
|
||||
) {
|
||||
if (typeof value !== 'number') {
|
||||
return (value === undefined ? '' : value) as string;
|
||||
if (typeof value !== 'number' || uom === undefined || value === undefined) {
|
||||
if (value === undefined || typeof value === 'boolean') {
|
||||
return '';
|
||||
}
|
||||
return value as string;
|
||||
}
|
||||
|
||||
switch (uom) {
|
||||
case DeviceValueUOM.HOURS:
|
||||
return value ? formatDurationMin(LL, value * 60) : LL.NUM_HOURS({ num: 0 });
|
||||
|
||||
@@ -114,10 +114,18 @@ export interface CoreData {
|
||||
devices: Device[];
|
||||
}
|
||||
|
||||
export interface DashboardItem {
|
||||
id: number; // unique index
|
||||
t?: number; // type from DeviceType
|
||||
n?: string; // name, optional
|
||||
dv?: DeviceValue; // device value, optional
|
||||
nodes?: DashboardItem[]; // children nodes, optional
|
||||
}
|
||||
|
||||
export interface DeviceValue {
|
||||
id: string; // index, contains mask+name
|
||||
v: unknown; // value, Number or String
|
||||
u: number; // uom
|
||||
v?: unknown; // value, Number, String or Boolean - can be undefined
|
||||
u?: number; // uom, optional
|
||||
c?: string; // command, optional
|
||||
l?: string[]; // list, optional
|
||||
h?: string; // help text, optional
|
||||
@@ -125,8 +133,9 @@ export interface DeviceValue {
|
||||
m?: number; // min, optional
|
||||
x?: number; // max, optional
|
||||
}
|
||||
|
||||
export interface DeviceData {
|
||||
data: DeviceValue[];
|
||||
nodes: DeviceValue[];
|
||||
}
|
||||
|
||||
export interface DeviceEntity {
|
||||
@@ -299,7 +308,7 @@ export interface ScheduleItem {
|
||||
time: string; // also used for Condition and On Change
|
||||
cmd: string;
|
||||
value: string;
|
||||
name: string; // is optional
|
||||
name: string; // can be empty
|
||||
o_id?: number;
|
||||
o_active?: boolean;
|
||||
o_deleted?: boolean;
|
||||
@@ -382,10 +391,10 @@ export interface Entities {
|
||||
// matches emsdevice.h DeviceType
|
||||
export const enum DeviceType {
|
||||
SYSTEM = 0,
|
||||
TEMPERATURESENSOR,
|
||||
ANALOGSENSOR,
|
||||
SCHEDULER,
|
||||
CUSTOM,
|
||||
TEMPERATURESENSOR = 1,
|
||||
ANALOGSENSOR = 2,
|
||||
SCHEDULER = 3,
|
||||
CUSTOM = 4,
|
||||
BOILER,
|
||||
THERMOSTAT,
|
||||
MIXER,
|
||||
|
||||
@@ -32,7 +32,7 @@ import {
|
||||
useLayoutTitle
|
||||
} from 'components';
|
||||
import { useI18nContext } from 'i18n/i18n-react';
|
||||
import { saveFile } from 'utils/file';
|
||||
import { saveFile } from 'utils';
|
||||
|
||||
const DownloadUpload = () => {
|
||||
const { LL } = useI18nContext();
|
||||
@@ -221,7 +221,7 @@ const DownloadUpload = () => {
|
||||
{LL.DOWNLOAD(0)}
|
||||
</Typography>
|
||||
|
||||
<Typography mb={1} variant="body2" color="warning">
|
||||
<Typography mb={1} variant="body1" color="warning">
|
||||
{LL.DOWNLOAD_SETTINGS_TEXT()}
|
||||
</Typography>
|
||||
<Grid container spacing={1}>
|
||||
@@ -269,7 +269,7 @@ const DownloadUpload = () => {
|
||||
</Typography>
|
||||
|
||||
<Box color="warning.main" sx={{ pb: 2 }}>
|
||||
<Typography variant="body2">{LL.UPLOAD_TEXT()}</Typography>
|
||||
<Typography variant="body1">{LL.UPLOAD_TEXT()}</Typography>
|
||||
</Box>
|
||||
|
||||
<SingleUpload doRestart={doRestart} />
|
||||
|
||||
@@ -38,7 +38,7 @@ const APStatus = () => {
|
||||
data,
|
||||
send: loadData,
|
||||
error
|
||||
} = useAutoRequest(APApi.readAPStatus, { pollingTime: 5000 });
|
||||
} = useAutoRequest(APApi.readAPStatus, { pollingTime: 3000 });
|
||||
|
||||
const { LL } = useI18nContext();
|
||||
useLayoutTitle(LL.STATUS_OF(LL.ACCESS_POINT(0)));
|
||||
|
||||
@@ -21,7 +21,7 @@ const SystemActivity = () => {
|
||||
data,
|
||||
send: loadData,
|
||||
error
|
||||
} = useAutoRequest(readActivity, { pollingTime: 2000 });
|
||||
} = useAutoRequest(readActivity, { pollingTime: 3000 });
|
||||
|
||||
const { LL } = useI18nContext();
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@ const HardwareStatus = () => {
|
||||
data,
|
||||
send: loadData,
|
||||
error
|
||||
} = useAutoRequest(SystemApi.readSystemStatus, { pollingTime: 2000 });
|
||||
} = useAutoRequest(SystemApi.readSystemStatus, { pollingTime: 3000 });
|
||||
|
||||
const content = () => {
|
||||
if (!data) {
|
||||
|
||||
@@ -58,7 +58,7 @@ const MqttStatus = () => {
|
||||
data,
|
||||
send: loadData,
|
||||
error
|
||||
} = useAutoRequest(MqttApi.readMqttStatus, { pollingTime: 5000 });
|
||||
} = useAutoRequest(MqttApi.readMqttStatus, { pollingTime: 3000 });
|
||||
|
||||
const { LL } = useI18nContext();
|
||||
useLayoutTitle(LL.STATUS_OF('MQTT'));
|
||||
|
||||
@@ -40,7 +40,7 @@ const NTPStatus = () => {
|
||||
data,
|
||||
send: loadData,
|
||||
error
|
||||
} = useAutoRequest(NTPApi.readNTPStatus, { pollingTime: 5000 });
|
||||
} = useAutoRequest(NTPApi.readNTPStatus, { pollingTime: 3000 });
|
||||
|
||||
const [localTime, setLocalTime] = useState<string>('');
|
||||
const [settingTime, setSettingTime] = useState<boolean>(false);
|
||||
|
||||
@@ -85,7 +85,7 @@ const NetworkStatus = () => {
|
||||
data,
|
||||
send: loadData,
|
||||
error
|
||||
} = useAutoRequest(NetworkApi.readNetworkStatus, { pollingTime: 5000 });
|
||||
} = useAutoRequest(NetworkApi.readNetworkStatus, { pollingTime: 3000 });
|
||||
|
||||
const { LL } = useI18nContext();
|
||||
useLayoutTitle(LL.STATUS_OF(LL.NETWORK(1)));
|
||||
|
||||
@@ -28,7 +28,7 @@ const RestartMonitor = () => {
|
||||
initialData: { status: 'Getting ready...' },
|
||||
async middleware(_, next) {
|
||||
if (count++ >= 1) {
|
||||
// skip first request (1 seconds) to allow AsyncWS to send its response
|
||||
// skip first request (1 second) to allow AsyncWS to send its response
|
||||
await next();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,7 +65,7 @@ const SystemStatus = () => {
|
||||
error
|
||||
} = useAutoRequest(readSystemStatus, {
|
||||
initialData: [],
|
||||
pollingTime: 5000,
|
||||
pollingTime: 3000,
|
||||
async middleware(_, next) {
|
||||
if (!restarting) {
|
||||
await next();
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import type { FC } from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
|
||||
@@ -24,9 +24,11 @@ const Layout: FC<RequiredChildrenProps> = ({ children }) => {
|
||||
|
||||
useEffect(() => setMobileOpen(false), [pathname]);
|
||||
|
||||
// cache the object to prevent unnecessary re-renders
|
||||
const obj = useMemo(() => ({ title, setTitle }), [title]);
|
||||
|
||||
return (
|
||||
// TODO wrap title/setTitle in a useMemo()
|
||||
<LayoutContext.Provider value={{ title, setTitle }}>
|
||||
<LayoutContext.Provider value={obj}>
|
||||
<LayoutAppBar title={title} onToggleDrawer={handleDrawerToggle} />
|
||||
<LayoutDrawer mobileOpen={mobileOpen} onClose={handleDrawerToggle} />
|
||||
<Box component="main" sx={{ marginLeft: { md: `${DRAWER_WIDTH}px` } }}>
|
||||
|
||||
@@ -11,6 +11,7 @@ import PersonIcon from '@mui/icons-material/Person';
|
||||
import PlaylistAddIcon from '@mui/icons-material/PlaylistAdd';
|
||||
import SensorsIcon from '@mui/icons-material/Sensors';
|
||||
import SettingsIcon from '@mui/icons-material/Settings';
|
||||
import StarIcon from '@mui/icons-material/Star';
|
||||
import {
|
||||
Avatar,
|
||||
Box,
|
||||
@@ -51,8 +52,8 @@ const LayoutMenu = () => {
|
||||
return (
|
||||
<>
|
||||
<List component="nav">
|
||||
<LayoutMenuItem icon={StarIcon} label="Dashboard" to={`/dashboard`} />
|
||||
<LayoutMenuItem icon={CategoryIcon} label={LL.DEVICES()} to={`/devices`} />
|
||||
<LayoutMenuItem icon={SensorsIcon} label={LL.SENSORS()} to={`/sensors`} />
|
||||
<Divider />
|
||||
|
||||
<Box
|
||||
@@ -77,19 +78,19 @@ const LayoutMenu = () => {
|
||||
mb: '2px',
|
||||
color: 'lightblue'
|
||||
}}
|
||||
secondary={
|
||||
LL.CUSTOMIZATIONS() +
|
||||
', ' +
|
||||
LL.SCHEDULER() +
|
||||
', ' +
|
||||
LL.CUSTOM_ENTITIES(0) +
|
||||
'...'
|
||||
}
|
||||
secondaryTypographyProps={{
|
||||
noWrap: true,
|
||||
fontSize: 12,
|
||||
color: menuOpen ? 'rgba(0,0,0,0)' : 'rgba(255,255,255,0.5)'
|
||||
}}
|
||||
// secondary={
|
||||
// LL.CUSTOMIZATIONS() +
|
||||
// ', ' +
|
||||
// LL.SCHEDULER() +
|
||||
// ', ' +
|
||||
// LL.CUSTOM_ENTITIES(0) +
|
||||
// '...'
|
||||
// }
|
||||
// secondaryTypographyProps={{
|
||||
// noWrap: true,
|
||||
// fontSize: 12,
|
||||
// color: menuOpen ? 'rgba(0,0,0,0)' : 'rgba(255,255,255,0.5)'
|
||||
// }}
|
||||
sx={{ my: 0 }}
|
||||
/>
|
||||
<KeyboardArrowDown
|
||||
@@ -103,6 +104,12 @@ const LayoutMenu = () => {
|
||||
</ListItemButton>
|
||||
{menuOpen && (
|
||||
<>
|
||||
<LayoutMenuItem
|
||||
icon={SensorsIcon}
|
||||
label={LL.SENSORS()}
|
||||
to={`/sensors`}
|
||||
/>
|
||||
|
||||
<LayoutMenuItem
|
||||
icon={ConstructionIcon}
|
||||
label={LL.CUSTOMIZATIONS()}
|
||||
@@ -142,8 +149,8 @@ const LayoutMenu = () => {
|
||||
</List>
|
||||
<Divider />
|
||||
<List>
|
||||
<ListItem disablePadding onClick={handleClick}>
|
||||
<ListItemButton>
|
||||
<ListItem disablePadding>
|
||||
<ListItemButton component="button" onClick={handleClick}>
|
||||
<ListItemIcon sx={{ color: '#9e9e9e' }}>
|
||||
<AccountCircleIcon />
|
||||
</ListItemIcon>
|
||||
|
||||
@@ -35,7 +35,7 @@ export function fetchLoginRedirect(): Partial<Path> {
|
||||
const signInSearch = getStorage().getItem(SIGN_IN_SEARCH);
|
||||
clearLoginRedirect();
|
||||
return {
|
||||
pathname: signInPathname || `/devices`,
|
||||
pathname: signInPathname || `/dashboard`,
|
||||
search: (signInPathname && signInSearch) || undefined
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import type { FC } from 'react';
|
||||
import { redirect } from 'react-router-dom';
|
||||
import { toast } from 'react-toastify';
|
||||
@@ -67,17 +67,15 @@ const Authentication: FC<RequiredChildrenProps> = ({ children }) => {
|
||||
void refresh();
|
||||
}, [refresh]);
|
||||
|
||||
// cache object to prevent re-renders
|
||||
const obj = useMemo(
|
||||
() => ({ signIn, signOut, me, refresh }),
|
||||
[signIn, signOut, me, refresh]
|
||||
);
|
||||
|
||||
if (initialized) {
|
||||
return (
|
||||
// TODO useMemo?
|
||||
<AuthenticationContext.Provider
|
||||
value={{
|
||||
signIn,
|
||||
signOut,
|
||||
me,
|
||||
refresh
|
||||
}}
|
||||
>
|
||||
<AuthenticationContext.Provider value={obj}>
|
||||
{children}
|
||||
</AuthenticationContext.Provider>
|
||||
);
|
||||
|
||||
@@ -340,7 +340,10 @@ const de: Translation = {
|
||||
PLEASE_WAIT: 'Bitte warten',
|
||||
RESTARTING_PRE: 'Initialisierung',
|
||||
RESTARTING_POST: 'Vorbereitung',
|
||||
AUTO_SCROLL: 'Automatisches Scrollen'
|
||||
AUTO_SCROLL: 'Automatisches Scrollen',
|
||||
DASHBOARD: 'Dashboard',
|
||||
NO_DATA: 'Keine Daten verfügbar',
|
||||
DASHBOARD_1: 'Passen Sie Ihr Dashboard an, indem Sie EMS-Entitäten mithilfe des Moduls „Anpassungen“ als Favorit markieren.'
|
||||
};
|
||||
|
||||
export default de;
|
||||
|
||||
@@ -340,7 +340,10 @@ const en: Translation = {
|
||||
PLEASE_WAIT: 'Please wait',
|
||||
RESTARTING_PRE: 'Initializing',
|
||||
RESTARTING_POST: 'Preparing',
|
||||
AUTO_SCROLL: 'Auto Scroll'
|
||||
AUTO_SCROLL: 'Auto Scroll',
|
||||
DASHBOARD: 'Dashboard',
|
||||
NO_DATA: 'No data available',
|
||||
DASHBOARD_1: 'Customize your dashboard by marking EMS entities as Favorite using the Customizations module.',
|
||||
};
|
||||
|
||||
export default en;
|
||||
|
||||
@@ -340,7 +340,10 @@ const fr: Translation = {
|
||||
PLEASE_WAIT: 'Please wait', // TODO translate
|
||||
RESTARTING_PRE: 'Initializing', // TODO translate
|
||||
RESTARTING_POST: 'Preparing', // TODO translate
|
||||
AUTO_SCROLL: 'Auto Scroll' // TODO translate
|
||||
AUTO_SCROLL: 'Auto Scroll', // TODO translate
|
||||
DASHBOARD: 'Dashboard', // TODO translate
|
||||
NO_DATA: 'No data available', // TODO translate
|
||||
DASHBOARD_1: 'Customize your dashboard by marking EMS entities as Favorite using the Customizations module.' // TODO translate
|
||||
};
|
||||
|
||||
export default fr;
|
||||
|
||||
@@ -340,7 +340,10 @@ const it: Translation = {
|
||||
PLEASE_WAIT: 'Please wait', // TODO translate
|
||||
RESTARTING_PRE: 'Initializing', // TODO translate
|
||||
RESTARTING_POST: 'Preparing', // TODO translate
|
||||
AUTO_SCROLL: 'Auto Scroll' // TODO translate
|
||||
AUTO_SCROLL: 'Auto Scroll', // TODO translate
|
||||
DASHBOARD: 'Dashboard', // TODO translate
|
||||
NO_DATA: 'No data available', // TODO translate
|
||||
DASHBOARD_1: 'Customize your dashboard by marking EMS entities as Favorite using the Customizations module.' // TODO translate
|
||||
};
|
||||
|
||||
export default it;
|
||||
|
||||
@@ -340,7 +340,10 @@ const nl: Translation = {
|
||||
PLEASE_WAIT: 'Please wait', // TODO translate
|
||||
RESTARTING_PRE: 'Initializing', // TODO translate
|
||||
RESTARTING_POST: 'Preparing', // TODO translate
|
||||
AUTO_SCROLL: 'Auto Scroll' // TODO translate
|
||||
AUTO_SCROLL: 'Auto Scroll', // TODO translate
|
||||
DASHBOARD: 'Dashboard', // TODO translate
|
||||
NO_DATA: 'No data available', // TODO translate
|
||||
DASHBOARD_1: 'Customize your dashboard by marking EMS entities as Favorite using the Customizations module.' // TODO translate
|
||||
};
|
||||
|
||||
export default nl;
|
||||
|
||||
@@ -340,7 +340,10 @@ const no: Translation = {
|
||||
PLEASE_WAIT: 'Please wait', // TODO translate
|
||||
RESTARTING_PRE: 'Initializing', // TODO translate
|
||||
RESTARTING_POST: 'Preparing', // TODO translate
|
||||
AUTO_SCROLL: 'Auto Scroll' // TODO translate
|
||||
AUTO_SCROLL: 'Auto Scroll', // TODO translate
|
||||
DASHBOARD: 'Dashboard', // TODO translate
|
||||
NO_DATA: 'No data available', // TODO translate
|
||||
DASHBOARD_1: 'Customize your dashboard by marking EMS entities as Favorite using the Customizations module.' // TODO translate
|
||||
};
|
||||
|
||||
export default no;
|
||||
|
||||
@@ -340,7 +340,10 @@ const pl: BaseTranslation = {
|
||||
PLEASE_WAIT: 'Please wait', // TODO translate
|
||||
RESTARTING_PRE: 'Initializing', // TODO translate
|
||||
RESTARTING_POST: 'Preparing', // TODO translate
|
||||
AUTO_SCROLL: 'Auto Scroll' // TODO translate
|
||||
AUTO_SCROLL: 'Auto Scroll', // TODO translate
|
||||
DASHBOARD: 'Dashboard', // TODO translate
|
||||
NO_DATA: 'No data available', // TODO translate
|
||||
DASHBOARD_1: 'Customize your dashboard by marking EMS entities as Favorite using the Customizations module.' // TODO translate
|
||||
};
|
||||
|
||||
export default pl;
|
||||
|
||||
@@ -340,7 +340,10 @@ const sk: Translation = {
|
||||
PLEASE_WAIT: 'Čakajte prosím',
|
||||
RESTARTING_PRE: 'Prebieha inicializácia',
|
||||
RESTARTING_POST: 'Príprava',
|
||||
AUTO_SCROLL: 'Auto Scroll' // TODO translate
|
||||
AUTO_SCROLL: 'Auto Scroll', // TODO translate
|
||||
DASHBOARD: 'Dashboard', // TODO translate
|
||||
NO_DATA: 'No data available', // TODO translate
|
||||
DASHBOARD_1: 'Customize your dashboard by marking EMS entities as Favorite using the Customizations module.' // TODO translate
|
||||
};
|
||||
|
||||
export default sk;
|
||||
|
||||
@@ -340,7 +340,10 @@ const sv: Translation = {
|
||||
PLEASE_WAIT: 'Please wait', // TODO translate
|
||||
RESTARTING_PRE: 'Initializing', // TODO translate
|
||||
RESTARTING_POST: 'Preparing', // TODO translate
|
||||
AUTO_SCROLL: 'Auto Scroll' // TODO translate
|
||||
AUTO_SCROLL: 'Auto Scroll', // TODO translate
|
||||
DASHBOARD: 'Dashboard', // TODO translate
|
||||
NO_DATA: 'No data available', // TODO translate
|
||||
DASHBOARD_1: 'Customize your dashboard by marking EMS entities as Favorite using the Customizations module.' // TODO translate
|
||||
};
|
||||
|
||||
export default sv;
|
||||
|
||||
@@ -340,7 +340,10 @@ const tr: Translation = {
|
||||
PLEASE_WAIT: 'Please wait', // TODO translate
|
||||
RESTARTING_PRE: 'Initializing', // TODO translate
|
||||
RESTARTING_POST: 'Preparing', // TODO translate
|
||||
AUTO_SCROLL: 'Auto Scroll' // TODO translate
|
||||
AUTO_SCROLL: 'Auto Scroll', // TODO translate
|
||||
DASHBOARD: 'Dashboard', // TODO translate
|
||||
NO_DATA: 'No data available', // TODO translate
|
||||
DASHBOARD_1: 'Customize your dashboard by marking EMS entities as Favorite using the Customizations module.' // TODO translate
|
||||
};
|
||||
|
||||
export default tr;
|
||||
|
||||
@@ -3,4 +3,7 @@ export * from './route';
|
||||
export * from './submit';
|
||||
export * from './time';
|
||||
export * from './useRest';
|
||||
export * from './useInterval';
|
||||
export * from './props';
|
||||
export * from './file';
|
||||
export * from './usePersistState';
|
||||
|
||||
25
interface/src/utils/useInterval.ts
Normal file
25
interface/src/utils/useInterval.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { useEffect, useRef } from 'react';
|
||||
|
||||
// adapted from https://www.joshwcomeau.com/snippets/react-hooks/use-interval/
|
||||
export const useInterval = (callback: () => void, delay: number) => {
|
||||
const intervalRef = useRef<number | null>(null);
|
||||
const savedCallback = useRef<() => void>(callback);
|
||||
|
||||
useEffect(() => {
|
||||
savedCallback.current = callback;
|
||||
}, [callback]);
|
||||
|
||||
useEffect(() => {
|
||||
const tick = () => savedCallback.current();
|
||||
if (typeof delay === 'number') {
|
||||
intervalRef.current = window.setInterval(tick, delay);
|
||||
return () => {
|
||||
if (intervalRef.current !== null) {
|
||||
window.clearInterval(intervalRef.current);
|
||||
}
|
||||
};
|
||||
}
|
||||
}, [delay]);
|
||||
|
||||
return intervalRef;
|
||||
};
|
||||
26
interface/src/utils/usePersistState.ts
Normal file
26
interface/src/utils/usePersistState.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
|
||||
export const usePersistState = <T>(
|
||||
initial_value: T,
|
||||
id: string
|
||||
): [T, (new_state: T) => void] => {
|
||||
// Set initial value
|
||||
const _initial_value = useMemo(() => {
|
||||
const local_storage_value_str = localStorage.getItem('state:' + id);
|
||||
// If there is a value stored in localStorage, use that
|
||||
if (local_storage_value_str) {
|
||||
return JSON.parse(local_storage_value_str);
|
||||
}
|
||||
// Otherwise use initial_value that was passed to the function
|
||||
return initial_value;
|
||||
}, []);
|
||||
|
||||
const [state, setState] = useState(_initial_value);
|
||||
|
||||
useEffect(() => {
|
||||
const state_str = JSON.stringify(state); // Stringified state
|
||||
localStorage.setItem('state:' + id, state_str); // Set stringified state as item in localStorage
|
||||
}, [state]);
|
||||
|
||||
return [state, setState];
|
||||
};
|
||||
Reference in New Issue
Block a user