import { FC, useState, useEffect, useCallback } from 'react'; import { Button, Typography, Box, MenuItem, Dialog, DialogActions, DialogContent, DialogTitle, ToggleButton, ToggleButtonGroup, Tooltip, Grid } from '@mui/material'; import { Table } from '@table-library/react-table-library/table'; import { useTheme } from '@table-library/react-table-library/theme'; import { useSort } from '@table-library/react-table-library/sort'; import { Header, HeaderRow, HeaderCell, Body, Row, Cell } from '@table-library/react-table-library/table'; import { useSnackbar } from 'notistack'; import SaveIcon from '@mui/icons-material/Save'; import CancelIcon from '@mui/icons-material/Cancel'; import EditOffOutlinedIcon from '@mui/icons-material/EditOffOutlined'; import StarIcon from '@mui/icons-material/Star'; import VisibilityOffOutlinedIcon from '@mui/icons-material/VisibilityOffOutlined'; import CommentsDisabledOutlinedIcon from '@mui/icons-material/CommentsDisabledOutlined'; import SettingsBackupRestoreIcon from '@mui/icons-material/SettingsBackupRestore'; import KeyboardArrowUpOutlinedIcon from '@mui/icons-material/KeyboardArrowUpOutlined'; import KeyboardArrowDownOutlinedIcon from '@mui/icons-material/KeyboardArrowDownOutlined'; import { ButtonRow, FormLoader, ValidatedTextField, SectionContent } from '../components'; import * as EMSESP from './api'; import { extractErrorMessage } from '../utils'; import { DeviceShort, Devices, DeviceEntity, DeviceEntityMask } from './types'; const SettingsCustomization: FC = () => { const { enqueueSnackbar } = useSnackbar(); const [deviceEntities, setDeviceEntities] = useState([{ id: '', v: 0, s: '', m: 0, w: false }]); const [devices, setDevices] = useState(); const [errorMessage, setErrorMessage] = useState(); const [selectedDevice, setSelectedDevice] = useState(0); const [confirmReset, setConfirmReset] = useState(false); const [selectedFilters, setSelectedFilters] = useState(0); // eslint-disable-next-line const [masks, setMasks] = useState(() => ['']); const entities_theme = useTheme({ Table: ` height: 100%; `, BaseRow: ` font-size: 14px; color: white; height: 32px; `, HeaderRow: ` text-transform: uppercase; background-color: black; border-bottom: 1px solid #e0e0e0; color: #90CAF9; `, Row: ` background-color: #1e1e1e; border-top: 1px solid #565656; border-bottom: 1px solid #565656; position: relative; z-index: 1; &:not(:last-of-type) { margin-bottom: -1px; } &:not(:first-of-type) { margin-top: -1px; } &:hover { z-index: 2; color: white; border-top: 1px solid #177ac9; border-bottom: 1px solid #177ac9; }, &.tr.tr-body.row-select.row-select-single-selected, &.tr.tr-body.row-select.row-select-selected { background-color: #3d4752; color: white; font-weight: normal; z-index: 2; border-top: 1px solid #177ac9; border-bottom: 1px solid #177ac9; } `, BaseCell: ` border-top: 1px solid transparent; border-right: 1px solid transparent; border-bottom: 1px solid transparent; &:nth-of-type(1) { width: 130px; } &:nth-of-type(2) { flex: 1; } &:last-of-type { text-align: right; } ` }); const getSortIcon = (state: any, sortKey: any) => { if (state.sortKey === sortKey && state.reverse) { return ; } if (state.sortKey === sortKey && !state.reverse) { return ; } }; const entity_sort = useSort( { nodes: deviceEntities }, { state: { sortKey: 'NAME', reverse: false } }, { sortIcon: { iconDefault: null, iconUp: , iconDown: }, sortFns: { NAME: (array) => array.sort((a, b) => a.id.localeCompare(b.id)) } } ); const fetchDevices = useCallback(async () => { try { setDevices((await EMSESP.readDevices()).data); } catch (error: any) { setErrorMessage(extractErrorMessage(error, 'Failed to fetch device list')); } }, []); const setInitialMask = (data: DeviceEntity[]) => { setDeviceEntities(data.map((de) => ({ ...de, om: de.m }))); }; const fetchDeviceEntities = async (unique_id: number) => { try { const data = (await EMSESP.readDeviceEntities({ id: unique_id })).data; setInitialMask(data); } catch (error: any) { setErrorMessage(extractErrorMessage(error, 'Problem fetching device entities')); } }; useEffect(() => { fetchDevices(); }, [fetchDevices]); function formatValue(value: any) { if (typeof value === 'number') { return new Intl.NumberFormat().format(value); } else if (value === undefined) { return ''; } else if (typeof value === 'boolean') { return value ? 'true' : 'false'; } return value; } const renderDeviceList = () => { if (!devices) { return ; } function compareDevices(a: DeviceShort, b: DeviceShort) { if (a.s < b.s) { return -1; } if (a.s > b.s) { return 1; } return 0; } const changeSelectedDevice = (event: React.ChangeEvent) => { const selected_device = parseInt(event.target.value, 10); setSelectedDevice(selected_device); fetchDeviceEntities(selected_device); }; return ( <> Select a device and customize each of its entities using the options. Select a device... {devices.devices.sort(compareDevices).map((device: DeviceShort, index) => ( {device.s} ))} ); }; const saveCustomization = async () => { if (deviceEntities && selectedDevice) { const masked_entities = deviceEntities .filter((de) => de.m !== de.om) .map((new_de) => new_de.m.toString(16).padStart(2, '0') + new_de.s); if (masked_entities.length > 50) { enqueueSnackbar( 'Too many selected entities (' + masked_entities.length + '). Limit is 50. Please Save in batches', { variant: 'warning' } ); return; } try { const response = await EMSESP.writeMaskedEntities({ id: selectedDevice, entity_ids: masked_entities }); if (response.status === 200) { enqueueSnackbar('Customization saved', { variant: 'success' }); } else { enqueueSnackbar('Customization save failed', { variant: 'error' }); } } catch (error: any) { enqueueSnackbar(extractErrorMessage(error, 'Problem sending entity list'), { variant: 'error' }); } setInitialMask(deviceEntities); } }; const renderDeviceData = () => { if (devices?.devices.length === 0 || deviceEntities[0].id === '') { return; } const getMaskNumber = (newMask: string[]) => { var new_mask = 0; for (let entry of newMask) { new_mask |= Number(entry); } return new_mask; }; const getMaskString = (m: number) => { var new_masks = []; if ((m & 1) === 1) { new_masks.push('1'); } if ((m & 2) === 2) { new_masks.push('2'); } if ((m & 4) === 4) { new_masks.push('4'); } if ((m & 8) === 8) { new_masks.push('8'); } return new_masks; }; const maskDisabled = (set: boolean) => { setDeviceEntities( deviceEntities .filter((de) => de.m & selectedFilters || !selectedFilters) .map(({ m, ...entities }) => ({ ...entities, m: set ? m | (DeviceEntityMask.DV_API_MQTT_EXCLUDE | DeviceEntityMask.DV_WEB_EXCLUDE) : m & ~(DeviceEntityMask.DV_API_MQTT_EXCLUDE | DeviceEntityMask.DV_WEB_EXCLUDE) })) ); }; return ( <> Apply filter:  { setSelectedFilters(getMaskNumber(mask)); }} > de.m & selectedFilters || !selectedFilters) }} theme={entities_theme} sort={entity_sort} layout={{ custom: true }} > {(tableList: any) => ( <>
OPTIONS VALUE
{tableList.map((de: DeviceEntity) => ( { de.m = getMaskNumber(mask); setMasks(['']); }} > {de.id} ({de.s}) {formatValue(de.v)} ))} )}
); }; const resetCustomization = async () => { try { await EMSESP.resetCustomizations(); enqueueSnackbar('All customizations have been removed. Restarting...', { variant: 'info' }); } catch (error: any) { enqueueSnackbar(extractErrorMessage(error, 'Problem resetting customizations'), { variant: 'error' }); } finally { setConfirmReset(false); } }; const renderResetDialog = () => ( setConfirmReset(false)}> Reset Are you sure you want remove all customizations including the custom settings of the Temperature and Analog sensors? ); const content = () => { return ( <> Device Entities {renderDeviceList()} {renderDeviceData()} {renderResetDialog()} ); }; return ( {content()} ); }; export default SettingsCustomization;