enable write in dashboard

This commit is contained in:
proddy
2024-10-04 13:32:42 +02:00
parent 01db9db6ae
commit fd7d8ca532
8 changed files with 286 additions and 165 deletions

View File

@@ -31,7 +31,7 @@
"async-validator": "^4.2.5", "async-validator": "^4.2.5",
"jwt-decode": "^4.0.0", "jwt-decode": "^4.0.0",
"mime-types": "^2.1.35", "mime-types": "^2.1.35",
"preact": "^10.24.1", "preact": "^10.24.2",
"react": "^18.3.1", "react": "^18.3.1",
"react-dom": "^18.3.1", "react-dom": "^18.3.1",
"react-icons": "^5.3.0", "react-icons": "^5.3.0",

View File

@@ -1,24 +1,38 @@
import { useEffect, useState } from 'react'; 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 ChevronRightIcon from '@mui/icons-material/ChevronRight';
import EditIcon from '@mui/icons-material/Edit';
import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
import UnfoldLessIcon from '@mui/icons-material/UnfoldLess'; import UnfoldLessIcon from '@mui/icons-material/UnfoldLess';
import UnfoldMoreIcon from '@mui/icons-material/UnfoldMore'; import UnfoldMoreIcon from '@mui/icons-material/UnfoldMore';
import { Box, ToggleButton, ToggleButtonGroup, Typography } from '@mui/material'; import {
Box,
IconButton,
ToggleButton,
ToggleButtonGroup,
Typography
} from '@mui/material';
import { Body, Cell, Row, Table } from '@table-library/react-table-library/table'; import { Body, Cell, Row, Table } from '@table-library/react-table-library/table';
import { useTheme } from '@table-library/react-table-library/theme'; import { useTheme } from '@table-library/react-table-library/theme';
import { CellTree, useTree } from '@table-library/react-table-library/tree'; import { CellTree, useTree } from '@table-library/react-table-library/tree';
import { useAutoRequest } from 'alova/client'; import { useAutoRequest, useRequest } from 'alova/client';
import { FormLoader, SectionContent, useLayoutTitle } from 'components'; import { FormLoader, SectionContent, useLayoutTitle } from 'components';
import { AuthenticatedContext } from 'contexts/authentication';
import { useI18nContext } from 'i18n/i18n-react'; import { useI18nContext } from 'i18n/i18n-react';
import { readDashboard } from '../../api/app'; import { readDashboard, writeDeviceValue } from '../../api/app';
import DeviceIcon from './DeviceIcon';
import DashboardDevicesDialog from './DevicesDialog';
import { formatValue } from './deviceValue'; import { formatValue } from './deviceValue';
import type { DashboardItem } from './types'; import { type DashboardItem, type DeviceValue } from './types';
import { deviceValueItemValidation } from './validators';
const Dashboard = () => { const Dashboard = () => {
const { LL } = useI18nContext(); const { LL } = useI18nContext();
const { me } = useContext(AuthenticatedContext);
useLayoutTitle('Dashboard'); // TODO translate useLayoutTitle('Dashboard'); // TODO translate
@@ -34,9 +48,39 @@ const Dashboard = () => {
pollingTime: 1500 pollingTime: 1500
}); });
const { loading: submitting, send: sendDeviceValue } = useRequest(
(data: { id: number; c: string; v: unknown }) => writeDeviceValue(data),
{
immediate: false
}
);
const [deviceValueDialogOpen, setDeviceValueDialogOpen] = useState(false);
const [selectedDeviceValue, setSelectedDeviceValue] = useState<DeviceValue>();
const deviceValueDialogClose = () => {
setDeviceValueDialogOpen(false);
void sendDeviceData(selectedDevice);
};
const deviceValueDialogSave = async (devicevalue: DeviceValue) => {
const id = Number(device_select.state.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(async () => {
setDeviceValueDialogOpen(false);
await sendDeviceData(id);
setSelectedDeviceValue(undefined);
});
};
const dashboard_theme = useTheme({ const dashboard_theme = useTheme({
Table: ` Table: `
--data-table-library_grid-template-columns: minmax(80px, auto) 120px; --data-table-library_grid-template-columns: minmax(80px, auto) 120px 40px;
`, `,
BaseRow: ` BaseRow: `
font-size: 14px; font-size: 14px;
@@ -46,10 +90,8 @@ const Dashboard = () => {
`, `,
Row: ` Row: `
background-color: #1e1e1e; background-color: #1e1e1e;
// position: relative;
// cursor: pointer;
.td { .td {
height: 24px; height: 22px;
} }
&:hover .td { &:hover .td {
border-top: 1px solid #177ac9; border-top: 1px solid #177ac9;
@@ -58,15 +100,8 @@ const Dashboard = () => {
` `
}); });
function onTreeChange(action, state) {
// do nothing for now
}
const tree = useTree( const tree = useTree(
{ nodes: data }, { nodes: data },
{
onChange: onTreeChange
},
{ {
treeIcon: { treeIcon: {
margin: '4px', margin: '4px',
@@ -84,22 +119,38 @@ const Dashboard = () => {
tree.fns.onToggleAll({}); tree.fns.onToggleAll({});
setFirstLoad(false); setFirstLoad(false);
} }
}, [data]); });
const showName = (di: DashboardItem) => { const showName = (di: DashboardItem) => {
if (di.id < 100) { if (di.id < 100) {
// if its a device row
if (di.nodes?.length) { if (di.nodes?.length) {
return ( return (
<span> <>
<span style={{ color: '#2196f3' }}>{di.n}</span> <span style="font-size: 14px;">
<DeviceIcon type_id={di.t} />
&nbsp;&nbsp;{di.n}
</span>
<span style={{ color: 'lightblue' }}>&nbsp;({di.nodes?.length})</span> <span style={{ color: 'lightblue' }}>&nbsp;({di.nodes?.length})</span>
</span> </>
); );
} }
return <div style={{ color: '#2196f3' }}>{di.n}</div>;
} }
return <div style={{ color: '#d3d3d3' }}>{di.n}</div>;
};
return <div>{di.n}</div>; const showDeviceValue = (di: DashboardItem) => {
// convert di to dv
// TODO should we not just use dv?
const dv: DeviceValue = {
id: ' ' + di.n,
v: di.v,
u: di.u,
c: di.c,
l: di.l
};
setSelectedDeviceValue(dv);
setDeviceValueDialogOpen(true);
}; };
const handleShowAll = ( const handleShowAll = (
@@ -129,6 +180,7 @@ const Dashboard = () => {
return ( return (
<> <>
<Typography mb={2} variant="body2" color="warning"> <Typography mb={2} variant="body2" color="warning">
{/* TODO translate */}
The dashboard shows all EMS entities that are marked as favorite, and the The dashboard shows all EMS entities that are marked as favorite, and the
sensors. sensors.
</Typography> </Typography>
@@ -158,33 +210,71 @@ const Dashboard = () => {
border: '1px solid grey' border: '1px solid grey'
}} }}
> >
<Table <IconContext.Provider
data={{ nodes: data }} value={{
theme={dashboard_theme} color: 'lightblue',
layout={{ custom: true }} size: '16',
tree={tree} style: { verticalAlign: 'middle' }
}}
> >
{(tableList: DashboardItem[]) => ( <Table
<Body> data={{ nodes: data }}
{tableList.map((di: DashboardItem) => ( theme={dashboard_theme}
<Row key={di.id} item={di} disabled={di.nodes?.length === 0}> layout={{ custom: true }}
{di.nodes?.length === 0 ? ( tree={tree}
<Cell>{showName(di)}</Cell> >
) : ( {(tableList: DashboardItem[]) => (
<CellTree item={di}>{showName(di)}</CellTree> <Body>
)} {tableList.map((di: DashboardItem) => (
<Cell pinRight>{formatValue(LL, di.v, di.u)}</Cell> <Row key={di.id} item={di} disabled={di.nodes?.length === 0}>
</Row> {di.nodes?.length === 0 ? (
))} <Cell>{showName(di)}</Cell>
</Body> ) : (
)} <CellTree item={di}>{showName(di)}</CellTree>
</Table> )}
<Cell pinRight>
<div style={{ color: '#d3d3d3' }}>
{formatValue(LL, di.v, di.u)}
</div>
</Cell>
<Cell stiff>
{me.admin && di.c && (
<IconButton
size="small"
onClick={() => showDeviceValue(di)}
>
<EditIcon color="primary" sx={{ fontSize: 16 }} />
</IconButton>
)}
</Cell>
</Row>
))}
</Body>
)}
</Table>
</IconContext.Provider>
</Box> </Box>
</> </>
); );
}; };
return <SectionContent>{renderContent()}</SectionContent>; return (
<SectionContent>
{renderContent()}
{selectedDeviceValue && (
<DashboardDevicesDialog
open={deviceValueDialogOpen}
onClose={deviceValueDialogClose}
onSave={deviceValueDialogSave}
selectedItem={selectedDeviceValue}
writeable={true}
validator={deviceValueItemValidation(selectedDeviceValue)}
progress={submitting}
/>
)}
</SectionContent>
);
}; };
export default Dashboard; export default Dashboard;

View File

@@ -2,6 +2,7 @@ import { AiOutlineAlert, AiOutlineControl, AiOutlineGateway } from 'react-icons/
import { CgSmartHomeBoiler } from 'react-icons/cg'; import { CgSmartHomeBoiler } from 'react-icons/cg';
import { FaSolarPanel } from 'react-icons/fa'; import { FaSolarPanel } from 'react-icons/fa';
import { GiHeatHaze, GiTap } from 'react-icons/gi'; import { GiHeatHaze, GiTap } from 'react-icons/gi';
import { MdPlaylistAdd } from 'react-icons/md';
import { import {
MdOutlineDevices, MdOutlineDevices,
MdOutlinePool, MdOutlinePool,
@@ -11,50 +12,40 @@ import {
import { TiFlowSwitch } from 'react-icons/ti'; import { TiFlowSwitch } from 'react-icons/ti';
import { VscVmConnect } from 'react-icons/vsc'; import { VscVmConnect } from 'react-icons/vsc';
import PlaylistAddIcon from '@mui/icons-material/PlaylistAdd'; import type { SvgIconProps } from '@mui/material';
import { DeviceType } from './types'; import { DeviceType } from './types';
const deviceIconLookup: {
[key in DeviceType]: React.ComponentType<SvgIconProps> | undefined;
} = {
[DeviceType.TEMPERATURESENSOR]: MdOutlineSensors,
[DeviceType.ANALOGSENSOR]: MdOutlineSensors,
[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]: undefined,
[DeviceType.SYSTEM]: undefined,
[DeviceType.SCHEDULER]: undefined,
[DeviceType.GENERIC]: undefined,
[DeviceType.VENTILATION]: undefined
};
const DeviceIcon = ({ type_id }: { type_id: DeviceType }) => { const DeviceIcon = ({ type_id }: { type_id: DeviceType }) => {
switch (type_id) { const Icon = deviceIconLookup[type_id];
case DeviceType.TEMPERATURESENSOR: return Icon ? <Icon /> : null;
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;
}
}; };
export default DeviceIcon; export default DeviceIcon;

View File

@@ -170,7 +170,7 @@ const Devices = () => {
common_theme, common_theme,
{ {
Table: ` 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: ` BaseRow: `
.td { .td {
@@ -527,57 +527,57 @@ const Devices = () => {
}; };
const renderCoreData = () => ( const renderCoreData = () => (
<IconContext.Provider <>
value={{ <IconContext.Provider
color: 'lightblue', value={{
size: '18', color: 'lightblue',
style: { verticalAlign: 'middle' } size: '18',
}} style: { verticalAlign: 'middle' }
> }}
{!coreData.connected && ( >
<MessageBox my={2} level="error" message={LL.EMS_BUS_WARNING()} /> {!coreData.connected && (
)} <MessageBox my={2} level="error" message={LL.EMS_BUS_WARNING()} />
)}
{coreData.connected && ( {coreData.connected && (
<Table <Table
data={{ nodes: coreData.devices }} data={{ nodes: coreData.devices }}
select={device_select} select={device_select}
theme={device_theme} theme={device_theme}
layout={{ custom: true }} layout={{ custom: true }}
> >
{(tableList: Device[]) => ( {(tableList: Device[]) => (
<> <>
<Header> <Header>
<HeaderRow> <HeaderRow>
<HeaderCell stiff /> <HeaderCell resize>{LL.DESCRIPTION()}</HeaderCell>
<HeaderCell resize>{LL.DESCRIPTION()}</HeaderCell> <HeaderCell stiff>{LL.TYPE(0)}</HeaderCell>
<HeaderCell stiff>{LL.TYPE(0)}</HeaderCell> </HeaderRow>
</HeaderRow> </Header>
</Header> <Body>
<Body> {tableList.length === 0 && (
{tableList.length === 0 && ( <CircularProgress sx={{ margin: 1 }} size={18} />
<CircularProgress sx={{ margin: 1 }} size={18} /> )}
)} {tableList.map((device: Device) => (
{tableList.map((device: Device) => ( <Row key={device.id} item={device}>
<Row key={device.id} item={device}> <Cell>
<Cell stiff> <DeviceIcon type_id={device.t} />
<DeviceIcon type_id={device.t} /> &nbsp;&nbsp;
</Cell> {device.n}
<Cell> <span style={{ color: 'lightblue' }}>
{device.n} &nbsp;&nbsp;({device.e})
<span style={{ color: 'lightblue' }}> </span>
&nbsp;&nbsp;({device.e}) </Cell>
</span> <Cell stiff>{device.tn}</Cell>
</Cell> </Row>
<Cell stiff>{device.tn}</Cell> ))}
</Row> </Body>
))} </>
</Body> )}
</> </Table>
)} )}
</Table> </IconContext.Provider>
)} </>
</IconContext.Provider>
); );
const deviceValueDialogClose = () => { const deviceValueDialogClose = () => {
@@ -733,7 +733,7 @@ const Devices = () => {
size="small" size="small"
onClick={() => showDeviceValue(dv)} onClick={() => showDeviceValue(dv)}
> >
{dv.v === '' && dv.c ? ( {dv.v === '' ? (
<PlayArrowIcon color="primary" sx={{ fontSize: 16 }} /> <PlayArrowIcon color="primary" sx={{ fontSize: 16 }} />
) : ( ) : (
<EditIcon color="primary" sx={{ fontSize: 16 }} /> <EditIcon color="primary" sx={{ fontSize: 16 }} />

View File

@@ -117,9 +117,13 @@ export interface CoreData {
export interface DashboardItem { export interface DashboardItem {
id: number; // unique index id: number; // unique index
n: string; // name n: string; // name
v?: unknown; // value v?: unknown; // value, optional
u: number; // uom u: number; // uom
nodes?: DashboardItem[]; // nodes t: number; // type from DeviceType
c?: string; // command, optional
l?: string[]; // list, optional
h?: string; // help text, optional
nodes?: DashboardItem[]; // nodes, optional
} }
export interface DashboardData { export interface DashboardData {

View File

@@ -144,8 +144,8 @@ const LayoutMenu = () => {
</List> </List>
<Divider /> <Divider />
<List> <List>
<ListItem disablePadding onClick={handleClick}> <ListItem disablePadding>
<ListItemButton> <ListItemButton component="button" onClick={handleClick}>
<ListItemIcon sx={{ color: '#9e9e9e' }}> <ListItemIcon sx={{ color: '#9e9e9e' }}>
<AccountCircleIcon /> <AccountCircleIcon />
</ListItemIcon> </ListItemIcon>

View File

@@ -1864,7 +1864,7 @@ __metadata:
formidable: "npm:^3.5.1" formidable: "npm:^3.5.1"
jwt-decode: "npm:^4.0.0" jwt-decode: "npm:^4.0.0"
mime-types: "npm:^2.1.35" mime-types: "npm:^2.1.35"
preact: "npm:^10.24.1" preact: "npm:^10.24.2"
prettier: "npm:^3.3.3" prettier: "npm:^3.3.3"
react: "npm:^18.3.1" react: "npm:^18.3.1"
react-dom: "npm:^18.3.1" react-dom: "npm:^18.3.1"
@@ -5793,10 +5793,10 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"preact@npm:^10.24.1": "preact@npm:^10.24.2":
version: 10.24.1 version: 10.24.2
resolution: "preact@npm:10.24.1" resolution: "preact@npm:10.24.2"
checksum: 10c0/f9bc8b2f88d340f1b8f854208889244059c46916449b8f8f2174fcacbc0904c445c5870896fb0cfeaf442eeade975857e8e03f0785135c41d63cd32d9414c9c6 checksum: 10c0/d1d22c5e1abc10eb8f83501857ef22c54a3fda2d20449d06f5b3c7d5ae812bd702c16c05b672138b8906504f9c893e072e9cebcbcada8cac320edf36265788fb
languageName: node languageName: node
linkType: hard linkType: hard

View File

@@ -32,6 +32,32 @@ const headers = {
let countWifiScanPoll = 0; // wifi network scan let countWifiScanPoll = 0; // wifi network scan
let countHardwarePoll = 0; // for during an upload let countHardwarePoll = 0; // for during an upload
// DeviceTypes
const enum DeviceType {
SYSTEM = 0,
TEMPERATURESENSOR,
ANALOGSENSOR,
SCHEDULER,
CUSTOM,
BOILER,
THERMOSTAT,
MIXER,
SOLAR,
HEATPUMP,
GATEWAY,
SWITCH,
CONTROLLER,
CONNECT,
ALERT,
EXTENSION,
GENERIC,
HEATSOURCE,
VENTILATION,
WATER,
POOL,
UNKNOWN
}
function updateMask(entity: any, de: any, dd: any) { function updateMask(entity: any, de: any, dd: any) {
const current_mask = parseInt(entity.slice(0, 2), 16); const current_mask = parseInt(entity.slice(0, 2), 16);
@@ -1708,7 +1734,7 @@ const emsesp_devicedata_3 = {
{ {
v: 'hot', v: 'hot',
u: 0, u: 0,
id: '00dhw comfort', id: '08dhw comfort',
c: 'dhw/comfort', c: 'dhw/comfort',
l: ['hot', 'eco', 'intelligent'] l: ['hot', 'eco', 'intelligent']
}, },
@@ -4268,11 +4294,11 @@ function getDashboardEntityData(id: number) {
else if (id == 10) device_data = emsesp_devicedata_10; else if (id == 10) device_data = emsesp_devicedata_10;
else if (id == 99) device_data = emsesp_devicedata_99; else if (id == 99) device_data = emsesp_devicedata_99;
// filter device_data, just want id, v, u // filter device_data
// and only favorite items (bit 8 set), only for non-Custom Entities // and only favorite items (bit 8 set), only for non-Custom Entities
// and replace id by striping off the 2-char mask // and replace id by striping off the 2-char mask
let new_data = (device_data as any).data let new_data = (device_data as any).data
.map(({ id, c, m, x, s, h, l, ...rest }) => ({ .map(({ id, m, x, s, ...rest }) => ({
...rest, ...rest,
id2: id id2: id
})) }))
@@ -4281,9 +4307,14 @@ function getDashboardEntityData(id: number) {
id: id * 100 + index, // unique id 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
c: item.c, // command
l: item.l, // list
h: item.h // help
})); }));
// TODO only and command if not marked as READONLY
return new_data; return new_data;
} }
@@ -4325,10 +4356,12 @@ 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, () => {
let dashboard_data = []; let dashboard_data: { id?: number; n?: string; t?: number; nodes?: any[] }[] =
let dashboard_object = {}; [];
let dashboard_object: { id?: number; n?: string; t?: number; nodes?: any[] } =
{};
let fake = false; let fake = false;
// let fake = true; // let fake = true; // fakes no data
if (!fake) { if (!fake) {
// pick EMS devices from coredata // pick EMS devices from coredata
@@ -4338,11 +4371,12 @@ router
dashboard_object = { dashboard_object = {
id: id, id: id,
n: element.n, n: element.n,
t: element.t,
nodes: getDashboardEntityData(id) nodes: getDashboardEntityData(id)
}; };
// only add to dashboard if we have values // only add to dashboard if we have values
if (dashboard_object.nodes.length > 0) { if ((dashboard_object.nodes ?? []).length > 0) {
dashboard_data.push(dashboard_object); dashboard_data.push(dashboard_object);
} }
} }
@@ -4351,15 +4385,16 @@ router
dashboard_object = { dashboard_object = {
id: 99, id: 99,
n: 'Custom Entities', n: 'Custom Entities',
t: 4, // DeviceType::CUSTOM
nodes: getDashboardEntityData(99) nodes: getDashboardEntityData(99)
}; };
// only add to dashboard if we have values // only add to dashboard if we have values
if (dashboard_object.nodes.length > 0) { if ((dashboard_object.nodes ?? []).length > 0) {
dashboard_data.push(dashboard_object); dashboard_data.push(dashboard_object);
} }
// add temperature sensor data // add temperature sensor data. no command c
let sensor_data = {}; let sensor_data: any[] = [];
sensor_data = emsesp_sensordata.ts.map((item, index) => ({ sensor_data = emsesp_sensordata.ts.map((item, index) => ({
id: 980 + 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
@@ -4369,17 +4404,17 @@ router
dashboard_object = { dashboard_object = {
id: 98, id: 98,
n: 'Temperature Sensors', n: 'Temperature Sensors',
t: 1, // DeviceType::TEMPERATURESENSOR
nodes: sensor_data nodes: sensor_data
}; };
// only add to dashboard if we have values // only add to dashboard if we have values
if (dashboard_object.nodes.length > 0) { if ((dashboard_object.nodes ?? []).length > 0) {
dashboard_data.push(dashboard_object); dashboard_data.push(dashboard_object);
} }
// add analog sensor data // add analog sensor data. no command c
// remove disabled sensors (t = 0) // remove disabled sensors first (t = 0)
sensor_data = emsesp_sensordata.as.filter((item) => item.t !== 0); sensor_data = emsesp_sensordata.as.filter((item) => item.t !== 0);
sensor_data = sensor_data.map((item, index) => ({ sensor_data = sensor_data.map((item, index) => ({
id: 970 + index, id: 970 + index,
n: item.n, n: item.n,
@@ -4390,10 +4425,11 @@ router
dashboard_object = { dashboard_object = {
id: 97, id: 97,
n: 'Analog Sensors', n: 'Analog Sensors',
t: 2, // DeviceType::ANALOGSENSOR
nodes: sensor_data nodes: sensor_data
}; };
// only add to dashboard if we have values // only add to dashboard if we have values
if (dashboard_object.nodes.length > 0) { if ((dashboard_object.nodes ?? []).length > 0) {
dashboard_data.push(dashboard_object); dashboard_data.push(dashboard_object);
} }
} }