import { FC, useState, useContext, useEffect } from 'react'; import { Button, Table, TableBody, TableHead, TableRow, Typography, Box, Dialog, DialogTitle, DialogContent, DialogActions, MenuItem, InputAdornment, FormHelperText, IconButton, List, ListItem, ListItemText, Grid, useMediaQuery } from '@mui/material'; import TableCell, { tableCellClasses } from '@mui/material/TableCell'; import { styled } from '@mui/material/styles'; import parseMilliseconds from 'parse-ms'; import { useSnackbar } from 'notistack'; 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 DeviceIcon from './DeviceIcon'; import { IconContext } from 'react-icons'; import { AuthenticatedContext } from '../contexts/authentication'; import { ButtonRow, FormLoader, ValidatedTextField, SectionContent, MessageBox } from '../components'; import * as EMSESP from './api'; import { numberValue, updateValue, extractErrorMessage, useRest } from '../utils'; import { SensorData, CoreData, DeviceData, DeviceValue, DeviceValueUOM, DeviceValueUOM_s, AnalogTypes, Sensor, Analog } from './types'; const StyledTableCell = styled(TableCell)(({ theme }) => ({ [`&.${tableCellClasses.head}`]: { backgroundColor: theme.palette.common.black, color: theme.palette.common.white, fontSize: 14 } })); const StyledTableRow = styled(TableRow)(({ theme }) => ({ '&:nth-of-type(odd)': { backgroundColor: theme.palette.action.hover }, '&:hover': { backgroundColor: theme.palette.info.light } })); const DashboardData: FC = () => { const { loadData, data, errorMessage } = useRest({ read: EMSESP.readCoreData }); const { me } = useContext(AuthenticatedContext); const { enqueueSnackbar } = useSnackbar(); const [deviceData, setDeviceData] = useState(); const [sensorData, setSensorData] = useState(); const [deviceValue, setDeviceValue] = useState(); const [sensor, setSensor] = useState(); const [analog, setAnalog] = useState(); const [selectedDevice, setSelectedDevice] = useState(); const [deviceDialog, setDeviceDialog] = useState(-1); const desktopWindow = useMediaQuery('(min-width:600px)'); const refreshAllData = () => { if (analog || sensor || deviceValue) { return; } loadData(); if (sensorData) { fetchSensorData(); } else if (selectedDevice) { fetchDeviceData(selectedDevice); } }; const refreshData = () => { if (analog || sensor || deviceValue) { return; } if (sensorData) { fetchSensorData(); } else if (selectedDevice) { fetchDeviceData(selectedDevice); } else { loadData(); } }; useEffect(() => { const timer = setInterval(() => refreshData(), 60000); return () => { clearInterval(timer); }; // eslint-disable-next-line }, [analog, sensor, deviceValue, sensorData, selectedDevice]); const fetchDeviceData = async (unique_id: number) => { try { setDeviceData((await EMSESP.readDeviceData({ id: unique_id })).data); } catch (error: any) { enqueueSnackbar(extractErrorMessage(error, 'Problem fetching device data'), { variant: 'error' }); } finally { setSelectedDevice(unique_id); setSensorData(undefined); } }; const fetchSensorData = async () => { try { setSensorData((await EMSESP.readSensorData()).data); } catch (error: any) { enqueueSnackbar(extractErrorMessage(error, 'Problem fetching sensor data'), { variant: 'error' }); } finally { setSelectedDevice(undefined); } }; const pluralize = (count: number, noun: string, suffix = 's') => ` ${Intl.NumberFormat().format(count)} ${noun}${count !== 1 ? suffix : ''} `; const formatDuration = (duration_min: number) => { const { days, hours, minutes } = parseMilliseconds(duration_min * 60000); let formatted = ''; if (days) { formatted += pluralize(days, 'day'); } if (hours) { formatted += pluralize(hours, 'hour'); } if (minutes) { formatted += pluralize(minutes, 'minute'); } return formatted; }; function formatValue(value: any, uom: number) { if (value === undefined) { return ''; } switch (uom) { case DeviceValueUOM.HOURS: return value ? formatDuration(value * 60) : '0 hours'; case DeviceValueUOM.MINUTES: return value ? formatDuration(value) : '0 minutes'; 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] ); case DeviceValueUOM.SECONDS: return pluralize(value, DeviceValueUOM_s[uom]); default: return new Intl.NumberFormat().format(value) + ' ' + DeviceValueUOM_s[uom]; } } const sendDeviceValue = async () => { if (selectedDevice && deviceValue) { try { const response = await EMSESP.writeValue({ id: selectedDevice, devicevalue: deviceValue }); if (response.status === 204) { enqueueSnackbar('Write command failed', { variant: 'error' }); } else if (response.status === 403) { enqueueSnackbar('Write access denied', { variant: 'error' }); } else { enqueueSnackbar('Write command sent', { variant: 'success' }); } setDeviceValue(undefined); } catch (error: any) { enqueueSnackbar(extractErrorMessage(error, 'Problem writing value'), { variant: 'error' }); } finally { setDeviceValue(undefined); fetchDeviceData(selectedDevice); loadData(); } } }; const renderDeviceValueDialog = () => { if (deviceValue) { return ( setDeviceValue(undefined)}> Change Value {deviceValue.l && ( {deviceValue.l.map((val) => ( {val} ))} )} {!deviceValue.l && ( {DeviceValueUOM_s[deviceValue.u]} }} /> )} {deviceValue.h && {deviceValue.h}} ); } }; const addAnalogSensor = () => { setAnalog({ i: 0, n: '', u: 0, v: 0, o: 0, t: 0, f: 1 }); }; const sendSensor = async () => { if (sensor) { try { const response = await EMSESP.writeSensor({ id_str: sensor.is, name: sensor.n, offset: sensor.o }); if (response.status === 204) { enqueueSnackbar('Sensor change failed', { variant: 'error' }); } else if (response.status === 403) { enqueueSnackbar('Access denied', { variant: 'error' }); } else { enqueueSnackbar('Sensor updated', { variant: 'success' }); } setSensor(undefined); } catch (error: any) { enqueueSnackbar(extractErrorMessage(error, 'Problem updating sensor'), { variant: 'error' }); } finally { setSensor(undefined); fetchSensorData(); } } }; const renderSensorDialog = () => { if (sensor) { return ( setSensor(undefined)}> Edit Temperature Sensor Sensor ID {sensor.is} °C }} /> ); } }; const renderDeviceDialog = () => { if (data && data.devices.length > 0 && deviceDialog !== -1) { return ( setDeviceDialog(-1)}> Device Details ); } }; const toggleDeviceData = (index: number) => { loadData(); if (selectedDevice === index) { setSelectedDevice(undefined); } else { fetchDeviceData(index); } }; const toggleSensorData = () => { loadData(); if (sensorData) { setSensorData(undefined); } else { fetchSensorData(); } }; const renderCoreData = () => { if (!data) { return ; } return ( {data.devices.length === 0 && } TYPE {desktopWindow && DESCRIPTION} ENTITIES {data.devices.map((device, index) => ( device.e && toggleDeviceData(device.i)} > {device.t} {desktopWindow && {device.n}} {device.e} setDeviceDialog(index)}> ))} {(data.active_sensors > 0 || data.analog_enabled) && ( toggleSensorData()}> Sensors {desktopWindow && Attached EMS-ESP Sensors} {data.active_sensors} addAnalogSensor()} disabled={!data.analog_enabled}> )}
); }; const renderDeviceData = () => { if (data?.devices.length === 0 || !deviceData || !selectedDevice) { return; } const sendCommand = (dv: DeviceValue) => { if (dv.c && me.admin) { setDeviceValue(dv); } }; const renderNameCell = (dv: DeviceValue) => { if (dv.v === undefined && dv.c) { return ( command: {dv.n} ); } return ( {dv.n} ); }; return ( <> {deviceData.label} ENTITY NAME/COMMAND VALUE {deviceData.data.map((dv, i) => ( sendCommand(dv)}> {dv.c && me.admin && ( )} {renderNameCell(dv)} {formatValue(dv.v, dv.u)} ))}
); }; const updateSensor = (sensordata: Sensor) => { if (sensordata && me.admin) { setSensor(sensordata); } }; const updateAnalog = (analogdata: Analog) => { if (me.admin) { setAnalog(analogdata); } }; const renderDallasData = () => ( <> Temperature Sensors NAME TEMPERATURE {sensorData?.sensors.map((sensor_data) => ( updateSensor(sensor_data)}> {me.admin && ( )} {sensor_data.n} {formatValue(sensor_data.t, sensor_data.u)} ))}
); const renderAnalogData = () => ( <> Analog Sensors GPIO NAME TYPE VALUE {sensorData?.analogs.map((analog_data) => ( updateAnalog(analog_data)}> {me.admin && ( )} {analog_data.i} {analog_data.n} {AnalogTypes[analog_data.t]} {formatValue(analog_data.v, analog_data.u)} ))}
); const sendRemoveAnalog = async () => { if (analog) { try { const response = await EMSESP.writeAnalog({ id: analog.i, name: analog.n, offset: analog.o, factor: analog.f, uom: analog.u, type: -1 }); if (response.status === 204) { enqueueSnackbar('Analog deletion failed', { variant: 'error' }); } else if (response.status === 403) { enqueueSnackbar('Access denied', { variant: 'error' }); } else { enqueueSnackbar('Analog sensor removed', { variant: 'success' }); } } catch (error: any) { enqueueSnackbar(extractErrorMessage(error, 'Problem updating analog sensor'), { variant: 'error' }); } finally { setAnalog(undefined); fetchSensorData(); } } }; const sendAnalog = async () => { if (analog) { try { const response = await EMSESP.writeAnalog({ id: analog.i, name: analog.n, offset: analog.o, factor: analog.f, uom: analog.u, type: analog.t }); if (response.status === 204) { enqueueSnackbar('Analog sensor update failed', { variant: 'error' }); } else if (response.status === 403) { enqueueSnackbar('Access denied', { variant: 'error' }); } else { enqueueSnackbar('Analog sensor updated', { variant: 'success' }); } } catch (error: any) { enqueueSnackbar(extractErrorMessage(error, 'Problem updating analog'), { variant: 'error' }); } finally { setAnalog(undefined); fetchSensorData(); } } }; const renderAnalogDialog = () => { if (analog) { return ( setAnalog(undefined)}> Edit Analog Sensor {AnalogTypes.map((val, i) => ( {val} ))} {analog.t === 3 && ( <> {DeviceValueUOM_s.map((val, i) => ( {val} ))} mV }} /> )} Warning: be careful when assigning a GPIO! ); } }; const content = () => { if (!data) { return ; } return ( <> {renderCoreData()} {renderDeviceData()} {renderDeviceDialog()} {sensorData && sensorData.sensors.length > 0 && renderDallasData()} {sensorData && sensorData.analogs.length > 0 && renderAnalogData()} {renderDeviceValueDialog()} {renderSensorDialog()} {renderAnalogDialog()} ); }; return ( {content()} ); }; export default DashboardData;