import { useCallback, useContext, useEffect, useLayoutEffect, useState } from 'react'; import type { FC } from 'react'; import { IconContext } from 'react-icons'; import { useNavigate } from 'react-router-dom'; import { toast } from 'react-toastify'; 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 { Box, Button, Dialog, DialogActions, DialogContent, DialogTitle, Grid, IconButton, List, ListItem, ListItemText, Tooltip, type TooltipProps, Typography, styled, tooltipClasses } from '@mui/material'; import { useRowSelect } from '@table-library/react-table-library/select'; import { SortToggleType, useSort } from '@table-library/react-table-library/sort'; import { Body, Cell, Header, HeaderCell, HeaderRow, Row, Table } from '@table-library/react-table-library/table'; import { useTheme } from '@table-library/react-table-library/theme'; import type { Action, State } from '@table-library/react-table-library/types/common'; import { dialogStyle } from 'CustomTheme'; import { useRequest } from 'alova'; import { ButtonRow, MessageBox, SectionContent, useLayoutTitle } from 'components'; import { AuthenticatedContext } from 'contexts/authentication'; import { useI18nContext } from 'i18n/i18n-react'; import * as EMSESP from './api'; import DeviceIcon from './DeviceIcon'; import DashboardDevicesDialog from './DevicesDialog'; import { formatValue } from './deviceValue'; import { DeviceEntityMask, DeviceType, DeviceValueUOM_s } from './types'; import type { Device, DeviceValue } from './types'; import { deviceValueItemValidation } from './validators'; 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: number) => EMSESP.readDeviceData(id), { initialData: { data: [] }, immediate: false } ); const { loading: submitting, send: writeDeviceValue } = useRequest( (data: { id: number; c: string; v: unknown }) => 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 ButtonTooltip = styled(({ className, ...props }: TooltipProps) => ( ))(({ theme }) => ({ [`& .${tooltipClasses.arrow}`]: { color: theme.palette.success.main }, [`& .${tooltipClasses.tooltip}`]: { backgroundColor: theme.palette.success.main, color: 'rgba(0, 0, 0, 0.87)', boxShadow: theme.shadows[1], fontSize: 10 } })); const getSortIcon = (state: State, sortKey: unknown) => { 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) => // eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-member-access array.sort((a, b) => a.v.toString().localeCompare(b.v.toString())) } } ); async function onSelectChange(action: Action, state: State) { setSelectedDevice(state.id as number); if (action.type === 'ADD_BY_ID_EXCLUSIVELY') { await readDeviceData(state.id as number); } } const device_select = useRowSelect( { nodes: coreData.devices }, { onChange: onSelectChange } ); const resetDeviceSelect = () => { device_select.fns.onRemoveAll(); }; const escFunction = useCallback( (event: KeyboardEvent) => { if (event.key === 'Escape') { 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: string) => { 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(0) }, { 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: string, rowItem: DeviceValue) => csvString + columns .map(({ accessor }: { accessor: (dv: DeviceValue) => unknown }) => escapeCsvCell(accessor(rowItem) as string) ) .join(';') + '\r\n', columns.map(({ name }: { name: string }) => escapeCsvCell(name)).join(';') + '\r\n' ); const downloadBlob = (blob: Blob) => { const downloadLink = document.createElement('a'); downloadLink.download = filename; downloadLink.href = window.URL.createObjectURL(blob); document.body.appendChild(downloadLink); downloadLink.click(); document.body.removeChild(downloadLink); }; const device = { ...{ device: coreData.devices[deviceIndex] }, ...deviceData }; downloadBlob( new Blob([JSON.stringify(device, null, 2)], { type: 'text;charset:utf-8' }) ); downloadBlob(new Blob([csvData], { type: 'text/csv;charset:utf-8' })); }; 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: 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: Device[]) => ( <>
{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: 0, bottom: 0, top: 64, zIndex: 'modal', maxHeight: () => size[1] - 126, 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: DeviceValue[]) => ( <>
{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;