import CancelIcon from '@mui/icons-material/Cancel'; import PowerSettingsNewIcon from '@mui/icons-material/PowerSettingsNew'; import SearchIcon from '@mui/icons-material/Search'; import SettingsBackupRestoreIcon from '@mui/icons-material/SettingsBackupRestore'; import WarningIcon from '@mui/icons-material/Warning'; import { Button, Typography, Box, MenuItem, Dialog, DialogActions, DialogContent, DialogTitle, ToggleButton, ToggleButtonGroup, Grid, TextField, Link, InputAdornment } from '@mui/material'; 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 } from 'react'; import { unstable_useBlocker as useBlocker } from 'react-router-dom'; import { toast } from 'react-toastify'; import EntityMaskToggle from './EntityMaskToggle'; import OptionIcon from './OptionIcon'; import SettingsCustomizationDialog from './SettingsCustomizationDialog'; import * as EMSESP from './api'; import { DeviceEntityMask } from './types'; import type { DeviceShort, DeviceEntity } from './types'; import type { FC } from 'react'; import { ButtonRow, SectionContent, MessageBox, BlockNavigation } from 'components'; import RestartMonitor from 'framework/system/RestartMonitor'; import { useI18nContext } from 'i18n/i18n-react'; export const APIURL = window.location.origin + '/api/'; const SettingsCustomization: FC = () => { const { LL } = useI18nContext(); const [numChanges, setNumChanges] = useState(0); const blocker = useBlocker(numChanges !== 0); const [restarting, setRestarting] = useState(false); const [restartNeeded, setRestartNeeded] = useState(false); const [deviceEntities, setDeviceEntities] = useState([]); const [selectedDevice, setSelectedDevice] = useState(-1); const [confirmReset, setConfirmReset] = useState(false); const [selectedFilters, setSelectedFilters] = useState(0); const [search, setSearch] = useState(''); const [selectedDeviceEntity, setSelectedDeviceEntity] = useState(); const [dialogOpen, setDialogOpen] = useState(false); const { send: resetCustomizations } = useRequest(EMSESP.resetCustomizations(), { immediate: false }); const { data: devices } = useRequest(EMSESP.readDevices()); const { send: writeCustomEntities } = useRequest((data) => EMSESP.writeCustomEntities(data), { immediate: false }); const { send: readDeviceEntities, update: updateDeviceEntities, onSuccess: onSuccess } = useRequest((data) => EMSESP.readDeviceEntities(data), { initialData: [], immediate: false, force: true }); 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 }))); }; onSuccess((event) => { setOriginalSettings(event.data); }); const { send: restartCommand } = useRequest(EMSESP.restart(), { immediate: false }); const entities_theme = useTheme({ Table: ` --data-table-library_grid-template-columns: 150px repeat(1, minmax(80px, 1fr)) 45px minmax(45px, auto) minmax(120px, auto); `, 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; height: 36px; } &: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; } &: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; } ` }); 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; } useEffect(() => { if (deviceEntities.length) { setNumChanges( 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 : '') ).length ); } }, [deviceEntities]); const restart = async () => { await restartCommand().catch((error) => { toast.error(error.message); }); setRestarting(true); }; 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(1) + ': ' + 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) => { const 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) => { updateDeviceEntities( 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); setNumChanges(0); void readDeviceEntities(devices?.devices[selected_device].i); setRestartNeeded(false); } }; const resetCustomization = async () => { try { await resetCustomizations(); toast.info(LL.CUSTOMIZATIONS_RESTART()); } catch (error) { toast.error(error.message); } finally { setConfirmReset(false); } }; const onDialogClose = () => { setDialogOpen(false); }; const updateDeviceEntity = (updatedItem: DeviceEntity) => { setDeviceEntities(deviceEntities?.map((de) => (de.id === updatedItem.id ? { ...de, ...updatedItem } : de))); }; const onDialogSave = (updatedItem: DeviceEntity) => { setDialogOpen(false); updateDeviceEntity(updatedItem); }; const editDeviceEntity = useCallback((de: DeviceEntity) => { if (de.n === undefined || (de.n && de.n[0] === '!')) { return; } if (de.cn === undefined) { de.cn = ''; } setSelectedDeviceEntity(de); setDialogOpen(true); }, []); const saveCustomization = async () => { if (devices && deviceEntities && selectedDevice !== -1) { const masked_entities = deviceEntities .filter((de: DeviceEntity) => 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 : '') ); // 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) { toast.warning(LL.CUSTOMIZATIONS_FULL()); return; } await writeCustomEntities({ id: devices?.devices[selectedDevice].i, entity_ids: masked_entities }).catch( (error) => { if (error.message === 'Reboot required') { setRestartNeeded(true); } else { toast.error(error.message); } } ); // does this work or use onSuccess hook? setOriginalSettings(deviceEntities); } }; const renderDeviceList = () => ( <> {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 renderDeviceData = () => { if (deviceEntities.length === 0) { return; } const shown_data = deviceEntities.filter( (de) => (de.m & selectedFilters || !selectedFilters) && de.id.toLowerCase().includes(search.toLowerCase()) ); return ( <> { setSearch(event.target.value); }} InputProps={{ startAdornment: ( ) }} /> { setSelectedFilters(getMaskNumber(mask)); }} > {LL.SHOWING()} {shown_data.length}/{deviceEntities.length} {(tableList: any) => ( <>
{LL.OPTIONS()} {LL.NAME(1)} {LL.MIN()} {LL.MAX()} {LL.VALUE(0)}
{tableList.map((de: DeviceEntity) => ( editDeviceEntity(de)}> {formatName(de)} {!(de.m & DeviceEntityMask.DV_READONLY) && formatValue(de.mi)} {!(de.m & DeviceEntityMask.DV_READONLY) && formatValue(de.ma)} {formatValue(de.v)} ))} )}
); }; const renderResetDialog = () => ( setConfirmReset(false)}> {LL.RESET(1)} {LL.CUSTOMIZATIONS_RESET()} ); const renderContent = () => ( <> {LL.DEVICE_ENTITIES()} {devices && renderDeviceList()} {renderDeviceData()} {restartNeeded && ( )} {!restartNeeded && ( {numChanges !== 0 && ( )} )} {renderResetDialog()} ); return ( {blocker ? : null} {restarting ? : renderContent()} {selectedDeviceEntity && ( )} ); }; export default SettingsCustomization;