import { FC, useState, useEffect, useCallback } from 'react'; import { unstable_useBlocker as useBlocker } from 'react-router-dom'; import { Button, Typography, Box, MenuItem, Dialog, DialogActions, DialogContent, DialogTitle, ToggleButton, ToggleButtonGroup, Tooltip, Grid, TextField, Link } from '@mui/material'; import { useTheme } from '@table-library/react-table-library/theme'; import { Table, Header, HeaderRow, HeaderCell, Body, Row, Cell } from '@table-library/react-table-library/table'; import { useSnackbar } from 'notistack'; import WarningIcon from '@mui/icons-material/Warning'; import CancelIcon from '@mui/icons-material/Cancel'; import DoneIcon from '@mui/icons-material/Done'; import SettingsBackupRestoreIcon from '@mui/icons-material/SettingsBackupRestore'; import SearchIcon from '@mui/icons-material/Search'; import FilterListIcon from '@mui/icons-material/FilterList'; import PowerSettingsNewIcon from '@mui/icons-material/PowerSettingsNew'; import OptionIcon from './OptionIcon'; import { ButtonRow, FormLoader, ValidatedTextField, SectionContent, MessageBox, BlockNavigation } from 'components'; import * as EMSESP from './api'; import { extractErrorMessage, updateValue } from 'utils'; import { DeviceShort, Devices, DeviceEntity, DeviceEntityMask } from './types'; import { useI18nContext } from 'i18n/i18n-react'; import RestartMonitor from 'framework/system/RestartMonitor'; export const APIURL = window.location.origin + '/api/'; const SettingsCustomization: FC = () => { const { LL } = useI18nContext(); const { enqueueSnackbar } = useSnackbar(); const emptyDeviceEntity = { id: '', v: 0, n: '', cn: '', m: 0, w: false }; const [numChanges, setNumChanges] = useState(0); const blocker = useBlocker(numChanges !== 0); const [restarting, setRestarting] = useState(false); const [restartNeeded, setRestartNeeded] = useState(false); const [deviceEntities, setDeviceEntities] = useState([emptyDeviceEntity]); const [devices, setDevices] = useState(); const [errorMessage, setErrorMessage] = useState(); const [selectedDevice, setSelectedDevice] = useState(-1); const [confirmReset, setConfirmReset] = useState(false); const [selectedFilters, setSelectedFilters] = useState(0); const [search, setSearch] = useState(''); const [deviceEntity, setDeviceEntity] = useState(); // eslint-disable-next-line const [masks, setMasks] = useState(() => ['']); useEffect(() => { countChanges(); }); const entities_theme = useTheme({ Table: ` --data-table-library_grid-template-columns: 150px repeat(1, minmax(80px, 1fr)) 45px 45px 120px; `, BaseRow: ` font-size: 14px; .td { height: 32px; } `, BaseCell: ` &:nth-of-type(3) { text-align: right; } &:nth-of-type(4) { text-align: right; } &:last-of-type { text-align: right; } `, HeaderRow: ` text-transform: uppercase; background-color: black; color: #90CAF9; .th { border-bottom: 1px solid #565656; font-weight: 500; } &:nth-of-type(1) .th { text-align: center; } `, Row: ` background-color: #1e1e1e; position: relative; cursor: pointer; .td { 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; } &:nth-of-type(odd) .td { background-color: #303030; } `, Cell: ` &:nth-of-type(2) { padding: 8px; } &:nth-of-type(3) { padding-right: 4px; } &:nth-of-type(4) { padding-right: 4px; } &:last-of-type { padding-right: 8px; } ` }); const fetchDevices = useCallback(async () => { try { setDevices((await EMSESP.readDevices()).data); } catch (error) { setErrorMessage(extractErrorMessage(error, LL.PROBLEM_LOADING())); } }, [LL]); const setOriginalSettings = (data: DeviceEntity[]) => { setDeviceEntities(data.map((de) => ({ ...de, o_m: de.m, o_cn: de.cn, o_mi: de.mi, o_ma: de.ma }))); }; const fetchDeviceEntities = async (unique_id: number) => { try { const new_deviceEntities = (await EMSESP.readDeviceEntities({ id: unique_id })).data; setOriginalSettings(new_deviceEntities); } catch (error) { setErrorMessage(extractErrorMessage(error, LL.PROBLEM_LOADING())); } }; 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; } function formatName(de: DeviceEntity) { return ( <> {de.n && (de.n[0] === '!' ? LL.COMMAND() + ': ' + de.n.slice(1) : de.cn && de.cn !== '' ? de.cn : de.n) + ' '}( {de.id} ) ); } const getMaskNumber = (newMask: string[]) => { let new_mask = 0; for (const entry of newMask) { new_mask |= Number(entry); } return new_mask; }; const getMaskString = (m: number) => { let new_masks: string[] = []; 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'); } if ((m & 128) === 128) { new_masks.push('128'); } return new_masks; }; const maskDisabled = (set: boolean) => { setDeviceEntities( deviceEntities.map(function (de) { if ((de.m & selectedFilters || !selectedFilters) && de.id.toLowerCase().includes(search.toLowerCase())) { return { ...de, m: set ? de.m | (DeviceEntityMask.DV_API_MQTT_EXCLUDE | DeviceEntityMask.DV_WEB_EXCLUDE) : de.m & ~(DeviceEntityMask.DV_API_MQTT_EXCLUDE | DeviceEntityMask.DV_WEB_EXCLUDE) }; } else { return de; } }) ); }; const changeSelectedDevice = (event: React.ChangeEvent) => { if (devices) { const selected_device = parseInt(event.target.value, 10); setSelectedDevice(selected_device); fetchDeviceEntities(devices?.devices[selected_device].i); setRestartNeeded(false); } }; const resetCustomization = async () => { try { await EMSESP.resetCustomizations(); enqueueSnackbar(LL.CUSTOMIZATIONS_RESTART(), { variant: 'info' }); } catch (error) { enqueueSnackbar(extractErrorMessage(error, LL.PROBLEM_UPDATING()), { variant: 'error' }); } finally { setConfirmReset(false); } }; function hasEntityChanged(de: DeviceEntity) { return (de?.cn || '') !== (de?.o_cn || '') || de.m !== de.o_m || de.ma !== de.o_ma || de.mi !== de.o_mi; } const getChanges = () => { if (!deviceEntities || selectedDevice === -1) { return []; } return deviceEntities .filter((de) => hasEntityChanged(de)) .map( (new_de) => new_de.m.toString(16).padStart(2, '0') + new_de.id + (new_de.cn || new_de.mi || new_de.ma ? '|' : '') + (new_de.cn ? new_de.cn : '') + (new_de.mi ? '>' + new_de.mi : '') + (new_de.ma ? '<' + new_de.ma : '') ); }; const countChanges = () => { setNumChanges(getChanges().length); }; const restart = async () => { try { await EMSESP.restart(); setRestarting(true); } catch (error) { enqueueSnackbar(extractErrorMessage(error, LL.PROBLEM_UPDATING()), { variant: 'error' }); } }; const saveCustomization = async () => { if (devices && deviceEntities && selectedDevice !== -1) { const masked_entities = getChanges(); // check size in bytes to match buffer in CPP, which is 2048 const bytes = new TextEncoder().encode(JSON.stringify(masked_entities)).length; if (bytes > 2000) { enqueueSnackbar(LL.CUSTOMIZATIONS_FULL(), { variant: 'warning' }); return; } try { const response = await EMSESP.writeCustomEntities({ id: devices?.devices[selectedDevice].i, entity_ids: masked_entities }); if (response.status === 200) { enqueueSnackbar(LL.CUSTOMIZATIONS_SAVED(), { variant: 'success' }); } else if (response.status === 201) { setRestartNeeded(true); } else { enqueueSnackbar(LL.PROBLEM_UPDATING(), { variant: 'error' }); } } catch (error) { enqueueSnackbar(extractErrorMessage(error, LL.PROBLEM_UPDATING()), { variant: 'error' }); } setOriginalSettings(deviceEntities); } }; const renderDeviceList = () => { if (!devices) { return ; } return ( <> {LL.CUSTOMIZATIONS_HELP_1()} ={LL.CUSTOMIZATIONS_HELP_2()}   ={LL.CUSTOMIZATIONS_HELP_3()}   ={LL.CUSTOMIZATIONS_HELP_4()}   ={LL.CUSTOMIZATIONS_HELP_5()}   ={LL.CUSTOMIZATIONS_HELP_6()} {LL.SELECT_DEVICE()}... {devices.devices.map((device: DeviceShort, index) => ( {device.s} ))} ); }; const editEntity = (de: DeviceEntity) => { if (de.n === undefined || (de.n && de.n[0] === '!')) { return; } if (de.cn === undefined) { de.cn = ''; } setDeviceEntity(de); }; const updateEntity = () => { if (deviceEntity) { setDeviceEntities((prevState) => { const newState = prevState.map((obj) => { if (obj.id === deviceEntity.id) { return { ...obj, cn: deviceEntity.cn, mi: deviceEntity.mi, ma: deviceEntity.ma }; } return obj; }); return newState; }); } setDeviceEntity(undefined); }; const renderDeviceData = () => { if (devices?.devices.length === 0 || deviceEntities[0].id === '') { return; } const shown_data = deviceEntities.filter( (de) => (de.m & selectedFilters || !selectedFilters) && de.id.toLowerCase().includes(search.toLowerCase()) ); return ( <> #: {shown_data.length}/{deviceEntities.length} : { setSearch(event.target.value); }} /> : { setSelectedFilters(getMaskNumber(mask)); }} > {(tableList: any) => ( <>
{LL.OPTIONS()} {LL.MIN()} {LL.MAX()} {LL.VALUE(0)}
{tableList.map((de: DeviceEntity) => ( editEntity(de)}> {!deviceEntity && ( { de.m = getMaskNumber(mask); if (de.n === '' && de.m & DeviceEntityMask.DV_READONLY) { de.m = de.m | DeviceEntityMask.DV_WEB_EXCLUDE; } if (de.m & DeviceEntityMask.DV_WEB_EXCLUDE) { de.m = de.m & ~DeviceEntityMask.DV_FAVORITE; } setMasks(['']); }} > = 3}> )} {!deviceEntity && formatName(de)} {!deviceEntity && !(de.m & DeviceEntityMask.DV_READONLY) && formatValue(de.mi)} {!deviceEntity && !(de.m & DeviceEntityMask.DV_READONLY) && formatValue(de.ma)} {!deviceEntity && formatValue(de.v)} ))} )}
); }; const renderResetDialog = () => ( setConfirmReset(false)}> {LL.RESET(1)} {LL.CUSTOMIZATIONS_RESET()} ); const renderContent = () => { return ( <> {LL.DEVICE_ENTITIES()} {renderDeviceList()} {renderDeviceData()} {restartNeeded && ( )} {!restartNeeded && ( {numChanges !== 0 && ( )} )} {renderResetDialog()} ); }; const renderEditDialog = () => { if (deviceEntity) { const de = deviceEntity; return ( setDeviceEntity(undefined)}> {LL.EDIT() + ' ' + LL.ENTITY() + ' "' + de.id + '"'} {LL.DEFAULT(1) + ' ' + LL.NAME(1)}: {deviceEntity.n} {typeof de.v === 'number' && de.w && !(de.m & DeviceEntityMask.DV_READONLY) && ( <> )} ); } }; return ( {blocker ? : null} {restarting ? : renderContent()} {renderEditDialog()} ); }; export default SettingsCustomization;