diff --git a/interface/src/project/DashboardData.tsx b/interface/src/project/DashboardData.tsx index 3c64dc673..0ba5600c5 100644 --- a/interface/src/project/DashboardData.tsx +++ b/interface/src/project/DashboardData.tsx @@ -24,10 +24,11 @@ import { useSnackbar } from 'notistack'; import { Table } from '@table-library/react-table-library/table'; import { useTheme } from '@table-library/react-table-library/theme'; -import { useSort, HeaderCellSort } from '@table-library/react-table-library/sort'; +import { useSort } from '@table-library/react-table-library/sort'; import { Header, HeaderRow, HeaderCell, Body, Row, Cell } from '@table-library/react-table-library/table'; import { useRowSelect } from '@table-library/react-table-library/select'; +import DownloadIcon from '@mui/icons-material/GetApp'; import RefreshIcon from '@mui/icons-material/Refresh'; import EditIcon from '@mui/icons-material/Edit'; import InfoOutlinedIcon from '@mui/icons-material/InfoOutlined'; @@ -98,8 +99,8 @@ const DashboardData: FC = () => { } `, HeaderRow: ` + text-transform: uppercase; background-color: black; - font-size: 14px; border-bottom: 1px solid #e0e0e0; `, Row: ` @@ -165,8 +166,9 @@ const DashboardData: FC = () => { height: 32px; `, HeaderRow: ` + text-transform: uppercase; background-color: black; - font-size: 14px; + color: #90CAF9; border-bottom: 1px solid #e0e0e0; `, Row: ` @@ -195,6 +197,7 @@ const DashboardData: FC = () => { } `, BaseCell: ` + padding-left: 8px; border-top: 1px solid transparent; border-right: 1px solid transparent; border-bottom: 1px solid transparent; @@ -202,9 +205,22 @@ const DashboardData: FC = () => { text-align: right; min-width: 64px; } + `, + HeaderCell: ` + padding-left: 0px; ` }); + const getSortIcon = (state: any, sortKey: any) => { + if (state.sortKey === sortKey && state.reverse) { + return ; + } + + if (state.sortKey === sortKey && !state.reverse) { + return ; + } + }; + const analog_sort = useSort( { nodes: sensorData.analogs }, { @@ -222,7 +238,8 @@ const DashboardData: FC = () => { sortFns: { GPIO: (array) => array.sort((a, b) => a.i - b.i), NAME: (array) => array.sort((a, b) => a.n.localeCompare(b.n)), - TYPE: (array) => array.sort((a, b) => a.t - b.t) + TYPE: (array) => array.sort((a, b) => a.t - b.t), + VALUE: (array) => array.sort((a, b) => a.v.toString().localeCompare(b.v.toString())) } } ); @@ -250,16 +267,21 @@ const DashboardData: FC = () => { const dv_sort = useSort( { nodes: deviceData.data }, - {}, + { + state: { + sortKey: 'NAME', + reverse: false + } + }, { sortIcon: { - margin: '0px', iconDefault: null, iconUp: , iconDown: }, sortFns: { - NAME: (array) => array.sort((a, b) => a.id.slice(2).localeCompare(b.id.slice(2))) + NAME: (array) => array.sort((a, b) => a.id.slice(2).localeCompare(b.id.slice(2))), + VALUE: (array) => array.sort((a, b) => a.v.toString().localeCompare(b.v.toString())) } } ); @@ -279,6 +301,52 @@ const DashboardData: FC = () => { } } + const escapeCsvCell = (cell: any) => { + if (cell == null) { + return ''; + } + const sc = cell.toString().trim(); + if (sc === '' || sc === '""') { + return sc; + } + if (sc.includes('"') || sc.includes(',') || sc.includes('\n') || sc.includes('\r')) { + return '"' + sc.replace(/"/g, '""') + '"'; + } + return sc; + }; + + const makeCsvData = (columns: any, data: any) => { + return data.reduce((csvString: any, rowItem: any) => { + return csvString + columns.map(({ accessor }: any) => escapeCsvCell(accessor(rowItem))).join(',') + '\r\n'; + }, columns.map(({ name }: any) => escapeCsvCell(name)).join(',') + '\r\n'); + }; + + const downloadAsCsv = (columns: any, data: any, filename: string) => { + const csvData = makeCsvData(columns, data); + const csvFile = new Blob([csvData], { type: 'text/csv' }); + const downloadLink = document.createElement('a'); + + downloadLink.download = filename; + downloadLink.href = window.URL.createObjectURL(csvFile); + document.body.appendChild(downloadLink); + downloadLink.click(); + document.body.removeChild(downloadLink); + }; + + const hasMask = (id: string, mask: number) => (parseInt(id.slice(0, 2), 16) & mask) === mask; + + const handleDownloadCsv = () => { + const columns = [ + { accessor: (dv: any) => dv.id.slice(2), name: 'Entity' }, + { accessor: (dv: any) => formatValue(dv.v, dv.u), name: 'Value' } + ]; + downloadAsCsv( + columns, + onlyFav ? deviceData.data.filter((dv) => hasMask(dv.id, DeviceEntityMask.DV_FAVORITE)) : deviceData.data, + 'device_entities' + ); + }; + const refreshData = () => { const selectedDevice = device_select.state.id; if (selectedDevice === 'sensor') { @@ -451,7 +519,7 @@ const DashboardData: FC = () => { }; const addAnalogSensor = () => { - setAnalog({ id: '0', n: '', u: 0, v: 0, o: 0, t: 0, f: 1 }); + setAnalog({ id: '0', i: 0, n: '', u: 0, v: 0, o: 0, t: 0, f: 1 }); }; const sendSensor = async () => { @@ -585,8 +653,6 @@ const DashboardData: FC = () => { return ; } - console.log('** Rendering main data'); - return ( {coreData.devices.length === 0 && } @@ -647,8 +713,6 @@ const DashboardData: FC = () => { return; } - const hasMask = (id: string, mask: number) => (parseInt(id.slice(0, 2), 16) & mask) === mask; - const sendCommand = (dv: DeviceValue) => { if (dv.c && me.admin && !hasMask(dv.id, DeviceEntityMask.DV_READONLY)) { setDeviceValue(dv); @@ -672,8 +736,8 @@ const DashboardData: FC = () => { setOnlyFav(!onlyFav)} />} - label="show favorites only" + control={ setOnlyFav(!onlyFav)} />} + label={favorites only} /> { <>
- - ENTITY NAME - - VALUE + + + + + +
@@ -745,10 +825,26 @@ const DashboardData: FC = () => { <>
- - NAME - - TEMPERATURE + + + + + +
@@ -784,16 +880,46 @@ const DashboardData: FC = () => { <>
- - GPIO - - - NAME - - - TYPE - - VALUE + + + + + + + + + + + +
@@ -803,7 +929,7 @@ const DashboardData: FC = () => { {a.id}{a.n}{AnalogTypeNames[a.t]} - {formatValue(a.v, a.u)} + {a.t ? formatValue(a.v, a.u) : ''} {me.admin && ( updateAnalog(a)}> @@ -824,7 +950,7 @@ const DashboardData: FC = () => { if (analog) { try { const response = await EMSESP.writeAnalog({ - id: analog.id, + i: analog.i, name: analog.n, offset: analog.o, factor: analog.f, @@ -852,7 +978,7 @@ const DashboardData: FC = () => { if (analog) { try { const response = await EMSESP.writeAnalog({ - id: analog.id, + i: analog.i, name: analog.n, offset: analog.o, factor: analog.f, @@ -885,11 +1011,10 @@ const DashboardData: FC = () => { { + {device_select.state.id && device_select.state.id !== 'sensor' && ( + + )} ); diff --git a/interface/src/project/types.ts b/interface/src/project/types.ts index d3d096aea..db3433a10 100644 --- a/interface/src/project/types.ts +++ b/interface/src/project/types.ts @@ -88,9 +88,10 @@ export interface Sensor { } export interface Analog { - id: string; // id string, is GPIO + id: string; // id string + i: number; // GPIO n: string; - v?: number; + v: number; // is optional u: number; o: number; f: number; @@ -127,10 +128,10 @@ export interface Devices { export interface DeviceValue { id: string; // index, contains mask+name - v?: any; // value, in any format + v: any; // value, in any format u: number; // uom - c?: string; // command - l?: string[]; // list + c?: string; // command, optional + l?: string[]; // list, optional h?: string; // help text, optional s?: string; // steps for up/down, optional m?: string; // min, optional @@ -277,7 +278,7 @@ export interface WriteValue { } export interface WriteAnalog { - id: string; + i: number; name: string; factor: number; offset: number; diff --git a/mock-api/server.js b/mock-api/server.js index 0645388ad..66fbacdf5 100644 --- a/mock-api/server.js +++ b/mock-api/server.js @@ -410,10 +410,10 @@ const emsesp_sensordata = { ], // sensors: [], analogs: [ - { id: '36', n: 'motor', u: 0, o: 17, f: 0, t: 0 }, - { id: '37', n: 'External switch', v: 13, u: 0, o: 17, f: 0, t: 1 }, - { id: '39', n: 'Pulse count', v: 144, u: 0, o: 0, f: 0, t: 2 }, - { id: '40', n: 'Pressure', v: 16, u: 17, o: 0, f: 0, t: 3 }, + { id: '36', i: 36, n: 'motor', v: 0, u: 0, o: 17, f: 0, t: 0 }, + { id: '37', i: 37, n: 'External switch', v: 13, u: 0, o: 17, f: 0, t: 1 }, + { id: '39', i: 39, n: 'Pulse count', v: 144, u: 0, o: 0, f: 0, t: 2 }, + { id: '40', i: 40, n: 'Pressure', v: 16, u: 17, o: 0, f: 0, t: 3 }, ], // analogs: [], } @@ -485,7 +485,7 @@ const emsesp_devicedata_1 = { const emsesp_devicedata_2 = { label: 'Boiler: Nefit GBx72/Trendline/Cerapur/Greenstar Si/27i', data: [ - { u: 0, id: '08reset', c: 'reset', l: ['-', 'maintenance', 'error'] }, + { v: 0, u: 0, id: '08reset', c: 'reset', l: ['-', 'maintenance', 'error'] }, { v: 'false', u: 0, id: '08heating active' }, { v: 'false', u: 0, id: '04tapwater active' }, { v: 5, u: 1, id: '04selected flow temperature', c: 'selflowtemp' }, @@ -872,7 +872,6 @@ rest_server.get(EMSESP_CORE_DATA_ENDPOINT, (req, res) => { }) rest_server.get(EMSESP_SENSOR_DATA_ENDPOINT, (req, res) => { console.log('send back sensor data...') - res.json(emsesp_sensordata) }) rest_server.get(EMSESP_DEVICES_ENDPOINT, (req, res) => { @@ -996,12 +995,13 @@ rest_server.post(EMSESP_WRITE_SENSOR_ENDPOINT, (req, res) => { rest_server.post(EMSESP_WRITE_ANALOG_ENDPOINT, (req, res) => { const analog = req.body console.log('Write analog: ' + JSON.stringify(analog)) - objIndex = emsesp_sensordata.analogs.findIndex((obj) => obj.id == analog.id) + objIndex = emsesp_sensordata.analogs.findIndex((obj) => obj.i == analog.i) if (objIndex === -1) { console.log('new analog') emsesp_sensordata.analogs.push({ - id: analog.id, + id: analog.i.toString(), + i: analog.i, n: analog.name, f: analog.factor, o: analog.offset, @@ -1010,9 +1010,10 @@ rest_server.post(EMSESP_WRITE_ANALOG_ENDPOINT, (req, res) => { }) } else { if (analog.type === -1) { - console.log('removing analog ' + analog.id) + console.log('removing analog ' + analog.i) emsesp_sensordata.analogs[objIndex].t = -1 } else { + console.log('updating analog ' + analog.i) emsesp_sensordata.analogs[objIndex].n = analog.name emsesp_sensordata.analogs[objIndex].o = analog.offset emsesp_sensordata.analogs[objIndex].f = analog.factor