import { FC, useState, useContext, useCallback, useEffect } from 'react'; import { Button, Typography, Box, Dialog, DialogTitle, DialogContent, DialogActions, MenuItem, InputAdornment, FormHelperText, IconButton, List, ListItem, ListItemText, Grid, FormControlLabel, Checkbox } from '@mui/material'; import { useSnackbar } from 'notistack'; import { Table } from '@table-library/react-table-library/table'; import { useTheme } from '@table-library/react-table-library/theme'; import { useSort, SortToggleType } 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'; import AddCircleOutlineOutlinedIcon from '@mui/icons-material/AddCircleOutlineOutlined'; import CancelIcon from '@mui/icons-material/Cancel'; import SendIcon from '@mui/icons-material/TrendingFlat'; import SaveIcon from '@mui/icons-material/Save'; import RemoveIcon from '@mui/icons-material/RemoveCircleOutline'; import PlayArrowIcon from '@mui/icons-material/PlayArrow'; import EditOffOutlinedIcon from '@mui/icons-material/EditOffOutlined'; import CommentsDisabledOutlinedIcon from '@mui/icons-material/CommentsDisabledOutlined'; import KeyboardArrowUpOutlinedIcon from '@mui/icons-material/KeyboardArrowUpOutlined'; import KeyboardArrowDownOutlinedIcon from '@mui/icons-material/KeyboardArrowDownOutlined'; import UnfoldMoreOutlinedIcon from '@mui/icons-material/UnfoldMoreOutlined'; import StarIcon from '@mui/icons-material/Star'; import DeviceIcon from './DeviceIcon'; import { IconContext } from 'react-icons'; import { AuthenticatedContext } from '../contexts/authentication'; import { ButtonRow, ValidatedTextField, SectionContent, MessageBox } from '../components'; import * as EMSESP from './api'; import { numberValue, updateValue, extractErrorMessage } from '../utils'; import { SensorData, Device, CoreData, DeviceData, DeviceValue, DeviceValueUOM, DeviceValueUOM_s, AnalogType, AnalogTypeNames, Sensor, Analog, DeviceEntityMask } from './types'; import { useI18nContext } from '../i18n/i18n-react'; const DashboardData: FC = () => { const { me } = useContext(AuthenticatedContext); const { LL } = useI18nContext(); const { enqueueSnackbar } = useSnackbar(); const [coreData, setCoreData] = useState({ connected: true, devices: [], s_n: '', active_sensors: 0, analog_enabled: false }); const [deviceData, setDeviceData] = useState({ label: '', data: [] }); const [sensorData, setSensorData] = useState({ sensors: [], analogs: [] }); const [deviceValue, setDeviceValue] = useState(); const [sensor, setSensor] = useState(); const [analog, setAnalog] = useState(); const [deviceDialog, setDeviceDialog] = useState(-1); const [onlyFav, setOnlyFav] = useState(false); const common_theme = useTheme({ BaseRow: ` font-size: 14px; `, HeaderRow: ` text-transform: uppercase; background-color: black; color: #90CAF9; .th { border-bottom: 1px solid #565656; } `, Row: ` background-color: #1e1e1e; position: relative; 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; color: white; font-weight: normal; } &:hover .td { border-top: 1px solid #177ac9; border-bottom: 1px solid #177ac9; } `, Cell: ` &:last-of-type { text-align: right; }, ` }); const device_theme = useTheme([ common_theme, { Table: ` --data-table-library_grid-template-columns: 40px 160px repeat(1, minmax(0, 1fr)) 100px 40px; `, BaseRow: ` .td { height: 42px; } `, BaseCell: ` &:nth-of-type(2) { text-align: left; }, &:nth-of-type(4) { text-align: center; } `, HeaderRow: ` .th { padding: 8px; height: 42px; font-weight: 500; ` } ]); const data_theme = useTheme([ common_theme, { Table: ` --data-table-library_grid-template-columns: minmax(0, 1fr) 35% 40px; `, BaseRow: ` .td { height: 32px; } `, BaseCell: ` &:nth-of-type(2) { text-align: right; }, `, HeaderRow: ` .th { height: 32px; } `, Row: ` &:nth-of-type(odd) .td { background-color: #303030; } ` } ]); const temperature_theme = useTheme([data_theme]); const analog_theme = useTheme([ data_theme, { Table: ` --data-table-library_grid-template-columns: 80px repeat(1, minmax(0, 1fr)) 120px 100px 40px; `, BaseCell: ` &:nth-of-type(2) { text-align: left; }, &:nth-of-type(4) { text-align: right; } ` } ]); const getSortIcon = (state: any, sortKey: any) => { if (state.sortKey === sortKey && state.reverse) { return ; } if (state.sortKey === sortKey && !state.reverse) { return ; } return ; }; const analog_sort = useSort( { nodes: sensorData.analogs }, {}, { sortIcon: { iconDefault: , iconUp: , iconDown: }, sortToggleType: SortToggleType.AlternateWithReset, sortFns: { GPIO: (array) => array.sort((a, b) => a.g - b.g), NAME: (array) => array.sort((a, b) => a.n.localeCompare(b.n)), TYPE: (array) => array.sort((a, b) => a.t - b.t) } } ); const sensor_sort = useSort( { nodes: sensorData.sensors }, {}, { sortIcon: { iconDefault: , iconUp: , iconDown: }, sortToggleType: SortToggleType.AlternateWithReset, sortFns: { NAME: (array) => array.sort((a, b) => a.n.localeCompare(b.n)), TEMPERATURE: (array) => array.sort((a, b) => a.t - b.t) } } ); const dv_sort = useSort( { nodes: deviceData.data }, {}, { sortIcon: { iconDefault: , iconUp: , iconDown: }, sortToggleType: SortToggleType.AlternateWithReset, sortFns: { 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())) } } ); const device_select = useRowSelect( { nodes: coreData.devices }, { onChange: onSelectChange } ); function onSelectChange(action: any, state: any) { if (action.type === 'ADD_BY_ID_EXCLUSIVELY') { refreshData(); } else { setSensorData({ sensors: [], analogs: [] }); } } 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;charset:utf-8' }); 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: LL.ENTITY_NAME() }, { accessor: (dv: any) => (typeof dv.v === 'number' ? new Intl.NumberFormat().format(dv.v) : dv.v), name: LL.VALUE(0) }, { accessor: (dv: any) => DeviceValueUOM_s[dv.u], name: 'UoM' } ]; downloadAsCsv( columns, onlyFav ? deviceData.data.filter((dv) => hasMask(dv.id, DeviceEntityMask.DV_FAVORITE)) : deviceData.data, 'device_entities' ); }; const refreshDataIndex = (selectedDevice: string) => { if (selectedDevice === 'sensor') { fetchSensorData(); return; } setSensorData({ sensors: [], analogs: [] }); if (selectedDevice) { fetchDeviceData(selectedDevice); } else { fetchCoreData(); } }; const refreshData = () => { refreshDataIndex(device_select.state.id); }; const fetchCoreData = useCallback(async () => { try { setCoreData((await EMSESP.readCoreData()).data); } catch (error) { enqueueSnackbar(extractErrorMessage(error, LL.PROBLEM_LOADING()), { variant: 'error' }); } }, [enqueueSnackbar, LL]); useEffect(() => { fetchCoreData(); }, [fetchCoreData]); useEffect(() => { const timer = setInterval(() => refreshData(), 60000); return () => { clearInterval(timer); }; // eslint-disable-next-line }, [analog, sensor, deviceValue, sensorData]); const fetchDeviceData = async (id: string) => { const unique_id = parseInt(id); try { setDeviceData((await EMSESP.readDeviceData({ id: unique_id })).data); } catch (error) { enqueueSnackbar(extractErrorMessage(error, LL.PROBLEM_LOADING()), { variant: 'error' }); } }; const fetchSensorData = async () => { try { setSensorData((await EMSESP.readSensorData()).data); } catch (error) { enqueueSnackbar(extractErrorMessage(error, LL.PROBLEM_LOADING()), { variant: 'error' }); } }; const isCmdOnly = (dv: DeviceValue) => dv.v === '' && dv.c; const formatDurationMin = (duration_min: number) => { const days = Math.trunc((duration_min * 60000) / 86400000); const hours = Math.trunc((duration_min * 60000) / 3600000) % 24; const minutes = Math.trunc((duration_min * 60000) / 60000) % 60; let formatted = ''; if (days) { formatted += LL.NUM_DAYS({ num: days }) + ' '; } if (hours) { formatted += LL.NUM_HOURS({ num: hours }) + ' '; } if (minutes) { formatted += LL.NUM_MINUTES({ num: minutes }); } return formatted; }; function formatValue(value: any, uom: number) { if (value === undefined) { return ''; } switch (uom) { case DeviceValueUOM.HOURS: return value ? formatDurationMin(value * 60) : LL.NUM_HOURS({ num: 0 }); case DeviceValueUOM.MINUTES: return value ? formatDurationMin(value) : LL.NUM_MINUTES({ num: 0 }); case DeviceValueUOM.SECONDS: return LL.NUM_SECONDS({ num: value }); case DeviceValueUOM.NONE: if (typeof value === 'number') { return new Intl.NumberFormat().format(value); } return value; case DeviceValueUOM.DEGREES: case DeviceValueUOM.DEGREES_R: case DeviceValueUOM.FAHRENHEIT: return ( new Intl.NumberFormat(undefined, { minimumFractionDigits: 1 }).format(value) + ' ' + DeviceValueUOM_s[uom] ); default: return new Intl.NumberFormat().format(value) + ' ' + DeviceValueUOM_s[uom]; } } const setUom = (uom: number) => { switch (uom) { case DeviceValueUOM.HOURS: return LL.HOURS(); case DeviceValueUOM.MINUTES: return LL.MINUTES(); case DeviceValueUOM.SECONDS: return LL.SECONDS(); default: return DeviceValueUOM_s[uom]; } }; const sendDeviceValue = async () => { if (deviceValue) { try { const response = await EMSESP.writeValue({ id: Number(device_select.state.id), devicevalue: deviceValue }); if (response.status === 204) { enqueueSnackbar(LL.WRITE_CMD_FAILED(), { variant: 'error' }); } else if (response.status === 403) { enqueueSnackbar(LL.ACCESS_DENIED(), { variant: 'error' }); } else { enqueueSnackbar(LL.WRITE_CMD_SENT(), { variant: 'success' }); } setDeviceValue(undefined); } catch (error) { enqueueSnackbar(extractErrorMessage(error, LL.PROBLEM_UPDATING()), { variant: 'error' }); } finally { refreshData(); setDeviceValue(undefined); } } }; const renderDeviceValueDialog = () => { if (deviceValue) { return ( setDeviceValue(undefined)}> {isCmdOnly(deviceValue) ? LL.RUN_COMMAND() : LL.CHANGE_VALUE()} {deviceValue.l && ( {deviceValue.l.map((val) => ( {val} ))} )} {!deviceValue.l && ( {setUom(deviceValue.u)} }} /> )} {deviceValue.h && {deviceValue.h}} ); } }; const addAnalogSensor = () => { setAnalog({ id: '0', g: 0, n: '', u: 0, v: 0, o: 0, t: 0, f: 1 }); }; const sendSensor = async () => { if (sensor) { try { const response = await EMSESP.writeSensor({ id: sensor.id, name: sensor.n, offset: sensor.o }); if (response.status === 204) { enqueueSnackbar(LL.UPLOAD_OF(LL.SENSOR()) + ' ' + LL.FAILED(), { variant: 'error' }); } else if (response.status === 403) { enqueueSnackbar(LL.ACCESS_DENIED(), { variant: 'error' }); } else { enqueueSnackbar(LL.UPDATED_OF(LL.SENSOR()), { variant: 'success' }); } setSensor(undefined); } catch (error) { enqueueSnackbar(extractErrorMessage(error, LL.PROBLEM_UPDATING()), { variant: 'error' }); } finally { setSensor(undefined); fetchSensorData(); } } }; const renderSensorDialog = () => { if (sensor) { return ( setSensor(undefined)}> {LL.EDIT()} {LL.TEMP_SENSOR()} {LL.ID_OF(LL.SENSOR())}: {sensor.id} °C }} /> ); } }; const renderDeviceDialog = () => { if (coreData && coreData.devices.length > 0 && deviceDialog !== -1) { return ( setDeviceDialog(-1)}> {LL.DEVICE_DETAILS()} ); } }; const renderCoreData = () => ( {!coreData.connected && } {coreData.connected && coreData.devices.length === 0 && ( )} {(tableList: any) => ( <>
{LL.TYPE()} {LL.DESCRIPTION()} {LL.ENTITIES()}
{tableList.map((device: Device, index: number) => ( {device.tn} {device.n} {device.e} setDeviceDialog(index)}> ))} {(coreData.active_sensors > 0 || coreData.analog_enabled) && ( {coreData.s_n} {LL.ATTACHED_SENSORS()} {coreData.active_sensors} addAnalogSensor()}> )} )}
); const renderDeviceData = () => { if (!device_select.state.id || device_select.state.id === 'sensor') { return; } const sendCommand = (dv: DeviceValue) => { if (dv.c && me.admin && !hasMask(dv.id, DeviceEntityMask.DV_READONLY)) { setDeviceValue(dv); } }; const renderNameCell = (dv: DeviceValue) => ( <> {dv.id.slice(2)}  {hasMask(dv.id, DeviceEntityMask.DV_FAVORITE) && } {hasMask(dv.id, DeviceEntityMask.DV_READONLY) && } {hasMask(dv.id, DeviceEntityMask.DV_API_MQTT_EXCLUDE) && ( )} ); return ( <> {deviceData.label} setOnlyFav(!onlyFav)} />} label={ {LL.SHOW_FAV()}  } /> hasMask(dv.id, DeviceEntityMask.DV_FAVORITE)) : deviceData.data }} theme={data_theme} sort={dv_sort} layout={{ custom: true }} > {(tableList: any) => ( <>
{tableList.map((dv: DeviceValue) => ( sendCommand(dv)}> {renderNameCell(dv)} {formatValue(dv.v, dv.u)} {dv.c && me.admin && !hasMask(dv.id, DeviceEntityMask.DV_READONLY) && ( sendCommand(dv)}> {isCmdOnly(dv) ? ( ) : ( )} )} ))} )}
); }; const updateSensor = (s: Sensor) => { if (s && me.admin) { setSensor(s); } }; const updateAnalog = (a: Analog) => { if (me.admin) { setAnalog(a); } }; const renderDallasData = () => ( <> {LL.TEMP_SENSORS()} {(tableList: any) => ( <>
{tableList.map((s: Sensor) => ( updateSensor(s)}> {s.n} {formatValue(s.t, s.u)} {me.admin && ( updateSensor(s)}> )} ))} )}
); const renderAnalogData = () => ( <> {LL.ANALOG_SENSORS()} {(tableList: any) => ( <>
{LL.VALUE(0)}
{tableList.map((a: Analog) => ( updateAnalog(a)}> {a.g} {a.n} {AnalogTypeNames[a.t]} {a.t ? formatValue(a.v, a.u) : ''} {me.admin && ( updateAnalog(a)}> )} ))} )}
); const sendRemoveAnalog = async () => { if (analog) { try { const response = await EMSESP.writeAnalog({ gpio: analog.g, name: analog.n, offset: analog.o, factor: analog.f, uom: analog.u, type: -1 }); if (response.status === 204) { enqueueSnackbar(LL.DELETION_OF(LL.ANALOG_SENSOR()) + ' ' + LL.FAILED(), { variant: 'error' }); } else if (response.status === 403) { enqueueSnackbar(LL.ACCESS_DENIED(), { variant: 'error' }); } else { enqueueSnackbar(LL.REMOVED_OF(LL.ANALOG_SENSOR()), { variant: 'success' }); } } catch (error) { enqueueSnackbar(extractErrorMessage(error, LL.PROBLEM_UPDATING()), { variant: 'error' }); } finally { setAnalog(undefined); fetchSensorData(); } } }; const sendAnalog = async () => { if (analog) { try { const response = await EMSESP.writeAnalog({ gpio: analog.g, name: analog.n, offset: analog.o, factor: analog.f, uom: analog.u, type: analog.t }); if (response.status === 204) { enqueueSnackbar(LL.UPDATE_OF(LL.ANALOG_SENSOR()) + ' ' + LL.FAILED(), { variant: 'error' }); } else if (response.status === 403) { enqueueSnackbar(LL.ACCESS_DENIED(), { variant: 'error' }); } else { enqueueSnackbar(LL.UPDATED_OF(LL.ANALOG_SENSOR()), { variant: 'success' }); } } catch (error) { enqueueSnackbar(extractErrorMessage(error, LL.PROBLEM_UPDATING()), { variant: 'error' }); } finally { setAnalog(undefined); fetchSensorData(); } } }; const renderAnalogDialog = () => { if (analog) { return ( setAnalog(undefined)}> {LL.EDIT()} {LL.ANALOG_SENSOR()} {AnalogTypeNames.map((val, i) => ( {val} ))} {analog.t >= AnalogType.COUNTER && analog.t <= AnalogType.RATE && ( <> {DeviceValueUOM_s.map((val, i) => ( {val} ))} {analog.t === AnalogType.ADC && ( mV }} /> )} {analog.t === AnalogType.COUNTER && ( )} )} {analog.t === AnalogType.DIGITAL_OUT && (analog.g === 25 || analog.g === 26) && ( <> )} {analog.t === AnalogType.DIGITAL_OUT && analog.g !== 25 && analog.g !== 26 && ( <> )} {analog.t >= AnalogType.PWM_0 && ( <> Hz }} /> % }} /> )} {LL.WARN_GPIO()} ); } }; return ( {renderCoreData()} {renderDeviceData()} {renderDeviceDialog()} {sensorData.sensors.length !== 0 && renderDallasData()} {sensorData.analogs.length !== 0 && renderAnalogData()} {renderDeviceValueDialog()} {renderSensorDialog()} {renderAnalogDialog()} {device_select.state.id && device_select.state.id !== 'sensor' && ( )} ); }; export default DashboardData;