mirror of
https://github.com/emsesp/EMS-ESP32.git
synced 2025-12-07 00:09:51 +03:00
dashboard v.01
This commit is contained in:
@@ -3,6 +3,7 @@ import { Navigate, Route, Routes } from 'react-router-dom';
|
|||||||
|
|
||||||
import CustomEntities from 'app/main/CustomEntities';
|
import CustomEntities from 'app/main/CustomEntities';
|
||||||
import Customizations from 'app/main/Customizations';
|
import Customizations from 'app/main/Customizations';
|
||||||
|
import Dashboard from 'app/main/Dashboard';
|
||||||
import Devices from 'app/main/Devices';
|
import Devices from 'app/main/Devices';
|
||||||
import Help from 'app/main/Help';
|
import Help from 'app/main/Help';
|
||||||
import Modules from 'app/main/Modules';
|
import Modules from 'app/main/Modules';
|
||||||
@@ -32,6 +33,7 @@ const AuthenticatedRouting = () => {
|
|||||||
return (
|
return (
|
||||||
<Layout>
|
<Layout>
|
||||||
<Routes>
|
<Routes>
|
||||||
|
<Route path="/dashboard/*" element={<Dashboard />} />
|
||||||
<Route path="/devices/*" element={<Devices />} />
|
<Route path="/devices/*" element={<Devices />} />
|
||||||
<Route path="/sensors/*" element={<Sensors />} />
|
<Route path="/sensors/*" element={<Sensors />} />
|
||||||
<Route path="/status/*" element={<Status />} />
|
<Route path="/status/*" element={<Status />} />
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import type {
|
|||||||
Action,
|
Action,
|
||||||
Activity,
|
Activity,
|
||||||
CoreData,
|
CoreData,
|
||||||
|
DashboardData,
|
||||||
DeviceData,
|
DeviceData,
|
||||||
DeviceEntity,
|
DeviceEntity,
|
||||||
Entities,
|
Entities,
|
||||||
@@ -19,7 +20,13 @@ import type {
|
|||||||
WriteTemperatureSensor
|
WriteTemperatureSensor
|
||||||
} from '../app/main/types';
|
} from '../app/main/types';
|
||||||
|
|
||||||
// DashboardDevices
|
// Dashboard
|
||||||
|
export const readDashboard = () =>
|
||||||
|
alovaInstance.Get<DashboardData>('/rest/dashboardData', {
|
||||||
|
responseType: 'arraybuffer' // uses msgpack
|
||||||
|
});
|
||||||
|
|
||||||
|
// Devices
|
||||||
export const readCoreData = () => alovaInstance.Get<CoreData>(`/rest/coreData`);
|
export const readCoreData = () => alovaInstance.Get<CoreData>(`/rest/coreData`);
|
||||||
export const readDeviceData = (id: number) =>
|
export const readDeviceData = (id: number) =>
|
||||||
alovaInstance.Get<DeviceData>('/rest/deviceData', {
|
alovaInstance.Get<DeviceData>('/rest/deviceData', {
|
||||||
|
|||||||
193
interface/src/app/main/Dashboard.tsx
Normal file
193
interface/src/app/main/Dashboard.tsx
Normal file
@@ -0,0 +1,193 @@
|
|||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
import ChevronRightIcon from '@mui/icons-material/ChevronRight';
|
||||||
|
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
|
||||||
|
import UnfoldLessIcon from '@mui/icons-material/UnfoldLess';
|
||||||
|
import UnfoldMoreIcon from '@mui/icons-material/UnfoldMore';
|
||||||
|
import { Box, ToggleButton, ToggleButtonGroup, Typography } from '@mui/material';
|
||||||
|
|
||||||
|
import {
|
||||||
|
Body,
|
||||||
|
Cell,
|
||||||
|
Header,
|
||||||
|
HeaderCell,
|
||||||
|
HeaderRow,
|
||||||
|
Row,
|
||||||
|
Table
|
||||||
|
} from '@table-library/react-table-library/table';
|
||||||
|
import { useTheme } from '@table-library/react-table-library/theme';
|
||||||
|
import { CellTree, useTree } from '@table-library/react-table-library/tree';
|
||||||
|
import { useAutoRequest } from 'alova/client';
|
||||||
|
import { FormLoader, SectionContent, useLayoutTitle } from 'components';
|
||||||
|
import { useI18nContext } from 'i18n/i18n-react';
|
||||||
|
|
||||||
|
import { readDashboard } from '../../api/app';
|
||||||
|
import { formatValue } from './deviceValue';
|
||||||
|
import type { DashboardItem } from './types';
|
||||||
|
|
||||||
|
const Dashboard = () => {
|
||||||
|
const { LL } = useI18nContext();
|
||||||
|
|
||||||
|
useLayoutTitle('Dashboard'); // TODO translate
|
||||||
|
|
||||||
|
const [firstLoad, setFirstLoad] = useState<boolean>(true);
|
||||||
|
const [showAll, setShowAll] = useState<boolean>(true);
|
||||||
|
|
||||||
|
const {
|
||||||
|
data,
|
||||||
|
send: fetchDashboard,
|
||||||
|
error
|
||||||
|
} = useAutoRequest(readDashboard, {
|
||||||
|
initialData: [],
|
||||||
|
pollingTime: 1500
|
||||||
|
});
|
||||||
|
|
||||||
|
const dashboard_theme = useTheme({
|
||||||
|
Table: `
|
||||||
|
--data-table-library_grid-template-columns: minmax(80px, auto) 120px;
|
||||||
|
`,
|
||||||
|
BaseRow: `
|
||||||
|
font-size: 14px;
|
||||||
|
.td {
|
||||||
|
height: 32px;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
HeaderRow: `
|
||||||
|
text-transform: uppercase;
|
||||||
|
background-color: black;
|
||||||
|
color: #90CAF9;
|
||||||
|
.th {
|
||||||
|
border-bottom: 1px solid #565656;
|
||||||
|
height: 36px;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
Row: `
|
||||||
|
background-color: #1e1e1e;
|
||||||
|
position: relative;
|
||||||
|
cursor: pointer;
|
||||||
|
.td {
|
||||||
|
height: 24px;
|
||||||
|
}
|
||||||
|
&:hover .td {
|
||||||
|
border-top: 1px solid #177ac9;
|
||||||
|
border-bottom: 1px solid #177ac9;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
});
|
||||||
|
|
||||||
|
function onTreeChange(action, state) {
|
||||||
|
// do nothing for now
|
||||||
|
}
|
||||||
|
|
||||||
|
const tree = useTree(
|
||||||
|
{ nodes: data },
|
||||||
|
{
|
||||||
|
onChange: onTreeChange
|
||||||
|
},
|
||||||
|
{
|
||||||
|
treeIcon: {
|
||||||
|
margin: '4px',
|
||||||
|
iconDefault: null,
|
||||||
|
iconRight: <ChevronRightIcon color="primary" />,
|
||||||
|
iconDown: <ExpandMoreIcon color="primary" />
|
||||||
|
},
|
||||||
|
indentation: 28
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// auto expand on first load
|
||||||
|
useEffect(() => {
|
||||||
|
if (data.length && firstLoad && !tree.state.ids.length) {
|
||||||
|
tree.fns.onToggleAll({});
|
||||||
|
setFirstLoad(false);
|
||||||
|
}
|
||||||
|
}, [data]);
|
||||||
|
|
||||||
|
const showName = (di: DashboardItem) => {
|
||||||
|
if (di.id < 100) {
|
||||||
|
if (di.nodes?.length) {
|
||||||
|
return (
|
||||||
|
<div style={{ color: '#2196f3' }}>
|
||||||
|
{di.n} ({di.nodes?.length})
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return <div style={{ color: '#2196f3' }}>{di.n}</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <div>{di.n}</div>;
|
||||||
|
};
|
||||||
|
|
||||||
|
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 mb={2} color="warning.main">
|
||||||
|
<Typography variant="body2">
|
||||||
|
Use Customizations to mark your favorite EMS entities
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
<ToggleButtonGroup
|
||||||
|
color="primary"
|
||||||
|
size="small"
|
||||||
|
value={showAll}
|
||||||
|
exclusive
|
||||||
|
onChange={handleShowAll}
|
||||||
|
>
|
||||||
|
<ToggleButton value={true}>
|
||||||
|
<UnfoldMoreIcon fontSize="small" />
|
||||||
|
</ToggleButton>
|
||||||
|
<ToggleButton value={false}>
|
||||||
|
<UnfoldLessIcon fontSize="small" />
|
||||||
|
</ToggleButton>
|
||||||
|
</ToggleButtonGroup>
|
||||||
|
<Table
|
||||||
|
data={{ nodes: data }}
|
||||||
|
theme={dashboard_theme}
|
||||||
|
layout={{ custom: true }}
|
||||||
|
tree={tree}
|
||||||
|
>
|
||||||
|
{(tableList: DashboardItem[]) => (
|
||||||
|
<>
|
||||||
|
<Header>
|
||||||
|
<HeaderRow>
|
||||||
|
<HeaderCell resize>Name</HeaderCell>
|
||||||
|
<HeaderCell>Value</HeaderCell>
|
||||||
|
</HeaderRow>
|
||||||
|
</Header>
|
||||||
|
<Body>
|
||||||
|
{tableList.map((di: DashboardItem) => (
|
||||||
|
<Row key={di.id} item={di} disabled={di.nodes?.length === 0}>
|
||||||
|
{di.nodes?.length === 0 ? (
|
||||||
|
<Cell>{showName(di)}</Cell>
|
||||||
|
) : (
|
||||||
|
<CellTree item={di}>{showName(di)}</CellTree>
|
||||||
|
)}
|
||||||
|
<Cell pinRight>{formatValue(LL, di.v, di.u)}</Cell>
|
||||||
|
</Row>
|
||||||
|
))}
|
||||||
|
</Body>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Table>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return <SectionContent>{renderContent()}</SectionContent>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Dashboard;
|
||||||
@@ -114,6 +114,18 @@ export interface CoreData {
|
|||||||
devices: Device[];
|
devices: Device[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface DashboardItem {
|
||||||
|
id: number; // unique index
|
||||||
|
n: string; // name
|
||||||
|
v?: unknown; // value
|
||||||
|
u: number; // uom
|
||||||
|
nodes?: DashboardItem[]; // nodes
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DashboardData {
|
||||||
|
data: DashboardItem[];
|
||||||
|
}
|
||||||
|
|
||||||
export interface DeviceValue {
|
export interface DeviceValue {
|
||||||
id: string; // index, contains mask+name
|
id: string; // index, contains mask+name
|
||||||
v: unknown; // value, Number or String
|
v: unknown; // value, Number or String
|
||||||
@@ -125,6 +137,7 @@ export interface DeviceValue {
|
|||||||
m?: number; // min, optional
|
m?: number; // min, optional
|
||||||
x?: number; // max, optional
|
x?: number; // max, optional
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DeviceData {
|
export interface DeviceData {
|
||||||
data: DeviceValue[];
|
data: DeviceValue[];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import PersonIcon from '@mui/icons-material/Person';
|
|||||||
import PlaylistAddIcon from '@mui/icons-material/PlaylistAdd';
|
import PlaylistAddIcon from '@mui/icons-material/PlaylistAdd';
|
||||||
import SensorsIcon from '@mui/icons-material/Sensors';
|
import SensorsIcon from '@mui/icons-material/Sensors';
|
||||||
import SettingsIcon from '@mui/icons-material/Settings';
|
import SettingsIcon from '@mui/icons-material/Settings';
|
||||||
|
import StarIcon from '@mui/icons-material/Star';
|
||||||
import {
|
import {
|
||||||
Avatar,
|
Avatar,
|
||||||
Box,
|
Box,
|
||||||
@@ -51,6 +52,7 @@ const LayoutMenu = () => {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<List component="nav">
|
<List component="nav">
|
||||||
|
<LayoutMenuItem icon={StarIcon} label="Dashboard" to={`/dashboard`} />
|
||||||
<LayoutMenuItem icon={CategoryIcon} label={LL.DEVICES()} to={`/devices`} />
|
<LayoutMenuItem icon={CategoryIcon} label={LL.DEVICES()} to={`/devices`} />
|
||||||
<LayoutMenuItem icon={SensorsIcon} label={LL.SENSORS()} to={`/sensors`} />
|
<LayoutMenuItem icon={SensorsIcon} label={LL.SENSORS()} to={`/sensors`} />
|
||||||
<Divider />
|
<Divider />
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ export function fetchLoginRedirect(): Partial<Path> {
|
|||||||
const signInSearch = getStorage().getItem(SIGN_IN_SEARCH);
|
const signInSearch = getStorage().getItem(SIGN_IN_SEARCH);
|
||||||
clearLoginRedirect();
|
clearLoginRedirect();
|
||||||
return {
|
return {
|
||||||
pathname: signInPathname || `/devices`,
|
pathname: signInPathname || `/dashboard`,
|
||||||
search: (signInPathname && signInSearch) || undefined
|
search: (signInPathname && signInSearch) || undefined
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4277,7 +4277,8 @@ function getDashboardEntityData(id: number) {
|
|||||||
id2: id
|
id2: id
|
||||||
}))
|
}))
|
||||||
.filter((item) => id === 99 || parseInt(item.id2.slice(0, 2), 16) & 0x08)
|
.filter((item) => id === 99 || parseInt(item.id2.slice(0, 2), 16) & 0x08)
|
||||||
.map((item) => ({
|
.map((item, index) => ({
|
||||||
|
id: id * 100 + index, // unique id
|
||||||
n: item.id2.slice(2), // name
|
n: item.id2.slice(2), // name
|
||||||
v: item.v, // value
|
v: item.v, // value
|
||||||
u: item.u // uom
|
u: item.u // uom
|
||||||
@@ -4324,7 +4325,6 @@ router
|
|||||||
params.id ? deviceEntities(Number(params.id)) : status(404)
|
params.id ? deviceEntities(Number(params.id)) : status(404)
|
||||||
)
|
)
|
||||||
.get(EMSESP_DASHBOARD_DATA_ENDPOINT, () => {
|
.get(EMSESP_DASHBOARD_DATA_ENDPOINT, () => {
|
||||||
// builds a JSON with id, t = typeID, tn = typeName, n=Name, data = [{n, v, u}]
|
|
||||||
let dashboard_data = [];
|
let dashboard_data = [];
|
||||||
let dashboard_object = {};
|
let dashboard_object = {};
|
||||||
|
|
||||||
@@ -4334,57 +4334,63 @@ router
|
|||||||
|
|
||||||
dashboard_object = {
|
dashboard_object = {
|
||||||
id: id,
|
id: id,
|
||||||
t: element.t,
|
|
||||||
tn: element.tn,
|
|
||||||
n: element.n,
|
n: element.n,
|
||||||
data: getDashboardEntityData(id)
|
nodes: getDashboardEntityData(id)
|
||||||
};
|
};
|
||||||
|
|
||||||
dashboard_data.push(dashboard_object); // add to dashboard_data
|
// only add to dashboard if we have values
|
||||||
|
if (dashboard_object.nodes.length > 0) {
|
||||||
|
dashboard_data.push(dashboard_object);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// add the custom entity data
|
// add the custom entity data
|
||||||
dashboard_object = {
|
dashboard_object = {
|
||||||
id: 99,
|
id: 99,
|
||||||
t: 99,
|
|
||||||
tn: 'custom',
|
|
||||||
n: 'Custom Entities',
|
n: 'Custom Entities',
|
||||||
data: getDashboardEntityData(99)
|
nodes: getDashboardEntityData(99)
|
||||||
};
|
};
|
||||||
dashboard_data.push(dashboard_object); // add to dashboard_data
|
// only add to dashboard if we have values
|
||||||
|
if (dashboard_object.nodes.length > 0) {
|
||||||
|
dashboard_data.push(dashboard_object);
|
||||||
|
}
|
||||||
|
|
||||||
// add temperature sensor data
|
// add temperature sensor data
|
||||||
let sensor_data = {};
|
let sensor_data = {};
|
||||||
sensor_data = emsesp_sensordata.ts.map((item) => ({
|
sensor_data = emsesp_sensordata.ts.map((item, index) => ({
|
||||||
|
id: 980 + index,
|
||||||
n: item.n ? item.n : item.id, // name may not be set
|
n: item.n ? item.n : item.id, // name may not be set
|
||||||
v: item.t ? item.t : undefined, // can have no value
|
v: item.t ? item.t : undefined, // can have no value
|
||||||
u: item.u
|
u: item.u
|
||||||
}));
|
}));
|
||||||
dashboard_object = {
|
dashboard_object = {
|
||||||
id: 98,
|
id: 98,
|
||||||
t: 98,
|
|
||||||
tn: 'ts',
|
|
||||||
n: 'Temperature Sensors',
|
n: 'Temperature Sensors',
|
||||||
data: sensor_data
|
nodes: sensor_data
|
||||||
};
|
};
|
||||||
dashboard_data.push(dashboard_object); // add to dashboard_data
|
// only add to dashboard if we have values
|
||||||
|
if (dashboard_object.nodes.length > 0) {
|
||||||
|
dashboard_data.push(dashboard_object);
|
||||||
|
}
|
||||||
|
|
||||||
// add analog sensor data
|
// add analog sensor data
|
||||||
sensor_data = emsesp_sensordata.as.map((item) => ({
|
sensor_data = emsesp_sensordata.as.map((item, index) => ({
|
||||||
|
id: 970 + index,
|
||||||
n: item.n,
|
n: item.n,
|
||||||
v: item.v,
|
v: item.v,
|
||||||
u: item.u
|
u: item.u
|
||||||
}));
|
}));
|
||||||
dashboard_object = {
|
dashboard_object = {
|
||||||
id: 97,
|
id: 97,
|
||||||
t: 97,
|
|
||||||
tn: 'as',
|
|
||||||
n: 'Analog Sensors',
|
n: 'Analog Sensors',
|
||||||
data: sensor_data
|
nodes: sensor_data
|
||||||
};
|
};
|
||||||
dashboard_data.push(dashboard_object); // add to dashboard_data
|
// only add to dashboard if we have values
|
||||||
|
if (dashboard_object.nodes.length > 0) {
|
||||||
|
dashboard_data.push(dashboard_object);
|
||||||
|
}
|
||||||
|
|
||||||
console.log('dashboard_data: ', dashboard_data);
|
// console.log('dashboard_data: ', dashboard_data);
|
||||||
|
|
||||||
return new Response(encoder.encode(dashboard_data), { headers }); // msgpack it
|
return new Response(encoder.encode(dashboard_data), { headers }); // msgpack it
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user