import CommentsDisabledOutlinedIcon from '@mui/icons-material/CommentsDisabledOutlined'; import EditIcon from '@mui/icons-material/Edit'; import EditOffOutlinedIcon from '@mui/icons-material/EditOffOutlined'; import FormatListNumberedIcon from '@mui/icons-material/FormatListNumbered'; import DownloadIcon from '@mui/icons-material/GetApp'; import HighlightOffIcon from '@mui/icons-material/HighlightOff'; import InfoOutlinedIcon from '@mui/icons-material/InfoOutlined'; import KeyboardArrowDownOutlinedIcon from '@mui/icons-material/KeyboardArrowDownOutlined'; import KeyboardArrowUpOutlinedIcon from '@mui/icons-material/KeyboardArrowUpOutlined'; import PlayArrowIcon from '@mui/icons-material/PlayArrow'; import RefreshIcon from '@mui/icons-material/Refresh'; import StarIcon from '@mui/icons-material/Star'; import StarBorderOutlinedIcon from '@mui/icons-material/StarBorderOutlined'; import UnfoldMoreOutlinedIcon from '@mui/icons-material/UnfoldMoreOutlined'; import { Button, Dialog, DialogTitle, DialogContent, DialogActions, IconButton, List, ListItem, ListItemText, Box, Grid, Typography } from '@mui/material'; import { useRowSelect } from '@table-library/react-table-library/select'; import { useSort, SortToggleType } from '@table-library/react-table-library/sort'; import { Table, Header, HeaderRow, HeaderCell, Body, Row, Cell } from '@table-library/react-table-library/table'; import { useTheme } from '@table-library/react-table-library/theme'; import { useRequest } from 'alova'; import { useState, useEffect, useCallback, useLayoutEffect, useContext } from 'react'; import { IconContext } from 'react-icons'; import { useNavigate } from 'react-router-dom'; import { toast } from 'react-toastify'; import DeviceIcon from './DeviceIcon'; import DashboardDevicesDialog from './DevicesDialog'; import * as EMSESP from './api'; import { formatValue } from './deviceValue'; import { DeviceValueUOM_s, DeviceEntityMask, DeviceType } from './types'; import { deviceValueItemValidation } from './validators'; import type { Device, DeviceValue } from './types'; import type { FC } from 'react'; import { dialogStyle } from 'CustomTheme'; import { ButtonRow, SectionContent, MessageBox, useLayoutTitle } from 'components'; import { AuthenticatedContext } from 'contexts/authentication'; import { useI18nContext } from 'i18n/i18n-react'; const Devices: FC = () => { const { LL } = useI18nContext(); const { me } = useContext(AuthenticatedContext); const [size, setSize] = useState([0, 0]); const [selectedDeviceValue, setSelectedDeviceValue] = useState(); const [onlyFav, setOnlyFav] = useState(false); const [deviceValueDialogOpen, setDeviceValueDialogOpen] = useState(false); const [showDeviceInfo, setShowDeviceInfo] = useState(false); const [selectedDevice, setSelectedDevice] = useState(); const navigate = useNavigate(); useLayoutTitle(LL.DEVICES()); const { data: coreData, send: readCoreData } = useRequest(() => EMSESP.readCoreData(), { initialData: { connected: true, devices: [] } }); const { data: deviceData, send: readDeviceData } = useRequest((id) => EMSESP.readDeviceData(id), { initialData: { data: [] }, immediate: false }); const { loading: submitting, send: writeDeviceValue } = useRequest((data) => EMSESP.writeDeviceValue(data), { immediate: false }); useLayoutEffect(() => { function updateSize() { setSize([window.innerWidth, window.innerHeight]); } window.addEventListener('resize', updateSize); updateSize(); return () => window.removeEventListener('resize', updateSize); }, []); const leftOffset = () => { const devicesWindow = document.getElementById('devices-window'); if (!devicesWindow) { return 0; } const clientRect = devicesWindow.getBoundingClientRect(); const left = clientRect.left; const right = clientRect.right; if (!left || !right) { return 0; } return left + (right - left < 400 ? 0 : 200); }; 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; font-weight: normal; } &:hover .td { border-top: 1px solid #177ac9; border-bottom: 1px solid #177ac9; } ` }); const device_theme = useTheme([ common_theme, { Table: ` --data-table-library_grid-template-columns: 40px repeat(1, minmax(0, 1fr)) 130px; `, BaseRow: ` .td { height: 42px; } `, BaseCell: ` &:nth-of-type(2) { text-align: left; }, &:nth-of-type(4) { text-align: center; } `, HeaderRow: ` .th { padding: 8px; height: 36px; ` } ]); const data_theme = useTheme([ common_theme, { Table: ` --data-table-library_grid-template-columns: minmax(200px, auto) minmax(150px, auto) 40px; height: auto; max-height: 100%; overflow-y: scroll; ::-webkit-scrollbar { display:none; } `, BaseRow: ` .td { height: 32px; } `, BaseCell: ` &:nth-of-type(1) { border-left: 1px solid #177ac9; }, &:nth-of-type(2) { text-align: right; }, &:nth-of-type(3) { border-right: 1px solid #177ac9; } `, HeaderRow: ` .th { border-top: 1px solid #565656; } `, Row: ` &:nth-of-type(odd) .td { background-color: #303030; } ` } ]); const getSortIcon = (state: any, sortKey: any) => { if (state.sortKey === sortKey && state.reverse) { return ; } if (state.sortKey === sortKey && !state.reverse) { return ; } return ; }; const dv_sort = useSort( { nodes: deviceData.data }, {}, { sortIcon: { iconDefault: , iconUp: , iconDown: }, sortToggleType: SortToggleType.AlternateWithReset, sortFns: { NAME: (array) => array.sort((a, b) => a.id.toString().slice(2).localeCompare(b.id.toString().slice(2))), VALUE: (array) => array.sort((a, b) => a.v.toString().localeCompare(b.v.toString())) } } ); async function onSelectChange(action: any, state: any) { setSelectedDevice(state.id); if (action.type === 'ADD_BY_ID_EXCLUSIVELY') { await readDeviceData(state.id); } } const device_select = useRowSelect( { nodes: coreData.devices }, { onChange: onSelectChange } ); const resetDeviceSelect = () => { device_select.fns.onRemoveAll(); }; const escFunction = useCallback( (event: any) => { if (event.keyCode === 27) { if (device_select) { device_select.fns.onRemoveAll(); } } }, [device_select] ); useEffect(() => { document.addEventListener('keydown', escFunction); return () => { document.removeEventListener('keydown', escFunction); }; }, [escFunction]); const refreshData = () => { if (!deviceValueDialogOpen) { selectedDevice ? void readDeviceData(selectedDevice) : void readCoreData(); } }; const customize = () => { if (selectedDevice == 99) { navigate('/customentities'); } else { navigate('/customizations', { state: selectedDevice }); } }; 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 hasMask = (id: string, mask: number) => (parseInt(id.slice(0, 2), 16) & mask) === mask; const handleDownloadCsv = () => { const deviceIndex = coreData.devices.findIndex((d) => d.id === device_select.state.id); if (deviceIndex === -1) { return; } const filename = coreData.devices[deviceIndex].tn + '_' + coreData.devices[deviceIndex].n; const columns = [ { accessor: (dv: DeviceValue) => dv.id.slice(2), name: LL.ENTITY_NAME(0) }, { accessor: (dv: DeviceValue) => (typeof dv.v === 'number' ? new Intl.NumberFormat().format(dv.v) : dv.v), name: LL.VALUE(1) }, { accessor: (dv: DeviceValue) => DeviceValueUOM_s[dv.u].replace(/[^a-zA-Z0-9]/g, ''), name: 'UoM' }, { accessor: (dv: DeviceValue) => (dv.c && !hasMask(dv.id, DeviceEntityMask.DV_READONLY) ? 'yes' : 'no'), name: LL.WRITEABLE() }, { accessor: (dv: DeviceValue) => dv.h ? dv.h : dv.l ? dv.l.join(' | ') : dv.m !== undefined && dv.x !== undefined ? dv.m + ', ' + dv.x : '', name: 'Range' } ]; const data = onlyFav ? deviceData.data.filter((dv) => hasMask(dv.id, DeviceEntityMask.DV_FAVORITE)) : deviceData.data; const csvData = data.reduce( (csvString: any, rowItem: any) => csvString + columns.map(({ accessor }: any) => escapeCsvCell(accessor(rowItem))).join(';') + '\r\n', columns.map(({ name }: any) => escapeCsvCell(name)).join(';') + '\r\n' ); 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); }; useEffect(() => { const timer = setInterval(() => refreshData(), 60000); return () => { clearInterval(timer); }; }); const deviceValueDialogSave = async (devicevalue: DeviceValue) => { const id = Number(device_select.state.id); await writeDeviceValue({ id, c: devicevalue.c, v: devicevalue.v }) .then(() => { toast.success(LL.WRITE_CMD_SENT()); }) .catch((error) => { toast.error(error.message); }) .finally(async () => { setDeviceValueDialogOpen(false); await readDeviceData(id); setSelectedDeviceValue(undefined); }); }; const renderDeviceDetails = () => { if (showDeviceInfo) { const deviceIndex = coreData.devices.findIndex((d) => d.id === device_select.state.id); if (deviceIndex === -1) { return; } return ( setShowDeviceInfo(false)}> {LL.DEVICE_DETAILS()} {coreData.devices[deviceIndex].t !== DeviceType.CUSTOM && ( <> )} ); } }; const renderCoreData = () => ( {!coreData.connected && } {coreData.connected && ( {(tableList: any) => ( <>
{LL.DESCRIPTION()} {LL.TYPE(0)}
{tableList.map((device: Device) => ( {device.n}   ({device.e}) {device.tn} ))} )}
)}
); const deviceValueDialogClose = () => { setDeviceValueDialogOpen(false); }; const renderDeviceData = () => { if (!selectedDevice) { return; } const showDeviceValue = (dv: DeviceValue) => { setSelectedDeviceValue(dv); setDeviceValueDialogOpen(true); }; 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) && ( )} ); const shown_data = onlyFav ? deviceData.data.filter((dv) => hasMask(dv.id, DeviceEntityMask.DV_FAVORITE)) : deviceData.data; const deviceIndex = coreData.devices.findIndex((d) => d.id === device_select.state.id); if (deviceIndex === -1) { return; } return ( leftOffset(), right: 16, bottom: 0, top: 128, zIndex: 'modal', maxHeight: () => size[1] - 189, border: '1px solid #177ac9' }} > {coreData.devices[deviceIndex].tn} | {coreData.devices[deviceIndex].n} {LL.SHOWING() + ' ' + shown_data.length + '/' + coreData.devices[deviceIndex].e + ' ' + LL.ENTITIES(shown_data.length)} setShowDeviceInfo(true)}> {me.admin && ( )} setOnlyFav(!onlyFav)}> {onlyFav ? ( ) : ( )} {(tableList: any) => ( <>
{tableList.map((dv: DeviceValue) => ( showDeviceValue(dv)}> {renderNameCell(dv)} {formatValue(LL, dv.v, dv.u)} {me.admin && dv.c && !hasMask(dv.id, DeviceEntityMask.DV_READONLY) && ( showDeviceValue(dv)}> {dv.v === '' && dv.c ? ( ) : ( )} )} ))} )}
); }; return ( {renderCoreData()} {renderDeviceData()} {renderDeviceDetails()} {selectedDeviceValue && ( )} ); }; export default Devices;