formatting

This commit is contained in:
proddy
2024-04-21 15:10:22 +02:00
parent befa487482
commit ac39a46442
100 changed files with 2778 additions and 798 deletions

View File

@@ -5,7 +5,17 @@ import { toast } from 'react-toastify';
import CancelIcon from '@mui/icons-material/Cancel';
import PowerSettingsNewIcon from '@mui/icons-material/PowerSettingsNew';
import WarningIcon from '@mui/icons-material/Warning';
import { Box, Button, Checkbox, Divider, Grid, InputAdornment, MenuItem, TextField, Typography } from '@mui/material';
import {
Box,
Button,
Checkbox,
Divider,
Grid,
InputAdornment,
MenuItem,
TextField,
Typography
} from '@mui/material';
import * as SystemApi from 'api/system';
@@ -61,7 +71,12 @@ const ApplicationSettings: FC = () => {
const { LL } = useI18nContext();
const updateFormValue = updateValueDirty(origData, dirtyFlags, setDirtyFlags, updateDataValue);
const updateFormValue = updateValueDirty(
origData,
dirtyFlags,
setDirtyFlags,
updateDataValue
);
const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>();
@@ -220,7 +235,9 @@ const ApplicationSettings: FC = () => {
<ValidatedTextField
fieldErrors={fieldErrors}
name="dallas_gpio"
label={LL.GPIO_OF(LL.TEMPERATURE()) + ' (0=' + LL.DISABLED(1) + ')'}
label={
LL.GPIO_OF(LL.TEMPERATURE()) + ' (0=' + LL.DISABLED(1) + ')'
}
fullWidth
variant="outlined"
value={numberValue(data.dallas_gpio)}
@@ -322,7 +339,13 @@ const ApplicationSettings: FC = () => {
<Typography sx={{ pt: 2 }} variant="h6" color="primary">
{LL.SETTINGS_OF(LL.EMS_BUS(0))}
</Typography>
<Grid container spacing={1} direction="row" justifyContent="flex-start" alignItems="flex-start">
<Grid
container
spacing={1}
direction="row"
justifyContent="flex-start"
alignItems="flex-start"
>
<Grid item xs={12} sm={6}>
<TextField
name="tx_mode"
@@ -396,54 +419,120 @@ const ApplicationSettings: FC = () => {
</Grid>
{data.led_gpio !== 0 && (
<BlockFormControlLabel
control={<Checkbox checked={data.hide_led} onChange={updateFormValue} name="hide_led" />}
control={
<Checkbox
checked={data.hide_led}
onChange={updateFormValue}
name="hide_led"
/>
}
label={LL.HIDE_LED()}
disabled={saving}
/>
)}
<BlockFormControlLabel
control={<Checkbox checked={data.telnet_enabled} onChange={updateFormValue} name="telnet_enabled" />}
control={
<Checkbox
checked={data.telnet_enabled}
onChange={updateFormValue}
name="telnet_enabled"
/>
}
label={LL.ENABLE_TELNET()}
disabled={saving}
/>
<BlockFormControlLabel
control={<Checkbox checked={data.analog_enabled} onChange={updateFormValue} name="analog_enabled" />}
control={
<Checkbox
checked={data.analog_enabled}
onChange={updateFormValue}
name="analog_enabled"
/>
}
label={LL.ENABLE_ANALOG()}
disabled={saving}
/>
<BlockFormControlLabel
control={<Checkbox checked={data.fahrenheit} onChange={updateFormValue} name="fahrenheit" />}
control={
<Checkbox
checked={data.fahrenheit}
onChange={updateFormValue}
name="fahrenheit"
/>
}
label={LL.CONVERT_FAHRENHEIT()}
disabled={saving}
/>
<BlockFormControlLabel
control={<Checkbox checked={data.notoken_api} onChange={updateFormValue} name="notoken_api" />}
control={
<Checkbox
checked={data.notoken_api}
onChange={updateFormValue}
name="notoken_api"
/>
}
label={LL.BYPASS_TOKEN()}
disabled={saving}
/>
<BlockFormControlLabel
control={<Checkbox checked={data.readonly_mode} onChange={updateFormValue} name="readonly_mode" />}
control={
<Checkbox
checked={data.readonly_mode}
onChange={updateFormValue}
name="readonly_mode"
/>
}
label={LL.READONLY()}
disabled={saving}
/>
<BlockFormControlLabel
control={<Checkbox checked={data.low_clock} onChange={updateFormValue} name="low_clock" />}
control={
<Checkbox
checked={data.low_clock}
onChange={updateFormValue}
name="low_clock"
/>
}
label={LL.UNDERCLOCK_CPU()}
disabled={saving}
/>
<BlockFormControlLabel
control={<Checkbox checked={data.boiler_heatingoff} onChange={updateFormValue} name="boiler_heatingoff" />}
control={
<Checkbox
checked={data.boiler_heatingoff}
onChange={updateFormValue}
name="boiler_heatingoff"
/>
}
label={LL.HEATINGOFF()}
disabled={saving}
/>
<Grid container spacing={0} direction="row" justifyContent="flex-start" alignItems="flex-start">
<Grid
container
spacing={0}
direction="row"
justifyContent="flex-start"
alignItems="flex-start"
>
<BlockFormControlLabel
control={<Checkbox checked={data.shower_timer} onChange={updateFormValue} name="shower_timer" />}
control={
<Checkbox
checked={data.shower_timer}
onChange={updateFormValue}
name="shower_timer"
/>
}
label={LL.ENABLE_SHOWER_TIMER()}
disabled={saving}
/>
<BlockFormControlLabel
control={<Checkbox checked={data.shower_alert} onChange={updateFormValue} name="shower_alert" />}
control={
<Checkbox
checked={data.shower_alert}
onChange={updateFormValue}
name="shower_alert"
/>
}
label={LL.ENABLE_SHOWER_ALERT()}
disabled={!data.shower_timer}
/>
@@ -465,7 +554,9 @@ const ApplicationSettings: FC = () => {
name="shower_alert_trigger"
label={LL.TRIGGER_TIME()}
InputProps={{
endAdornment: <InputAdornment position="end">{LL.MINUTES()}</InputAdornment>
endAdornment: (
<InputAdornment position="end">{LL.MINUTES()}</InputAdornment>
)
}}
variant="outlined"
value={numberValue(data.shower_alert_trigger)}
@@ -481,7 +572,9 @@ const ApplicationSettings: FC = () => {
name="shower_alert_coldshot"
label={LL.COLD_SHOT_DURATION()}
InputProps={{
endAdornment: <InputAdornment position="end">{LL.SECONDS()}</InputAdornment>
endAdornment: (
<InputAdornment position="end">{LL.SECONDS()}</InputAdornment>
)
}}
variant="outlined"
value={numberValue(data.shower_alert_coldshot)}
@@ -497,7 +590,13 @@ const ApplicationSettings: FC = () => {
<Typography sx={{ pt: 3 }} variant="h6" color="primary">
{LL.FORMATTING_OPTIONS()}
</Typography>
<Grid container spacing={1} direction="row" justifyContent="flex-start" alignItems="flex-start">
<Grid
container
spacing={1}
direction="row"
justifyContent="flex-start"
alignItems="flex-start"
>
<Grid item xs={12} sm={6} md={4}>
<TextField
name="bool_dashboard"
@@ -556,7 +655,13 @@ const ApplicationSettings: FC = () => {
{LL.TEMP_SENSORS()}
</Typography>
<BlockFormControlLabel
control={<Checkbox checked={data.dallas_parasite} onChange={updateFormValue} name="dallas_parasite" />}
control={
<Checkbox
checked={data.dallas_parasite}
onChange={updateFormValue}
name="dallas_parasite"
/>
}
label={LL.ENABLE_PARASITE()}
disabled={saving}
/>
@@ -566,7 +671,13 @@ const ApplicationSettings: FC = () => {
{LL.LOGGING()}
</Typography>
<BlockFormControlLabel
control={<Checkbox checked={data.trace_raw} onChange={updateFormValue} name="trace_raw" />}
control={
<Checkbox
checked={data.trace_raw}
onChange={updateFormValue}
name="trace_raw"
/>
}
label={LL.LOG_HEX()}
disabled={saving}
/>
@@ -582,7 +693,13 @@ const ApplicationSettings: FC = () => {
label={LL.ENABLE_SYSLOG()}
/>
{data.syslog_enabled && (
<Grid container spacing={1} direction="row" justifyContent="flex-start" alignItems="flex-start">
<Grid
container
spacing={1}
direction="row"
justifyContent="flex-start"
alignItems="flex-start"
>
<Grid item xs={12} sm={6}>
<ValidatedTextField
fieldErrors={fieldErrors}
@@ -636,7 +753,9 @@ const ApplicationSettings: FC = () => {
name="syslog_mark_interval"
label={LL.MARK_INTERVAL()}
InputProps={{
endAdornment: <InputAdornment position="end">{LL.SECONDS()}</InputAdornment>
endAdornment: (
<InputAdornment position="end">{LL.SECONDS()}</InputAdornment>
)
}}
fullWidth
variant="outlined"
@@ -651,7 +770,12 @@ const ApplicationSettings: FC = () => {
)}
{restartNeeded && (
<MessageBox my={2} level="warning" message={LL.RESTART_TEXT(0)}>
<Button startIcon={<PowerSettingsNewIcon />} variant="contained" color="error" onClick={restart}>
<Button
startIcon={<PowerSettingsNewIcon />}
variant="contained"
color="error"
onClick={restart}
>
{LL.RESTART()}
</Button>
</MessageBox>

View File

@@ -10,10 +10,24 @@ import RefreshIcon from '@mui/icons-material/Refresh';
import WarningIcon from '@mui/icons-material/Warning';
import { Box, Button, Typography } from '@mui/material';
import { Body, Cell, Header, HeaderCell, HeaderRow, Row, Table } from '@table-library/react-table-library/table';
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 { updateState, useRequest } from 'alova';
import { BlockNavigation, ButtonRow, FormLoader, SectionContent, useLayoutTitle } from 'components';
import {
BlockNavigation,
ButtonRow,
FormLoader,
SectionContent,
useLayoutTitle
} from 'components';
import { useI18nContext } from 'i18n/i18n-react';
import * as EMSESP from './api';
@@ -171,8 +185,13 @@ const CustomEntities: FC = () => {
updateState('entities', (data: EntityItem[]) => {
const new_data = creating
? [...data.filter((ei) => creating || ei.o_id !== updatedItem.o_id), updatedItem]
: data.map((ei) => (ei.id === updatedItem.id ? { ...ei, ...updatedItem } : ei));
? [
...data.filter((ei) => creating || ei.o_id !== updatedItem.o_id),
updatedItem
]
: data.map((ei) =>
ei.id === updatedItem.id ? { ...ei, ...updatedItem } : ei
);
setNumChanges(new_data.filter((ei) => hasEntityChanged(ei)).length);
return new_data;
});
@@ -201,7 +220,8 @@ const CustomEntities: FC = () => {
return value === undefined
? ''
: typeof value === 'number'
? new Intl.NumberFormat().format(value) + (uom === 0 ? '' : ' ' + DeviceValueUOM_s[uom])
? new Intl.NumberFormat().format(value) +
(uom === 0 ? '' : ' ' + DeviceValueUOM_s[uom])
: (value as string);
}
@@ -215,7 +235,11 @@ const CustomEntities: FC = () => {
}
return (
<Table data={{ nodes: entities.filter((ei) => !ei.deleted) }} theme={entity_theme} layout={{ custom: true }}>
<Table
data={{ nodes: entities.filter((ei) => !ei.deleted) }}
theme={entity_theme}
layout={{ custom: true }}
>
{(tableList: EntityItem[]) => (
<>
<Header>
@@ -233,12 +257,18 @@ const CustomEntities: FC = () => {
<Row key={ei.name} item={ei} onClick={() => editEntityItem(ei)}>
<Cell>
{ei.name}&nbsp;
{ei.writeable && <EditOutlinedIcon color="primary" sx={{ fontSize: 12 }} />}
{ei.writeable && (
<EditOutlinedIcon color="primary" sx={{ fontSize: 12 }} />
)}
</Cell>
<Cell>
{ei.ram === 1 ? '' : showHex(ei.device_id as number, 2)}
</Cell>
<Cell>{ei.ram === 1 ? '' : showHex(ei.device_id as number, 2)}</Cell>
<Cell>{ei.ram === 1 ? '' : showHex(ei.type_id as number, 3)}</Cell>
<Cell>{ei.ram === 1 ? '' : ei.offset}</Cell>
<Cell>{ei.ram === 1 ? 'RAM' : DeviceValueTypeNames[ei.value_type]}</Cell>
<Cell>
{ei.ram === 1 ? 'RAM' : DeviceValueTypeNames[ei.value_type]}
</Cell>
<Cell>{formatValue(ei.value, ei.uom)}</Cell>
</Row>
))}
@@ -273,7 +303,12 @@ const CustomEntities: FC = () => {
<Box flexGrow={1}>
{numChanges > 0 && (
<ButtonRow>
<Button startIcon={<CancelIcon />} variant="outlined" onClick={onDialogCancel} color="secondary">
<Button
startIcon={<CancelIcon />}
variant="outlined"
onClick={onDialogCancel}
color="secondary"
>
{LL.CANCEL()}
</Button>
<Button
@@ -289,10 +324,20 @@ const CustomEntities: FC = () => {
</Box>
<Box flexWrap="nowrap" whiteSpace="nowrap">
<ButtonRow>
<Button startIcon={<RefreshIcon />} variant="outlined" color="secondary" onClick={fetchEntities}>
<Button
startIcon={<RefreshIcon />}
variant="outlined"
color="secondary"
onClick={fetchEntities}
>
{LL.REFRESH()}
</Button>
<Button startIcon={<AddIcon />} variant="outlined" color="primary" onClick={addEntityItem}>
<Button
startIcon={<AddIcon />}
variant="outlined"
color="primary"
onClick={addEntityItem}
>
{LL.ADD(0)}
</Button>
</ButtonRow>

View File

@@ -142,7 +142,13 @@ const CustomEntitiesDialog = ({
<>
<Grid item xs={4} mt={3}>
<BlockFormControlLabel
control={<Checkbox checked={editItem.writeable} onChange={updateFormValue} name="writeable" />}
control={
<Checkbox
checked={editItem.writeable}
onChange={updateFormValue}
name="writeable"
/>
}
label={LL.WRITEABLE()}
/>
</Grid>
@@ -157,7 +163,11 @@ const CustomEntitiesDialog = ({
value={editItem.device_id as string}
onChange={updateFormValue}
inputProps={{ style: { textTransform: 'uppercase' } }}
InputProps={{ startAdornment: <InputAdornment position="start">0x</InputAdornment> }}
InputProps={{
startAdornment: (
<InputAdornment position="start">0x</InputAdornment>
)
}}
/>
</Grid>
<Grid item xs={4}>
@@ -170,7 +180,11 @@ const CustomEntitiesDialog = ({
value={editItem.type_id}
onChange={updateFormValue}
inputProps={{ style: { textTransform: 'uppercase' } }}
InputProps={{ startAdornment: <InputAdornment position="start">0x</InputAdornment> }}
InputProps={{
startAdornment: (
<InputAdornment position="start">0x</InputAdornment>
)
}}
/>
</Grid>
<Grid item xs={4}>
@@ -207,55 +221,57 @@ const CustomEntitiesDialog = ({
</TextField>
</Grid>
{editItem.value_type !== DeviceValueType.BOOL && editItem.value_type !== DeviceValueType.STRING && (
<>
{editItem.value_type !== DeviceValueType.BOOL &&
editItem.value_type !== DeviceValueType.STRING && (
<>
<Grid item xs={4}>
<TextField
name="factor"
label={LL.FACTOR()}
value={numberValue(editItem.factor)}
variant="outlined"
onChange={updateFormValue}
fullWidth
margin="normal"
type="number"
inputProps={{ step: '0.001' }}
/>
</Grid>
<Grid item xs={4}>
<TextField
name="uom"
label={LL.UNIT()}
value={editItem.uom}
margin="normal"
fullWidth
onChange={updateFormValue}
select
>
{DeviceValueUOM_s.map((val, i) => (
<MenuItem key={i} value={i}>
{val}
</MenuItem>
))}
</TextField>
</Grid>
</>
)}
{editItem.value_type === DeviceValueType.STRING &&
editItem.device_id !== '0' && (
<Grid item xs={4}>
<TextField
name="factor"
label={LL.FACTOR()}
value={numberValue(editItem.factor)}
label="Bytes"
value={editItem.factor}
variant="outlined"
onChange={updateFormValue}
fullWidth
margin="normal"
type="number"
inputProps={{ step: '0.001' }}
inputProps={{ min: '1', max: '27', step: '1' }}
/>
</Grid>
<Grid item xs={4}>
<TextField
name="uom"
label={LL.UNIT()}
value={editItem.uom}
margin="normal"
fullWidth
onChange={updateFormValue}
select
>
{DeviceValueUOM_s.map((val, i) => (
<MenuItem key={i} value={i}>
{val}
</MenuItem>
))}
</TextField>
</Grid>
</>
)}
{editItem.value_type === DeviceValueType.STRING && editItem.device_id !== '0' && (
<Grid item xs={4}>
<TextField
name="factor"
label="Bytes"
value={editItem.factor}
variant="outlined"
onChange={updateFormValue}
fullWidth
margin="normal"
type="number"
inputProps={{ min: '1', max: '27', step: '1' }}
/>
</Grid>
)}
)}
</>
)}
</Grid>
@@ -264,15 +280,30 @@ const CustomEntitiesDialog = ({
<DialogActions>
{!creating && (
<Box flexGrow={1}>
<Button startIcon={<RemoveIcon />} variant="outlined" color="warning" onClick={remove}>
<Button
startIcon={<RemoveIcon />}
variant="outlined"
color="warning"
onClick={remove}
>
{LL.REMOVE()}
</Button>
</Box>
)}
<Button startIcon={<CancelIcon />} variant="outlined" onClick={close} color="secondary">
<Button
startIcon={<CancelIcon />}
variant="outlined"
onClick={close}
color="secondary"
>
{LL.CANCEL()}
</Button>
<Button startIcon={creating ? <AddIcon /> : <DoneIcon />} variant="outlined" onClick={save} color="primary">
<Button
startIcon={creating ? <AddIcon /> : <DoneIcon />}
variant="outlined"
onClick={save}
color="primary"
>
{creating ? LL.ADD(0) : LL.UPDATE()}
</Button>
</DialogActions>

View File

@@ -27,11 +27,25 @@ import {
import * as SystemApi from 'api/system';
import { Body, Cell, Header, HeaderCell, HeaderRow, Row, Table } from '@table-library/react-table-library/table';
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 { dialogStyle } from 'CustomTheme';
import { useRequest } from 'alova';
import { BlockNavigation, ButtonRow, MessageBox, SectionContent, useLayoutTitle } from 'components';
import {
BlockNavigation,
ButtonRow,
MessageBox,
SectionContent,
useLayoutTitle
} from 'components';
import RestartMonitor from 'framework/system/RestartMonitor';
import { useI18nContext } from 'i18n/i18n-react';
@@ -63,7 +77,9 @@ const Customization: FC = () => {
// fetch devices first
const { data: devices } = useRequest(EMSESP.readDevices);
const [selectedDevice, setSelectedDevice] = useState<number>(Number(useLocation().state) || -1);
const [selectedDevice, setSelectedDevice] = useState<number>(
Number(useLocation().state) || -1
);
const [selectedDeviceName, setSelectedDeviceName] = useState<string>('');
const { send: resetCustomizations } = useRequest(EMSESP.resetCustomizations(), {
@@ -71,7 +87,8 @@ const Customization: FC = () => {
});
const { send: writeCustomizationEntities } = useRequest(
(data: { id: number; entity_ids: string[] }) => EMSESP.writeCustomizationEntities(data),
(data: { id: number; entity_ids: string[] }) =>
EMSESP.writeCustomizationEntities(data),
{
immediate: false
}
@@ -86,7 +103,15 @@ const Customization: FC = () => {
);
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 })));
setDeviceEntities(
data.map((de) => ({
...de,
o_m: de.m,
o_cn: de.cn,
o_mi: de.mi,
o_ma: de.ma
}))
);
};
onSuccess((event) => {
@@ -166,7 +191,12 @@ const Customization: FC = () => {
});
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;
return (
(de?.cn || '') !== (de?.o_cn || '') ||
de.m !== de.o_m ||
de.ma !== de.o_ma ||
de.mi !== de.o_mi
);
}
useEffect(() => {
@@ -221,8 +251,11 @@ const Customization: FC = () => {
}
const formatName = (de: DeviceEntity, withShortname: boolean) =>
(de.n && de.n[0] === '!' ? LL.COMMAND(1) + ': ' + de.n.slice(1) : de.cn && de.cn !== '' ? de.cn : de.n) +
(withShortname ? ' ' + de.id : '');
(de.n && de.n[0] === '!'
? LL.COMMAND(1) + ': ' + de.n.slice(1)
: de.cn && de.cn !== ''
? de.cn
: de.n) + (withShortname ? ' ' + de.id : '');
const getMaskNumber = (newMask: string[]) => {
let new_mask = 0;
@@ -253,7 +286,8 @@ const Customization: FC = () => {
};
const filter_entity = (de: DeviceEntity) =>
(de.m & selectedFilters || !selectedFilters) && formatName(de, true).includes(search.toLocaleLowerCase());
(de.m & selectedFilters || !selectedFilters) &&
formatName(de, true).includes(search.toLocaleLowerCase());
const maskDisabled = (set: boolean) => {
setDeviceEntities(
@@ -262,8 +296,14 @@ const Customization: FC = () => {
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)
? 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;
@@ -288,7 +328,11 @@ const Customization: FC = () => {
};
const updateDeviceEntity = (updatedItem: DeviceEntity) => {
setDeviceEntities(deviceEntities?.map((de) => (de.id === updatedItem.id ? { ...de, ...updatedItem } : de)));
setDeviceEntities(
deviceEntities?.map((de) =>
de.id === updatedItem.id ? { ...de, ...updatedItem } : de
)
);
};
const onDialogSave = (updatedItem: DeviceEntity) => {
@@ -330,7 +374,10 @@ const Customization: FC = () => {
return;
}
await writeCustomizationEntities({ id: selectedDevice, entity_ids: masked_entities }).catch((error: Error) => {
await writeCustomizationEntities({
id: selectedDevice,
entity_ids: masked_entities
}).catch((error: Error) => {
if (error.message === 'Reboot required') {
setRestartNeeded(true);
} else {
@@ -376,14 +423,26 @@ const Customization: FC = () => {
<>
<Box color="warning.main">
<Typography variant="body2" mt={1}>
<OptionIcon type="favorite" isSet={true} />={LL.CUSTOMIZATIONS_HELP_2()}&nbsp;&nbsp;
<OptionIcon type="readonly" isSet={true} />={LL.CUSTOMIZATIONS_HELP_3()}&nbsp;&nbsp;
<OptionIcon type="api_mqtt_exclude" isSet={true} />={LL.CUSTOMIZATIONS_HELP_4()}&nbsp;&nbsp;
<OptionIcon type="web_exclude" isSet={true} />={LL.CUSTOMIZATIONS_HELP_5()}&nbsp;&nbsp;
<OptionIcon type="favorite" isSet={true} />={LL.CUSTOMIZATIONS_HELP_2()}
&nbsp;&nbsp;
<OptionIcon type="readonly" isSet={true} />={LL.CUSTOMIZATIONS_HELP_3()}
&nbsp;&nbsp;
<OptionIcon type="api_mqtt_exclude" isSet={true} />=
{LL.CUSTOMIZATIONS_HELP_4()}&nbsp;&nbsp;
<OptionIcon type="web_exclude" isSet={true} />=
{LL.CUSTOMIZATIONS_HELP_5()}&nbsp;&nbsp;
<OptionIcon type="deleted" isSet={true} />={LL.CUSTOMIZATIONS_HELP_6()}
</Typography>
</Box>
<Grid container mb={1} mt={0} spacing={1} direction="row" justifyContent="flex-start" alignItems="center">
<Grid
container
mb={1}
mt={0}
spacing={1}
direction="row"
justifyContent="flex-start"
alignItems="center"
>
<Grid item xs={2}>
<TextField
size="small"
@@ -455,11 +514,16 @@ const Customization: FC = () => {
</Grid>
<Grid item>
<Typography variant="subtitle2" color="primary">
{LL.SHOWING()}&nbsp;{shown_data.length}/{deviceEntities.length}&nbsp;{LL.ENTITIES(deviceEntities.length)}
{LL.SHOWING()}&nbsp;{shown_data.length}/{deviceEntities.length}
&nbsp;{LL.ENTITIES(deviceEntities.length)}
</Typography>
</Grid>
</Grid>
<Table data={{ nodes: shown_data }} theme={entities_theme} layout={{ custom: true }}>
<Table
data={{ nodes: shown_data }}
theme={entities_theme}
layout={{ custom: true }}
>
{(tableList: DeviceEntity[]) => (
<>
<Header>
@@ -479,13 +543,20 @@ const Customization: FC = () => {
</Cell>
<Cell>
{formatName(de, false)}&nbsp;(
<Link target="_blank" href={APIURL + selectedDeviceName + '/' + de.id}>
<Link
target="_blank"
href={APIURL + selectedDeviceName + '/' + de.id}
>
{de.id}
</Link>
)
</Cell>
<Cell>{!(de.m & DeviceEntityMask.DV_READONLY) && formatValue(de.mi)}</Cell>
<Cell>{!(de.m & DeviceEntityMask.DV_READONLY) && formatValue(de.ma)}</Cell>
<Cell>
{!(de.m & DeviceEntityMask.DV_READONLY) && formatValue(de.mi)}
</Cell>
<Cell>
{!(de.m & DeviceEntityMask.DV_READONLY) && formatValue(de.ma)}
</Cell>
<Cell>{formatValue(de.v)}</Cell>
</Row>
))}
@@ -498,14 +569,28 @@ const Customization: FC = () => {
};
const renderResetDialog = () => (
<Dialog sx={dialogStyle} open={confirmReset} onClose={() => setConfirmReset(false)}>
<Dialog
sx={dialogStyle}
open={confirmReset}
onClose={() => setConfirmReset(false)}
>
<DialogTitle>{LL.RESET(1)}</DialogTitle>
<DialogContent dividers>{LL.CUSTOMIZATIONS_RESET()}</DialogContent>
<DialogActions>
<Button startIcon={<CancelIcon />} variant="outlined" onClick={() => setConfirmReset(false)} color="secondary">
<Button
startIcon={<CancelIcon />}
variant="outlined"
onClick={() => setConfirmReset(false)}
color="secondary"
>
{LL.CANCEL()}
</Button>
<Button startIcon={<SettingsBackupRestoreIcon />} variant="outlined" onClick={resetCustomization} color="error">
<Button
startIcon={<SettingsBackupRestoreIcon />}
variant="outlined"
onClick={resetCustomization}
color="error"
>
{LL.RESET(0)}
</Button>
</DialogActions>
@@ -518,7 +603,12 @@ const Customization: FC = () => {
{deviceEntities && renderDeviceData()}
{restartNeeded && (
<MessageBox my={2} level="warning" message={LL.RESTART_TEXT(0)}>
<Button startIcon={<PowerSettingsNewIcon />} variant="contained" color="error" onClick={restart}>
<Button
startIcon={<PowerSettingsNewIcon />}
variant="contained"
color="error"
onClick={restart}
>
{LL.RESTART()}
</Button>
</MessageBox>

View File

@@ -30,7 +30,12 @@ interface SettingsCustomizationDialogProps {
selectedItem: DeviceEntity;
}
const CustomizationDialog = ({ open, onClose, onSave, selectedItem }: SettingsCustomizationDialogProps) => {
const CustomizationDialog = ({
open,
onClose,
onSave,
selectedItem
}: SettingsCustomizationDialogProps) => {
const { LL } = useI18nContext();
const [editItem, setEditItem] = useState<DeviceEntity>(selectedItem);
const [error, setError] = useState<boolean>(false);
@@ -38,7 +43,9 @@ const CustomizationDialog = ({ open, onClose, onSave, selectedItem }: SettingsCu
const updateFormValue = updateValue(setEditItem);
const isWriteableNumber =
typeof editItem.v === 'number' && editItem.w && !(editItem.m & DeviceEntityMask.DV_READONLY);
typeof editItem.v === 'number' &&
editItem.w &&
!(editItem.m & DeviceEntityMask.DV_READONLY);
useEffect(() => {
if (open) {
@@ -52,7 +59,12 @@ const CustomizationDialog = ({ open, onClose, onSave, selectedItem }: SettingsCu
};
const save = () => {
if (isWriteableNumber && editItem.mi && editItem.ma && editItem.mi > editItem?.ma) {
if (
isWriteableNumber &&
editItem.mi &&
editItem.ma &&
editItem.mi > editItem?.ma
) {
setError(true);
} else {
onSave(editItem);
@@ -140,10 +152,20 @@ const CustomizationDialog = ({ open, onClose, onSave, selectedItem }: SettingsCu
)}
</DialogContent>
<DialogActions>
<Button startIcon={<CancelIcon />} variant="outlined" onClick={close} color="secondary">
<Button
startIcon={<CancelIcon />}
variant="outlined"
onClick={close}
color="secondary"
>
{LL.CANCEL()}
</Button>
<Button startIcon={<DoneIcon />} variant="outlined" onClick={save} color="primary">
<Button
startIcon={<DoneIcon />}
variant="outlined"
onClick={save}
color="primary"
>
{LL.UPDATE()}
</Button>
</DialogActions>

View File

@@ -3,7 +3,12 @@ import { AiOutlineAlert, AiOutlineControl, AiOutlineGateway } from 'react-icons/
import { CgSmartHomeBoiler } from 'react-icons/cg';
import { FaSolarPanel } from 'react-icons/fa';
import { GiHeatHaze, GiTap } from 'react-icons/gi';
import { MdOutlineDevices, MdOutlinePool, MdOutlineSensors, MdThermostatAuto } from 'react-icons/md';
import {
MdOutlineDevices,
MdOutlinePool,
MdOutlineSensors,
MdThermostatAuto
} from 'react-icons/md';
import { TiFlowSwitch } from 'react-icons/ti';
import { VscVmConnect } from 'react-icons/vsc';
@@ -47,7 +52,11 @@ const DeviceIcon: FC<DeviceIconProps> = ({ type_id }) => {
case DeviceType.POOL:
return <MdOutlinePool />;
case DeviceType.CUSTOM:
return <PlaylistAddIcon sx={{ color: 'lightblue', fontSize: 22, verticalAlign: 'middle' }} />;
return (
<PlaylistAddIcon
sx={{ color: 'lightblue', fontSize: 22, verticalAlign: 'middle' }}
/>
);
default:
return null;
}

View File

@@ -1,4 +1,10 @@
import { useCallback, useContext, useEffect, useLayoutEffect, useState } from 'react';
import {
useCallback,
useContext,
useEffect,
useLayoutEffect,
useState
} from 'react';
import type { FC } from 'react';
import { IconContext } from 'react-icons';
import { useNavigate } from 'react-router-dom';
@@ -35,7 +41,15 @@ import {
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 {
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';
@@ -67,19 +81,25 @@ const Devices: FC = () => {
useLayoutTitle(LL.DEVICES());
const { data: coreData, send: readCoreData } = useRequest(() => EMSESP.readCoreData(), {
initialData: {
connected: true,
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 { 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),
@@ -235,9 +255,14 @@ const Devices: FC = () => {
},
sortToggleType: SortToggleType.AlternateWithReset,
sortFns: {
NAME: (array) => array.sort((a, b) => a.id.toString().slice(2).localeCompare(b.id.toString().slice(2))),
// eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call
VALUE: (array) => array.sort((a, b) => a.v.toString().localeCompare(b.v.toString()))
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()))
}
}
);
@@ -300,35 +325,59 @@ const Devices: FC = () => {
if (sc === '' || sc === '""') {
return sc;
}
if (sc.includes('"') || sc.includes(';') || sc.includes('\n') || sc.includes('\r')) {
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 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);
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 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),
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'),
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 : '',
dv.h
? dv.h
: dv.l
? dv.l.join(' | ')
: dv.m !== undefined && dv.x !== undefined
? dv.m + ', ' + dv.x
: '',
name: 'Range'
}
];
@@ -341,10 +390,13 @@ const Devices: FC = () => {
(csvString: string, rowItem: DeviceValue) =>
csvString +
columns
.map(({ accessor }: { accessor: (dv: DeviceValue) => unknown }) => escapeCsvCell(accessor(rowItem) as string))
.map(({ accessor }: { accessor: (dv: DeviceValue) => unknown }) =>
escapeCsvCell(accessor(rowItem) as string)
)
.join(';') +
'\r\n',
columns.map(({ name }: { name: string }) => escapeCsvCell(name)).join(';') + '\r\n'
columns.map(({ name }: { name: string }) => escapeCsvCell(name)).join(';') +
'\r\n'
);
const csvFile = new Blob([csvData], { type: 'text/csv;charset:utf-8' });
@@ -381,45 +433,76 @@ const Devices: FC = () => {
const renderDeviceDetails = () => {
if (showDeviceInfo) {
const deviceIndex = coreData.devices.findIndex((d) => d.id === device_select.state.id);
const deviceIndex = coreData.devices.findIndex(
(d) => d.id === device_select.state.id
);
if (deviceIndex === -1) {
return;
}
return (
<Dialog sx={dialogStyle} open={showDeviceInfo} onClose={() => setShowDeviceInfo(false)}>
<Dialog
sx={dialogStyle}
open={showDeviceInfo}
onClose={() => setShowDeviceInfo(false)}
>
<DialogTitle>{LL.DEVICE_DETAILS()}</DialogTitle>
<DialogContent dividers>
<List dense={true}>
<ListItem>
<ListItemText primary={LL.TYPE(0)} secondary={coreData.devices[deviceIndex].tn} />
<ListItemText
primary={LL.TYPE(0)}
secondary={coreData.devices[deviceIndex].tn}
/>
</ListItem>
<ListItem>
<ListItemText primary={LL.NAME(0)} secondary={coreData.devices[deviceIndex].n} />
<ListItemText
primary={LL.NAME(0)}
secondary={coreData.devices[deviceIndex].n}
/>
</ListItem>
{coreData.devices[deviceIndex].t !== DeviceType.CUSTOM && (
<>
<ListItem>
<ListItemText primary={LL.BRAND()} secondary={coreData.devices[deviceIndex].b} />
<ListItemText
primary={LL.BRAND()}
secondary={coreData.devices[deviceIndex].b}
/>
</ListItem>
<ListItem>
<ListItemText
primary={LL.ID_OF(LL.DEVICE())}
secondary={'0x' + ('00' + coreData.devices[deviceIndex].d.toString(16).toUpperCase()).slice(-2)}
secondary={
'0x' +
(
'00' +
coreData.devices[deviceIndex].d.toString(16).toUpperCase()
).slice(-2)
}
/>
</ListItem>
<ListItem>
<ListItemText primary={LL.ID_OF(LL.PRODUCT())} secondary={coreData.devices[deviceIndex].p} />
<ListItemText
primary={LL.ID_OF(LL.PRODUCT())}
secondary={coreData.devices[deviceIndex].p}
/>
</ListItem>
<ListItem>
<ListItemText primary={LL.VERSION()} secondary={coreData.devices[deviceIndex].v} />
<ListItemText
primary={LL.VERSION()}
secondary={coreData.devices[deviceIndex].v}
/>
</ListItem>
</>
)}
</List>
</DialogContent>
<DialogActions>
<Button variant="outlined" onClick={() => setShowDeviceInfo(false)} color="secondary">
<Button
variant="outlined"
onClick={() => setShowDeviceInfo(false)}
color="secondary"
>
{LL.CLOSE()}
</Button>
</DialogActions>
@@ -429,11 +512,24 @@ const Devices: FC = () => {
};
const renderCoreData = () => (
<IconContext.Provider value={{ color: 'lightblue', size: '18', style: { verticalAlign: 'middle' } }}>
{!coreData.connected && <MessageBox my={2} level="error" message={LL.EMS_BUS_WARNING()} />}
<IconContext.Provider
value={{
color: 'lightblue',
size: '18',
style: { verticalAlign: 'middle' }
}}
>
{!coreData.connected && (
<MessageBox my={2} level="error" message={LL.EMS_BUS_WARNING()} />
)}
{coreData.connected && (
<Table data={{ nodes: coreData.devices }} select={device_select} theme={device_theme} layout={{ custom: true }}>
<Table
data={{ nodes: coreData.devices }}
select={device_select}
theme={device_theme}
layout={{ custom: true }}
>
{(tableList: Device[]) => (
<>
<Header>
@@ -451,7 +547,9 @@ const Devices: FC = () => {
</Cell>
<Cell>
{device.n}
<span style={{ color: 'lightblue' }}>&nbsp;&nbsp;({device.e})</span>
<span style={{ color: 'lightblue' }}>
&nbsp;&nbsp;({device.e})
</span>
</Cell>
<Cell stiff>{device.tn}</Cell>
</Row>
@@ -481,8 +579,12 @@ const Devices: FC = () => {
const renderNameCell = (dv: DeviceValue) => (
<>
{dv.id.slice(2)}&nbsp;
{hasMask(dv.id, DeviceEntityMask.DV_FAVORITE) && <StarIcon color="primary" sx={{ fontSize: 12 }} />}
{hasMask(dv.id, DeviceEntityMask.DV_READONLY) && <EditOffOutlinedIcon color="primary" sx={{ fontSize: 12 }} />}
{hasMask(dv.id, DeviceEntityMask.DV_FAVORITE) && (
<StarIcon color="primary" sx={{ fontSize: 12 }} />
)}
{hasMask(dv.id, DeviceEntityMask.DV_READONLY) && (
<EditOffOutlinedIcon color="primary" sx={{ fontSize: 12 }} />
)}
{hasMask(dv.id, DeviceEntityMask.DV_API_MQTT_EXCLUDE) && (
<CommentsDisabledOutlinedIcon color="primary" sx={{ fontSize: 12 }} />
)}
@@ -493,7 +595,9 @@ const Devices: FC = () => {
? deviceData.data.filter((dv) => hasMask(dv.id, DeviceEntityMask.DV_FAVORITE))
: deviceData.data;
const deviceIndex = coreData.devices.findIndex((d) => d.id === device_select.state.id);
const deviceIndex = coreData.devices.findIndex(
(d) => d.id === device_select.state.id
);
if (deviceIndex === -1) {
return;
}
@@ -514,7 +618,8 @@ const Devices: FC = () => {
>
<Box sx={{ border: '1px solid #177ac9' }}>
<Typography noWrap variant="subtitle1" color="warning.main" sx={{ ml: 1 }}>
{coreData.devices[deviceIndex].tn}&nbsp;&#124;&nbsp;{coreData.devices[deviceIndex].n}
{coreData.devices[deviceIndex].tn}&nbsp;&#124;&nbsp;
{coreData.devices[deviceIndex].n}
</Typography>
<Grid container justifyContent="space-between">
@@ -527,30 +632,50 @@ const Devices: FC = () => {
' ' +
LL.ENTITIES(shown_data.length)}
<IconButton onClick={() => setShowDeviceInfo(true)}>
<InfoOutlinedIcon color="primary" sx={{ fontSize: 18, verticalAlign: 'middle' }} />
<InfoOutlinedIcon
color="primary"
sx={{ fontSize: 18, verticalAlign: 'middle' }}
/>
</IconButton>
{me.admin && (
<IconButton onClick={customize}>
<FormatListNumberedIcon sx={{ fontSize: 18, verticalAlign: 'middle' }} />
<FormatListNumberedIcon
sx={{ fontSize: 18, verticalAlign: 'middle' }}
/>
</IconButton>
)}
<IconButton onClick={handleDownloadCsv}>
<DownloadIcon color="primary" sx={{ fontSize: 18, verticalAlign: 'middle' }} />
<DownloadIcon
color="primary"
sx={{ fontSize: 18, verticalAlign: 'middle' }}
/>
</IconButton>
<IconButton onClick={() => setOnlyFav(!onlyFav)}>
{onlyFav ? (
<StarIcon color="primary" sx={{ fontSize: 18, verticalAlign: 'middle' }} />
<StarIcon
color="primary"
sx={{ fontSize: 18, verticalAlign: 'middle' }}
/>
) : (
<StarBorderOutlinedIcon color="primary" sx={{ fontSize: 18, verticalAlign: 'middle' }} />
<StarBorderOutlinedIcon
color="primary"
sx={{ fontSize: 18, verticalAlign: 'middle' }}
/>
)}
</IconButton>
<IconButton onClick={refreshData}>
<RefreshIcon color="primary" sx={{ fontSize: 18, verticalAlign: 'middle' }} />
<RefreshIcon
color="primary"
sx={{ fontSize: 18, verticalAlign: 'middle' }}
/>
</IconButton>
</Typography>
<Grid item zeroMinWidth justifyContent="flex-end">
<IconButton onClick={resetDeviceSelect}>
<HighlightOffIcon color="primary" sx={{ fontSize: 18, verticalAlign: 'middle' }} />
<HighlightOffIcon
color="primary"
sx={{ fontSize: 18, verticalAlign: 'middle' }}
/>
</IconButton>
</Grid>
</Grid>
@@ -595,15 +720,20 @@ const Devices: FC = () => {
<Cell>{renderNameCell(dv)}</Cell>
<Cell>{formatValue(LL, dv.v, dv.u)}</Cell>
<Cell stiff>
{me.admin && dv.c && !hasMask(dv.id, DeviceEntityMask.DV_READONLY) && (
<IconButton size="small" onClick={() => showDeviceValue(dv)}>
{dv.v === '' && dv.c ? (
<PlayArrowIcon color="primary" sx={{ fontSize: 16 }} />
) : (
<EditIcon color="primary" sx={{ fontSize: 16 }} />
)}
</IconButton>
)}
{me.admin &&
dv.c &&
!hasMask(dv.id, DeviceEntityMask.DV_READONLY) && (
<IconButton
size="small"
onClick={() => showDeviceValue(dv)}
>
{dv.v === '' && dv.c ? (
<PlayArrowIcon color="primary" sx={{ fontSize: 16 }} />
) : (
<EditIcon color="primary" sx={{ fontSize: 16 }} />
)}
</IconButton>
)}
</Cell>
</Row>
))}
@@ -627,14 +757,20 @@ const Devices: FC = () => {
onSave={deviceValueDialogSave}
selectedItem={selectedDeviceValue}
writeable={
selectedDeviceValue.c !== undefined && !hasMask(selectedDeviceValue.id, DeviceEntityMask.DV_READONLY)
selectedDeviceValue.c !== undefined &&
!hasMask(selectedDeviceValue.id, DeviceEntityMask.DV_READONLY)
}
validator={deviceValueItemValidation(selectedDeviceValue)}
progress={submitting}
/>
)}
<ButtonRow mt={1}>
<Button startIcon={<RefreshIcon />} variant="outlined" color="secondary" onClick={refreshData}>
<Button
startIcon={<RefreshIcon />}
variant="outlined"
color="secondary"
onClick={refreshData}
>
{LL.REFRESH()}
</Button>
</ButtonRow>

View File

@@ -102,7 +102,11 @@ const DevicesDialog = ({
return (
<Dialog sx={dialogStyle} open={open} onClose={close}>
<DialogTitle>
{selectedItem.v === '' && selectedItem.c ? LL.RUN_COMMAND() : writeable ? LL.CHANGE_VALUE() : LL.VALUE(1)}
{selectedItem.v === '' && selectedItem.c
? LL.RUN_COMMAND()
: writeable
? LL.CHANGE_VALUE()
: LL.VALUE(1)}
</DialogTitle>
<DialogContent dividers>
<Box color="warning.main" p={0} pl={0} pr={0} mt={0} mb={2}>
@@ -138,9 +142,17 @@ const DevicesDialog = ({
type="number"
sx={{ width: '30ch' }}
onChange={updateFormValue}
inputProps={editItem.s ? { min: editItem.m, max: editItem.x, step: editItem.s } : {}}
inputProps={
editItem.s
? { min: editItem.m, max: editItem.x, step: editItem.s }
: {}
}
InputProps={{
startAdornment: <InputAdornment position="start">{setUom(editItem.u)}</InputAdornment>
startAdornment: (
<InputAdornment position="start">
{setUom(editItem.u)}
</InputAdornment>
)
}}
/>
) : (
@@ -175,10 +187,20 @@ const DevicesDialog = ({
position: 'relative'
}}
>
<Button startIcon={<CancelIcon />} variant="outlined" onClick={close} color="secondary">
<Button
startIcon={<CancelIcon />}
variant="outlined"
onClick={close}
color="secondary"
>
{LL.CANCEL()}
</Button>
<Button startIcon={<WarningIcon color="warning" />} variant="contained" onClick={save} color="info">
<Button
startIcon={<WarningIcon color="warning" />}
variant="contained"
onClick={save}
color="info"
>
{selectedItem.v === '' && selectedItem.c ? LL.EXECUTE() : LL.UPDATE()}
</Button>
{progress && (

View File

@@ -55,25 +55,46 @@ const EntityMaskToggle = ({ onUpdate, de }: EntityMaskToggleProps) => {
}}
>
<ToggleButton value="8" disabled={(de.m & 0x81) !== 0 || de.n === undefined}>
<OptionIcon type="favorite" isSet={(de.m & DeviceEntityMask.DV_FAVORITE) === DeviceEntityMask.DV_FAVORITE} />
<OptionIcon
type="favorite"
isSet={
(de.m & DeviceEntityMask.DV_FAVORITE) === DeviceEntityMask.DV_FAVORITE
}
/>
</ToggleButton>
<ToggleButton value="4" disabled={!de.w || (de.m & 0x83) >= 3}>
<OptionIcon type="readonly" isSet={(de.m & DeviceEntityMask.DV_READONLY) === DeviceEntityMask.DV_READONLY} />
<OptionIcon
type="readonly"
isSet={
(de.m & DeviceEntityMask.DV_READONLY) === DeviceEntityMask.DV_READONLY
}
/>
</ToggleButton>
<ToggleButton value="2" disabled={de.n === '' || (de.m & 0x80) !== 0}>
<OptionIcon
type="api_mqtt_exclude"
isSet={(de.m & DeviceEntityMask.DV_API_MQTT_EXCLUDE) === DeviceEntityMask.DV_API_MQTT_EXCLUDE}
isSet={
(de.m & DeviceEntityMask.DV_API_MQTT_EXCLUDE) ===
DeviceEntityMask.DV_API_MQTT_EXCLUDE
}
/>
</ToggleButton>
<ToggleButton value="1" disabled={de.n === undefined || (de.m & 0x80) !== 0}>
<OptionIcon
type="web_exclude"
isSet={(de.m & DeviceEntityMask.DV_WEB_EXCLUDE) === DeviceEntityMask.DV_WEB_EXCLUDE}
isSet={
(de.m & DeviceEntityMask.DV_WEB_EXCLUDE) ===
DeviceEntityMask.DV_WEB_EXCLUDE
}
/>
</ToggleButton>
<ToggleButton value="128">
<OptionIcon type="deleted" isSet={(de.m & DeviceEntityMask.DV_DELETED) === DeviceEntityMask.DV_DELETED} />
<OptionIcon
type="deleted"
isSet={
(de.m & DeviceEntityMask.DV_DELETED) === DeviceEntityMask.DV_DELETED
}
/>
</ToggleButton>
</ToggleButtonGroup>
);

View File

@@ -29,9 +29,12 @@ const Help: FC = () => {
const { LL } = useI18nContext();
useLayoutTitle(LL.HELP_OF(''));
const { send: getAPI, onSuccess: onGetAPI } = useRequest((data: APIcall) => EMSESP.API(data), {
immediate: false
});
const { send: getAPI, onSuccess: onGetAPI } = useRequest(
(data: APIcall) => EMSESP.API(data),
{
immediate: false
}
);
onGetAPI((event) => {
const anchor = document.createElement('a');
@@ -40,8 +43,10 @@ const Help: FC = () => {
type: 'text/plain'
})
);
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
anchor.download = 'emsesp_' + event.sendArgs[0].device + '_' + event.sendArgs[0].entity + '.txt';
anchor.download =
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
'emsesp_' + event.sendArgs[0].device + '_' + event.sendArgs[0].entity + '.txt';
anchor.click();
URL.revokeObjectURL(anchor.href);
toast.info(LL.DOWNLOAD_SUCCESSFUL());
@@ -79,7 +84,10 @@ const Help: FC = () => {
</ListItem>
<ListItem>
<ListItemButton component="a" href="https://github.com/emsesp/EMS-ESP32/issues/new/choose">
<ListItemButton
component="a"
href="https://github.com/emsesp/EMS-ESP32/issues/new/choose"
>
<ListItemAvatar>
<Avatar sx={{ bgcolor: '#72caf9' }}>
<GitHubIcon />
@@ -119,7 +127,11 @@ const Help: FC = () => {
<b>{LL.HELP_INFORMATION_5()}</b>
</Typography>
<Typography align="center">
<Link target="_blank" href="https://github.com/emsesp/EMS-ESP32" color="primary">
<Link
target="_blank"
href="https://github.com/emsesp/EMS-ESP32"
color="primary"
>
{'github.com/emsesp/EMS-ESP32'}
</Link>
</Typography>

View File

@@ -12,9 +12,19 @@ import VisibilityOffOutlinedIcon from '@mui/icons-material/VisibilityOffOutlined
import VisibilityOutlinedIcon from '@mui/icons-material/VisibilityOutlined';
import type { SvgIconProps } from '@mui/material';
type OptionType = 'deleted' | 'readonly' | 'web_exclude' | 'api_mqtt_exclude' | 'favorite';
type OptionType =
| 'deleted'
| 'readonly'
| 'web_exclude'
| 'api_mqtt_exclude'
| 'favorite';
const OPTION_ICONS: { [type in OptionType]: [React.ComponentType<SvgIconProps>, React.ComponentType<SvgIconProps>] } = {
const OPTION_ICONS: {
[type in OptionType]: [
React.ComponentType<SvgIconProps>,
React.ComponentType<SvgIconProps>
];
} = {
deleted: [DeleteForeverIcon, DeleteOutlineIcon],
readonly: [EditOffOutlinedIcon, EditOutlinedIcon],
web_exclude: [VisibilityOffOutlinedIcon, VisibilityOutlinedIcon],

View File

@@ -9,10 +9,24 @@ import CircleIcon from '@mui/icons-material/Circle';
import WarningIcon from '@mui/icons-material/Warning';
import { Box, Button, Divider, Stack, Typography } from '@mui/material';
import { Body, Cell, Header, HeaderCell, HeaderRow, Row, Table } from '@table-library/react-table-library/table';
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 { updateState, useRequest } from 'alova';
import { BlockNavigation, ButtonRow, FormLoader, SectionContent, useLayoutTitle } from 'components';
import {
BlockNavigation,
ButtonRow,
FormLoader,
SectionContent,
useLayoutTitle
} from 'components';
import { useI18nContext } from 'i18n/i18n-react';
import * as EMSESP from './api';
@@ -39,9 +53,12 @@ const Scheduler: FC = () => {
force: true
});
const { send: writeSchedule } = useRequest((data: Schedule) => EMSESP.writeSchedule(data), {
immediate: false
});
const { send: writeSchedule } = useRequest(
(data: Schedule) => EMSESP.writeSchedule(data),
{
immediate: false
}
);
function hasScheduleChanged(si: ScheduleItem) {
return (
@@ -57,7 +74,10 @@ const Scheduler: FC = () => {
}
useEffect(() => {
const formatter = new Intl.DateTimeFormat(locale, { weekday: 'short', timeZone: 'UTC' });
const formatter = new Intl.DateTimeFormat(locale, {
weekday: 'short',
timeZone: 'UTC'
});
const days = [1, 2, 3, 4, 5, 6, 7].map((day) => {
const dd = day < 10 ? `0${day}` : day;
return new Date(`2017-01-${dd}T00:00:00+00:00`);
@@ -157,8 +177,13 @@ const Scheduler: FC = () => {
updateState('schedule', (data: ScheduleItem[]) => {
const new_data = creating
? [...data.filter((si) => creating || si.o_id !== updatedItem.o_id), updatedItem]
: data.map((si) => (si.id === updatedItem.id ? { ...si, ...updatedItem } : si));
? [
...data.filter((si) => creating || si.o_id !== updatedItem.o_id),
updatedItem
]
: data.map((si) =>
si.id === updatedItem.id ? { ...si, ...updatedItem } : si
);
setNumChanges(new_data.filter((si) => hasScheduleChanged(si)).length);
@@ -189,8 +214,13 @@ const Scheduler: FC = () => {
const dayBox = (si: ScheduleItem, flag: number) => (
<>
<Box>
<Typography sx={{ fontSize: 11 }} color={(si.flags & flag) === flag ? 'primary' : 'grey'}>
{flag === ScheduleFlag.SCHEDULE_TIMER ? LL.TIMER(0) : dow[Math.log(flag) / Math.log(2)]}
<Typography
sx={{ fontSize: 11 }}
color={(si.flags & flag) === flag ? 'primary' : 'grey'}
>
{flag === ScheduleFlag.SCHEDULE_TIMER
? LL.TIMER(0)
: dow[Math.log(flag) / Math.log(2)]}
</Typography>
</Box>
<Divider orientation="vertical" flexItem />
@@ -201,7 +231,11 @@ const Scheduler: FC = () => {
return (
<Table
data={{ nodes: schedule.filter((si) => !si.deleted).sort((a, b) => a.time.localeCompare(b.time)) }}
data={{
nodes: schedule
.filter((si) => !si.deleted)
.sort((a, b) => a.time.localeCompare(b.time))
}}
theme={schedule_theme}
layout={{ custom: true }}
>
@@ -222,9 +256,15 @@ const Scheduler: FC = () => {
<Row key={si.id} item={si} onClick={() => editScheduleItem(si)}>
<Cell stiff>
{si.active ? (
<CircleIcon color="success" sx={{ fontSize: 16, verticalAlign: 'middle' }} />
<CircleIcon
color="success"
sx={{ fontSize: 16, verticalAlign: 'middle' }}
/>
) : (
<CircleIcon color="error" sx={{ fontSize: 16, verticalAlign: 'middle' }} />
<CircleIcon
color="error"
sx={{ fontSize: 16, verticalAlign: 'middle' }}
/>
)}
</Cell>
<Cell stiff>
@@ -277,7 +317,12 @@ const Scheduler: FC = () => {
<Box flexGrow={1}>
{numChanges !== 0 && (
<ButtonRow>
<Button startIcon={<CancelIcon />} variant="outlined" onClick={onDialogCancel} color="secondary">
<Button
startIcon={<CancelIcon />}
variant="outlined"
onClick={onDialogCancel}
color="secondary"
>
{LL.CANCEL()}
</Button>
<Button
@@ -293,7 +338,12 @@ const Scheduler: FC = () => {
</Box>
<Box flexWrap="nowrap" whiteSpace="nowrap">
<ButtonRow>
<Button startIcon={<AddIcon />} variant="outlined" color="secondary" onClick={addScheduleItem}>
<Button
startIcon={<AddIcon />}
variant="outlined"
color="secondary"
onClick={addScheduleItem}
>
{LL.ADD(0)}
</Button>
</ButtonRow>

View File

@@ -40,7 +40,15 @@ interface SchedulerDialogProps {
dow: string[];
}
const SchedulerDialog = ({ open, creating, onClose, onSave, selectedItem, validator, dow }: SchedulerDialogProps) => {
const SchedulerDialog = ({
open,
creating,
onClose,
onSave,
selectedItem,
validator,
dow
}: SchedulerDialogProps) => {
const { LL } = useI18nContext();
const [editItem, setEditItem] = useState<ScheduleItem>(selectedItem);
const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>();
@@ -111,8 +119,14 @@ const SchedulerDialog = ({ open, creating, onClose, onSave, selectedItem, valida
};
const showFlag = (si: ScheduleItem, flag: number) => (
<Typography variant="button" sx={{ fontSize: 10 }} color={(si.flags & flag) === flag ? 'primary' : 'grey'}>
{flag === ScheduleFlag.SCHEDULE_TIMER ? LL.TIMER(0) : dow[Math.log(flag) / Math.log(2)]}
<Typography
variant="button"
sx={{ fontSize: 10 }}
color={(si.flags & flag) === flag ? 'primary' : 'grey'}
>
{flag === ScheduleFlag.SCHEDULE_TIMER
? LL.TIMER(0)
: dow[Math.log(flag) / Math.log(2)]}
</Typography>
);
@@ -121,7 +135,8 @@ const SchedulerDialog = ({ open, creating, onClose, onSave, selectedItem, valida
return (
<Dialog sx={dialogStyle} open={open} onClose={close}>
<DialogTitle>
{creating ? LL.ADD(1) + ' ' + LL.NEW(0) : LL.EDIT()}&nbsp;{LL.SCHEDULE(1)}
{creating ? LL.ADD(1) + ' ' + LL.NEW(0) : LL.EDIT()}&nbsp;
{LL.SCHEDULE(1)}
</DialogTitle>
<DialogContent dividers>
<Box display="flex" flexWrap="wrap" mb={1}>
@@ -134,13 +149,27 @@ const SchedulerDialog = ({ open, creating, onClose, onSave, selectedItem, valida
setEditItem({ ...editItem, flags: getFlagNumber(flag) & 127 });
}}
>
<ToggleButton value="2">{showFlag(editItem, ScheduleFlag.SCHEDULE_MON)}</ToggleButton>
<ToggleButton value="4">{showFlag(editItem, ScheduleFlag.SCHEDULE_TUE)}</ToggleButton>
<ToggleButton value="8">{showFlag(editItem, ScheduleFlag.SCHEDULE_WED)}</ToggleButton>
<ToggleButton value="16">{showFlag(editItem, ScheduleFlag.SCHEDULE_THU)}</ToggleButton>
<ToggleButton value="32">{showFlag(editItem, ScheduleFlag.SCHEDULE_FRI)}</ToggleButton>
<ToggleButton value="64">{showFlag(editItem, ScheduleFlag.SCHEDULE_SAT)}</ToggleButton>
<ToggleButton value="1">{showFlag(editItem, ScheduleFlag.SCHEDULE_SUN)}</ToggleButton>
<ToggleButton value="2">
{showFlag(editItem, ScheduleFlag.SCHEDULE_MON)}
</ToggleButton>
<ToggleButton value="4">
{showFlag(editItem, ScheduleFlag.SCHEDULE_TUE)}
</ToggleButton>
<ToggleButton value="8">
{showFlag(editItem, ScheduleFlag.SCHEDULE_WED)}
</ToggleButton>
<ToggleButton value="16">
{showFlag(editItem, ScheduleFlag.SCHEDULE_THU)}
</ToggleButton>
<ToggleButton value="32">
{showFlag(editItem, ScheduleFlag.SCHEDULE_FRI)}
</ToggleButton>
<ToggleButton value="64">
{showFlag(editItem, ScheduleFlag.SCHEDULE_SAT)}
</ToggleButton>
<ToggleButton value="1">
{showFlag(editItem, ScheduleFlag.SCHEDULE_SUN)}
</ToggleButton>
</ToggleButtonGroup>
</Box>
<Box flexWrap="nowrap" whiteSpace="nowrap">
@@ -160,7 +189,10 @@ const SchedulerDialog = ({ open, creating, onClose, onSave, selectedItem, valida
size="large"
variant="outlined"
onClick={() => {
setEditItem({ ...editItem, flags: ScheduleFlag.SCHEDULE_TIMER });
setEditItem({
...editItem,
flags: ScheduleFlag.SCHEDULE_TIMER
});
}}
>
{showFlag(editItem, ScheduleFlag.SCHEDULE_TIMER)}
@@ -170,7 +202,13 @@ const SchedulerDialog = ({ open, creating, onClose, onSave, selectedItem, valida
</Box>
<Grid container>
<BlockFormControlLabel
control={<Checkbox checked={editItem.active} onChange={updateFormValue} name="active" />}
control={
<Checkbox
checked={editItem.active}
onChange={updateFormValue}
name="active"
/>
}
label={LL.ACTIVE()}
/>
</Grid>
@@ -220,15 +258,30 @@ const SchedulerDialog = ({ open, creating, onClose, onSave, selectedItem, valida
<DialogActions>
{!creating && (
<Box flexGrow={1}>
<Button startIcon={<RemoveIcon />} variant="outlined" color="warning" onClick={remove}>
<Button
startIcon={<RemoveIcon />}
variant="outlined"
color="warning"
onClick={remove}
>
{LL.REMOVE()}
</Button>
</Box>
)}
<Button startIcon={<CancelIcon />} variant="outlined" onClick={close} color="secondary">
<Button
startIcon={<CancelIcon />}
variant="outlined"
onClick={close}
color="secondary"
>
{LL.CANCEL()}
</Button>
<Button startIcon={creating ? <AddIcon /> : <DoneIcon />} variant="outlined" onClick={save} color="primary">
<Button
startIcon={creating ? <AddIcon /> : <DoneIcon />}
variant="outlined"
onClick={save}
color="primary"
>
{creating ? LL.ADD(0) : LL.UPDATE()}
</Button>
</DialogActions>

View File

@@ -10,7 +10,15 @@ import UnfoldMoreOutlinedIcon from '@mui/icons-material/UnfoldMoreOutlined';
import { Box, Button, Typography } from '@mui/material';
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 {
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 { State } from '@table-library/react-table-library/types/common';
import { useRequest } from 'alova';
@@ -21,28 +29,45 @@ import { useI18nContext } from 'i18n/i18n-react';
import * as EMSESP from './api';
import DashboardSensorsAnalogDialog from './SensorsAnalogDialog';
import DashboardSensorsTemperatureDialog from './SensorsTemperatureDialog';
import { AnalogType, AnalogTypeNames, DeviceValueUOM, DeviceValueUOM_s } from './types';
import type { AnalogSensor, TemperatureSensor, WriteAnalogSensor, WriteTemperatureSensor } from './types';
import { analogSensorItemValidation, temperatureSensorItemValidation } from './validators';
import {
AnalogType,
AnalogTypeNames,
DeviceValueUOM,
DeviceValueUOM_s
} from './types';
import type {
AnalogSensor,
TemperatureSensor,
WriteAnalogSensor,
WriteTemperatureSensor
} from './types';
import {
analogSensorItemValidation,
temperatureSensorItemValidation
} from './validators';
const Sensors: FC = () => {
const { LL } = useI18nContext();
const { me } = useContext(AuthenticatedContext);
const [selectedTemperatureSensor, setSelectedTemperatureSensor] = useState<TemperatureSensor>();
const [selectedTemperatureSensor, setSelectedTemperatureSensor] =
useState<TemperatureSensor>();
const [selectedAnalogSensor, setSelectedAnalogSensor] = useState<AnalogSensor>();
const [temperatureDialogOpen, setTemperatureDialogOpen] = useState<boolean>(false);
const [analogDialogOpen, setAnalogDialogOpen] = useState<boolean>(false);
const [creating, setCreating] = useState<boolean>(false);
const { data: sensorData, send: fetchSensorData } = useRequest(() => EMSESP.readSensorData(), {
initialData: {
ts: [],
as: [],
analog_enabled: false,
platform: 'ESP32'
const { data: sensorData, send: fetchSensorData } = useRequest(
() => EMSESP.readSensorData(),
{
initialData: {
ts: [],
as: [],
analog_enabled: false,
platform: 'ESP32'
}
}
});
);
const { send: writeTemperatureSensor } = useRequest(
(data: WriteTemperatureSensor) => EMSESP.writeTemperatureSensor(data),
@@ -51,9 +76,12 @@ const Sensors: FC = () => {
}
);
const { send: writeAnalogSensor } = useRequest((data: WriteAnalogSensor) => EMSESP.writeAnalogSensor(data), {
immediate: false
});
const { send: writeAnalogSensor } = useRequest(
(data: WriteAnalogSensor) => EMSESP.writeAnalogSensor(data),
{
immediate: false
}
);
const common_theme = useTheme({
BaseRow: `
@@ -304,7 +332,12 @@ const Sensors: FC = () => {
};
const RenderTemperatureSensors = () => (
<Table data={{ nodes: sensorData.ts }} theme={temperature_theme} sort={temperature_sort} layout={{ custom: true }}>
<Table
data={{ nodes: sensorData.ts }}
theme={temperature_theme}
sort={temperature_sort}
layout={{ custom: true }}
>
{(tableList: TemperatureSensor[]) => (
<>
<Header>
@@ -314,7 +347,9 @@ const Sensors: FC = () => {
fullWidth
style={{ fontSize: '14px', justifyContent: 'flex-start' }}
endIcon={getSortIcon(temperature_sort.state, 'NAME')}
onClick={() => temperature_sort.fns.onToggleSort({ sortKey: 'NAME' })}
onClick={() =>
temperature_sort.fns.onToggleSort({ sortKey: 'NAME' })
}
>
{LL.NAME(0)}
</Button>
@@ -324,7 +359,9 @@ const Sensors: FC = () => {
fullWidth
style={{ fontSize: '14px', justifyContent: 'flex-end' }}
endIcon={getSortIcon(temperature_sort.state, 'VALUE')}
onClick={() => temperature_sort.fns.onToggleSort({ sortKey: 'VALUE' })}
onClick={() =>
temperature_sort.fns.onToggleSort({ sortKey: 'VALUE' })
}
>
{LL.VALUE(0)}
</Button>
@@ -345,7 +382,12 @@ const Sensors: FC = () => {
);
const RenderAnalogSensors = () => (
<Table data={{ nodes: sensorData.as }} theme={analog_theme} sort={analog_sort} layout={{ custom: true }}>
<Table
data={{ nodes: sensorData.as }}
theme={analog_theme}
sort={analog_sort}
layout={{ custom: true }}
>
{(tableList: AnalogSensor[]) => (
<>
<Header>
@@ -439,7 +481,11 @@ const Sensors: FC = () => {
onSave={onAnalogDialogSave}
creating={creating}
selectedItem={selectedAnalogSensor}
validator={analogSensorItemValidation(sensorData.as, creating, sensorData.platform)}
validator={analogSensorItemValidation(
sensorData.as,
creating,
sensorData.platform
)}
/>
)}
</>
@@ -447,7 +493,12 @@ const Sensors: FC = () => {
<ButtonRow>
<Box mt={1} display="flex" flexWrap="wrap">
<Box flexGrow={1}>
<Button startIcon={<RefreshIcon />} variant="outlined" color="secondary" onClick={fetchSensorData}>
<Button
startIcon={<RefreshIcon />}
variant="outlined"
color="secondary"
onClick={fetchSensorData}
>
{LL.REFRESH()}
</Button>
</Box>

View File

@@ -79,7 +79,8 @@ const SensorsAnalogDialog = ({
return (
<Dialog sx={dialogStyle} open={open} onClose={close}>
<DialogTitle>
{creating ? LL.ADD(1) + ' ' + LL.NEW(0) : LL.EDIT()}&nbsp;{LL.ANALOG_SENSOR(0)}
{creating ? LL.ADD(1) + ' ' + LL.NEW(0) : LL.EDIT()}&nbsp;
{LL.ANALOG_SENSOR(0)}
</DialogTitle>
<DialogContent dividers>
<Grid container spacing={2}>
@@ -113,7 +114,14 @@ const SensorsAnalogDialog = ({
/>
</Grid>
<Grid item xs={8}>
<TextField name="t" label={LL.TYPE(0)} value={editItem.t} fullWidth select onChange={updateFormValue}>
<TextField
name="t"
label={LL.TYPE(0)}
value={editItem.t}
fullWidth
select
onChange={updateFormValue}
>
{AnalogTypeNames.map((val, i) => (
<MenuItem key={i} value={i}>
{val}
@@ -123,7 +131,14 @@ const SensorsAnalogDialog = ({
</Grid>
{editItem.t >= AnalogType.COUNTER && editItem.t <= AnalogType.RATE && (
<Grid item xs={4}>
<TextField name="u" label={LL.UNIT()} value={editItem.u} fullWidth select onChange={updateFormValue}>
<TextField
name="u"
label={LL.UNIT()}
value={editItem.u}
fullWidth
select
onChange={updateFormValue}
>
{DeviceValueUOM_s.map((val, i) => (
<MenuItem key={i} value={i}>
{val}
@@ -144,7 +159,9 @@ const SensorsAnalogDialog = ({
onChange={updateFormValue}
inputProps={{ min: '0', max: '3300', step: '1' }}
InputProps={{
startAdornment: <InputAdornment position="start">mV</InputAdornment>
startAdornment: (
<InputAdornment position="start">mV</InputAdornment>
)
}}
/>
</Grid>
@@ -177,70 +194,75 @@ const SensorsAnalogDialog = ({
/>
</Grid>
)}
{editItem.t === AnalogType.DIGITAL_OUT && (editItem.g === 25 || editItem.g === 26) && (
<Grid item xs={4}>
<TextField
name="o"
label={LL.VALUE(1)}
value={numberValue(editItem.o)}
fullWidth
type="number"
variant="outlined"
onChange={updateFormValue}
inputProps={{ min: '0', max: '255', step: '1' }}
/>
</Grid>
)}
{editItem.t === AnalogType.DIGITAL_OUT && editItem.g !== 25 && editItem.g !== 26 && (
<>
{editItem.t === AnalogType.DIGITAL_OUT &&
(editItem.g === 25 || editItem.g === 26) && (
<Grid item xs={4}>
<TextField
name="o"
label={LL.VALUE(1)}
value={numberValue(editItem.o)}
fullWidth
select
type="number"
variant="outlined"
onChange={updateFormValue}
>
<MenuItem value={0}>{LL.OFF()}</MenuItem>
<MenuItem value={1}>{LL.ON()}</MenuItem>
</TextField>
inputProps={{ min: '0', max: '255', step: '1' }}
/>
</Grid>
<Grid item xs={4}>
<TextField
name="f"
label={LL.POLARITY()}
value={editItem.f}
fullWidth
select
onChange={updateFormValue}
>
<MenuItem value={1}>{LL.ACTIVEHIGH()}</MenuItem>
<MenuItem value={-1}>{LL.ACTIVELOW()}</MenuItem>
</TextField>
</Grid>
<Grid item xs={4}>
<TextField
name="u"
label={LL.STARTVALUE()}
value={editItem.u}
fullWidth
select
onChange={updateFormValue}
>
<MenuItem value={0}>{LL.UNCHANGED()}</MenuItem>
<MenuItem value={1}>
{LL.ALWAYS()}&nbsp;{LL.OFF()}
</MenuItem>
<MenuItem value={2}>
{LL.ALWAYS()}&nbsp;{LL.ON()}
</MenuItem>
</TextField>
</Grid>
</>
)}
{(editItem.t === AnalogType.PWM_0 || editItem.t === AnalogType.PWM_1 || editItem.t === AnalogType.PWM_2) && (
)}
{editItem.t === AnalogType.DIGITAL_OUT &&
editItem.g !== 25 &&
editItem.g !== 26 && (
<>
<Grid item xs={4}>
<TextField
name="o"
label={LL.VALUE(1)}
value={numberValue(editItem.o)}
fullWidth
select
variant="outlined"
onChange={updateFormValue}
>
<MenuItem value={0}>{LL.OFF()}</MenuItem>
<MenuItem value={1}>{LL.ON()}</MenuItem>
</TextField>
</Grid>
<Grid item xs={4}>
<TextField
name="f"
label={LL.POLARITY()}
value={editItem.f}
fullWidth
select
onChange={updateFormValue}
>
<MenuItem value={1}>{LL.ACTIVEHIGH()}</MenuItem>
<MenuItem value={-1}>{LL.ACTIVELOW()}</MenuItem>
</TextField>
</Grid>
<Grid item xs={4}>
<TextField
name="u"
label={LL.STARTVALUE()}
value={editItem.u}
fullWidth
select
onChange={updateFormValue}
>
<MenuItem value={0}>{LL.UNCHANGED()}</MenuItem>
<MenuItem value={1}>
{LL.ALWAYS()}&nbsp;{LL.OFF()}
</MenuItem>
<MenuItem value={2}>
{LL.ALWAYS()}&nbsp;{LL.ON()}
</MenuItem>
</TextField>
</Grid>
</>
)}
{(editItem.t === AnalogType.PWM_0 ||
editItem.t === AnalogType.PWM_1 ||
editItem.t === AnalogType.PWM_2) && (
<>
<Grid item xs={4}>
<TextField
@@ -253,7 +275,9 @@ const SensorsAnalogDialog = ({
onChange={updateFormValue}
inputProps={{ min: '1', max: '5000', step: '1' }}
InputProps={{
startAdornment: <InputAdornment position="start">Hz</InputAdornment>
startAdornment: (
<InputAdornment position="start">Hz</InputAdornment>
)
}}
/>
</Grid>
@@ -268,7 +292,9 @@ const SensorsAnalogDialog = ({
onChange={updateFormValue}
inputProps={{ min: '0', max: '100', step: '0.1' }}
InputProps={{
startAdornment: <InputAdornment position="start">%</InputAdornment>
startAdornment: (
<InputAdornment position="start">%</InputAdornment>
)
}}
/>
</Grid>
@@ -279,15 +305,30 @@ const SensorsAnalogDialog = ({
<DialogActions>
{!creating && (
<Box flexGrow={1} sx={{ '& button': { mt: 0 } }}>
<Button startIcon={<RemoveIcon />} variant="outlined" color="error" onClick={remove}>
<Button
startIcon={<RemoveIcon />}
variant="outlined"
color="error"
onClick={remove}
>
{LL.REMOVE()}
</Button>
</Box>
)}
<Button startIcon={<CancelIcon />} variant="outlined" onClick={close} color="secondary">
<Button
startIcon={<CancelIcon />}
variant="outlined"
onClick={close}
color="secondary"
>
{LL.CANCEL()}
</Button>
<Button startIcon={<WarningIcon color="warning" />} variant="contained" onClick={save} color="info">
<Button
startIcon={<WarningIcon color="warning" />}
variant="contained"
onClick={save}
color="info"
>
{creating ? LL.ADD(0) : LL.UPDATE()}
</Button>
</DialogActions>

View File

@@ -107,10 +107,20 @@ const SensorsTemperatureDialog = ({
</Grid>
</DialogContent>
<DialogActions>
<Button startIcon={<CancelIcon />} variant="outlined" onClick={close} color="secondary">
<Button
startIcon={<CancelIcon />}
variant="outlined"
onClick={close}
color="secondary"
>
{LL.CANCEL()}
</Button>
<Button startIcon={<WarningIcon color="warning" />} variant="contained" onClick={save} color="info">
<Button
startIcon={<WarningIcon color="warning" />}
variant="contained"
onClick={save}
color="info"
>
{LL.UPDATE()}
</Button>
</DialogActions>

View File

@@ -4,7 +4,15 @@ import type { FC } from 'react';
import RefreshIcon from '@mui/icons-material/Refresh';
import { Button } from '@mui/material';
import { Body, Cell, Header, HeaderCell, HeaderRow, Row, Table } from '@table-library/react-table-library/table';
import {
Body,
Cell,
Header,
HeaderCell,
HeaderRow,
Row,
Table
} from '@table-library/react-table-library/table';
import { useTheme as tableTheme } from '@table-library/react-table-library/theme';
import { useRequest } from 'alova';
import { ButtonRow, FormLoader, SectionContent, useLayoutTitle } from 'components';
@@ -93,7 +101,11 @@ const SystemActivity: FC = () => {
return (
<>
<Table data={{ nodes: data.stats }} theme={stats_theme} layout={{ custom: true }}>
<Table
data={{ nodes: data.stats }}
theme={stats_theme}
layout={{ custom: true }}
>
{(tableList: Stat[]) => (
<>
<Header>
@@ -118,7 +130,12 @@ const SystemActivity: FC = () => {
)}
</Table>
<ButtonRow mt={1}>
<Button startIcon={<RefreshIcon />} variant="outlined" color="secondary" onClick={loadData}>
<Button
startIcon={<RefreshIcon />}
variant="outlined"
color="secondary"
onClick={loadData}
>
{LL.REFRESH()}
</Button>
</ButtonRow>

View File

@@ -30,17 +30,20 @@ export const writeDeviceValue = (data: { id: number; c: string; v: unknown }) =>
// Application Settings
export const readSettings = () => alovaInstance.Get<Settings>('/rest/settings');
export const writeSettings = (data: Settings) => alovaInstance.Post('/rest/settings', data);
export const writeSettings = (data: Settings) =>
alovaInstance.Post('/rest/settings', data);
export const getBoardProfile = (boardProfile: string) =>
alovaInstance.Get('/rest/boardProfile', {
params: { boardProfile }
});
// Sensors
export const readSensorData = () => alovaInstance.Get<SensorData>('/rest/sensorData');
export const readSensorData = () =>
alovaInstance.Get<SensorData>('/rest/sensorData');
export const writeTemperatureSensor = (ts: WriteTemperatureSensor) =>
alovaInstance.Post('/rest/writeTemperatureSensor', ts);
export const writeAnalogSensor = (as: WriteAnalogSensor) => alovaInstance.Post('/rest/writeAnalogSensor', as);
export const writeAnalogSensor = (as: WriteAnalogSensor) =>
alovaInstance.Post('/rest/writeAnalogSensor', as);
// Activity
export const readActivity = () => alovaInstance.Get<Activity>('/rest/activity');
@@ -73,9 +76,12 @@ export const readDeviceEntities = (id: number) =>
}
});
export const readDevices = () => alovaInstance.Get<Devices>('/rest/devices');
export const resetCustomizations = () => alovaInstance.Post('/rest/resetCustomizations');
export const writeCustomizationEntities = (data: { id: number; entity_ids: string[] }) =>
alovaInstance.Post('/rest/customizationEntities', data);
export const resetCustomizations = () =>
alovaInstance.Post('/rest/resetCustomizations');
export const writeCustomizationEntities = (data: {
id: number;
entity_ids: string[];
}) => alovaInstance.Post('/rest/customizationEntities', data);
// SettingsScheduler
export const readSchedule = () =>
@@ -95,7 +101,8 @@ export const readSchedule = () =>
}));
}
});
export const writeSchedule = (data: Schedule) => alovaInstance.Post('/rest/schedule', data);
export const writeSchedule = (data: Schedule) =>
alovaInstance.Post('/rest/schedule', data);
// SettingsEntities
export const readCustomEntities = () =>

View File

@@ -25,7 +25,11 @@ const formatDurationMin = (LL: TranslationFunctions, duration_min: number) => {
return formatted;
};
export function formatValue(LL: TranslationFunctions, value: unknown, uom: DeviceValueUOM) {
export function formatValue(
LL: TranslationFunctions,
value: unknown,
uom: DeviceValueUOM
) {
if (typeof value !== 'number') {
return '';
}

View File

@@ -5,7 +5,11 @@ import { IP_OR_HOSTNAME_VALIDATOR } from 'validators/shared';
import type { AnalogSensor, DeviceValue, ScheduleItem, Settings } from './types';
export const GPIO_VALIDATOR = {
validator(rule: InternalRuleItem, value: number, callback: (error?: string) => void) {
validator(
rule: InternalRuleItem,
value: number,
callback: (error?: string) => void
) {
if (
value &&
(value === 1 ||
@@ -24,7 +28,11 @@ export const GPIO_VALIDATOR = {
};
export const GPIO_VALIDATORR = {
validator(rule: InternalRuleItem, value: number, callback: (error?: string) => void) {
validator(
rule: InternalRuleItem,
value: number,
callback: (error?: string) => void
) {
if (
value &&
(value === 1 ||
@@ -44,7 +52,11 @@ export const GPIO_VALIDATORR = {
};
export const GPIO_VALIDATORC3 = {
validator(rule: InternalRuleItem, value: number, callback: (error?: string) => void) {
validator(
rule: InternalRuleItem,
value: number,
callback: (error?: string) => void
) {
if (value && ((value >= 11 && value <= 19) || value > 21 || value < 0)) {
callback('Must be an valid GPIO port');
} else {
@@ -54,8 +66,18 @@ export const GPIO_VALIDATORC3 = {
};
export const GPIO_VALIDATORS2 = {
validator(rule: InternalRuleItem, value: number, callback: (error?: string) => void) {
if (value && ((value >= 19 && value <= 20) || (value >= 22 && value <= 32) || value > 40 || value < 0)) {
validator(
rule: InternalRuleItem,
value: number,
callback: (error?: string) => void
) {
if (
value &&
((value >= 19 && value <= 20) ||
(value >= 22 && value <= 32) ||
value > 40 ||
value < 0)
) {
callback('Must be an valid GPIO port');
} else {
callback();
@@ -64,7 +86,11 @@ export const GPIO_VALIDATORS2 = {
};
export const GPIO_VALIDATORS3 = {
validator(rule: InternalRuleItem, value: number, callback: (error?: string) => void) {
validator(
rule: InternalRuleItem,
value: number,
callback: (error?: string) => void
) {
if (
value &&
((value >= 19 && value <= 20) ||
@@ -84,46 +110,121 @@ export const createSettingsValidator = (settings: Settings) =>
new Schema({
...(settings.board_profile === 'CUSTOM' &&
settings.platform === 'ESP32' && {
led_gpio: [{ required: true, message: 'LED GPIO is required' }, GPIO_VALIDATOR],
dallas_gpio: [{ required: true, message: 'GPIO is required' }, GPIO_VALIDATOR],
pbutton_gpio: [{ required: true, message: 'Button GPIO is required' }, GPIO_VALIDATOR],
tx_gpio: [{ required: true, message: 'Tx GPIO is required' }, GPIO_VALIDATOR],
led_gpio: [
{ required: true, message: 'LED GPIO is required' },
GPIO_VALIDATOR
],
dallas_gpio: [
{ required: true, message: 'GPIO is required' },
GPIO_VALIDATOR
],
pbutton_gpio: [
{ required: true, message: 'Button GPIO is required' },
GPIO_VALIDATOR
],
tx_gpio: [
{ required: true, message: 'Tx GPIO is required' },
GPIO_VALIDATOR
],
rx_gpio: [{ required: true, message: 'Rx GPIO is required' }, GPIO_VALIDATOR]
}),
...(settings.board_profile === 'CUSTOM' &&
settings.platform === 'ESP32R' && {
led_gpio: [{ required: true, message: 'LED GPIO is required' }, GPIO_VALIDATORR],
dallas_gpio: [{ required: true, message: 'GPIO is required' }, GPIO_VALIDATORR],
pbutton_gpio: [{ required: true, message: 'Button GPIO is required' }, GPIO_VALIDATORR],
tx_gpio: [{ required: true, message: 'Tx GPIO is required' }, GPIO_VALIDATORR],
rx_gpio: [{ required: true, message: 'Rx GPIO is required' }, GPIO_VALIDATORR]
led_gpio: [
{ required: true, message: 'LED GPIO is required' },
GPIO_VALIDATORR
],
dallas_gpio: [
{ required: true, message: 'GPIO is required' },
GPIO_VALIDATORR
],
pbutton_gpio: [
{ required: true, message: 'Button GPIO is required' },
GPIO_VALIDATORR
],
tx_gpio: [
{ required: true, message: 'Tx GPIO is required' },
GPIO_VALIDATORR
],
rx_gpio: [
{ required: true, message: 'Rx GPIO is required' },
GPIO_VALIDATORR
]
}),
...(settings.board_profile === 'CUSTOM' &&
settings.platform === 'ESP32-C3' && {
led_gpio: [{ required: true, message: 'LED GPIO is required' }, GPIO_VALIDATORC3],
dallas_gpio: [{ required: true, message: 'GPIO is required' }, GPIO_VALIDATORC3],
pbutton_gpio: [{ required: true, message: 'Button GPIO is required' }, GPIO_VALIDATORC3],
tx_gpio: [{ required: true, message: 'Tx GPIO is required' }, GPIO_VALIDATORC3],
rx_gpio: [{ required: true, message: 'Rx GPIO is required' }, GPIO_VALIDATORC3]
led_gpio: [
{ required: true, message: 'LED GPIO is required' },
GPIO_VALIDATORC3
],
dallas_gpio: [
{ required: true, message: 'GPIO is required' },
GPIO_VALIDATORC3
],
pbutton_gpio: [
{ required: true, message: 'Button GPIO is required' },
GPIO_VALIDATORC3
],
tx_gpio: [
{ required: true, message: 'Tx GPIO is required' },
GPIO_VALIDATORC3
],
rx_gpio: [
{ required: true, message: 'Rx GPIO is required' },
GPIO_VALIDATORC3
]
}),
...(settings.board_profile === 'CUSTOM' &&
settings.platform === 'ESP32-S2' && {
led_gpio: [{ required: true, message: 'LED GPIO is required' }, GPIO_VALIDATORS2],
dallas_gpio: [{ required: true, message: 'GPIO is required' }, GPIO_VALIDATORS2],
pbutton_gpio: [{ required: true, message: 'Button GPIO is required' }, GPIO_VALIDATORS2],
tx_gpio: [{ required: true, message: 'Tx GPIO is required' }, GPIO_VALIDATORS2],
rx_gpio: [{ required: true, message: 'Rx GPIO is required' }, GPIO_VALIDATORS2]
led_gpio: [
{ required: true, message: 'LED GPIO is required' },
GPIO_VALIDATORS2
],
dallas_gpio: [
{ required: true, message: 'GPIO is required' },
GPIO_VALIDATORS2
],
pbutton_gpio: [
{ required: true, message: 'Button GPIO is required' },
GPIO_VALIDATORS2
],
tx_gpio: [
{ required: true, message: 'Tx GPIO is required' },
GPIO_VALIDATORS2
],
rx_gpio: [
{ required: true, message: 'Rx GPIO is required' },
GPIO_VALIDATORS2
]
}),
...(settings.board_profile === 'CUSTOM' &&
settings.platform === 'ESP32-S3' && {
led_gpio: [{ required: true, message: 'LED GPIO is required' }, GPIO_VALIDATORS3],
dallas_gpio: [{ required: true, message: 'GPIO is required' }, GPIO_VALIDATORS3],
pbutton_gpio: [{ required: true, message: 'Button GPIO is required' }, GPIO_VALIDATORS3],
tx_gpio: [{ required: true, message: 'Tx GPIO is required' }, GPIO_VALIDATORS3],
rx_gpio: [{ required: true, message: 'Rx GPIO is required' }, GPIO_VALIDATORS3]
led_gpio: [
{ required: true, message: 'LED GPIO is required' },
GPIO_VALIDATORS3
],
dallas_gpio: [
{ required: true, message: 'GPIO is required' },
GPIO_VALIDATORS3
],
pbutton_gpio: [
{ required: true, message: 'Button GPIO is required' },
GPIO_VALIDATORS3
],
tx_gpio: [
{ required: true, message: 'Tx GPIO is required' },
GPIO_VALIDATORS3
],
rx_gpio: [
{ required: true, message: 'Rx GPIO is required' },
GPIO_VALIDATORS3
]
}),
...(settings.syslog_enabled && {
syslog_host: [{ required: true, message: 'Host is required' }, IP_OR_HOSTNAME_VALIDATOR],
syslog_host: [
{ required: true, message: 'Host is required' },
IP_OR_HOSTNAME_VALIDATOR
],
syslog_port: [
{ required: true, message: 'Port is required' },
{ type: 'number', min: 0, max: 65535, message: 'Invalid Port' }
@@ -134,14 +235,35 @@ export const createSettingsValidator = (settings: Settings) =>
]
}),
...(settings.shower_alert && {
shower_alert_trigger: [{ type: 'number', min: 1, max: 20, message: 'Time must be between 1 and 20 minutes' }],
shower_alert_coldshot: [{ type: 'number', min: 1, max: 10, message: 'Time must be between 1 and 10 seconds' }]
shower_alert_trigger: [
{
type: 'number',
min: 1,
max: 20,
message: 'Time must be between 1 and 20 minutes'
}
],
shower_alert_coldshot: [
{
type: 'number',
min: 1,
max: 10,
message: 'Time must be between 1 and 10 seconds'
}
]
})
});
export const uniqueNameValidator = (schedule: ScheduleItem[], o_name?: string) => ({
validator(rule: InternalRuleItem, name: string, callback: (error?: string) => void) {
if ((o_name === undefined || o_name !== name) && schedule.find((si) => si.name === name)) {
validator(
rule: InternalRuleItem,
name: string,
callback: (error?: string) => void
) {
if (
(o_name === undefined || o_name !== name) &&
schedule.find((si) => si.name === name)
) {
callback('Name already in use');
} else {
callback();
@@ -149,7 +271,10 @@ export const uniqueNameValidator = (schedule: ScheduleItem[], o_name?: string) =
}
});
export const schedulerItemValidation = (schedule: ScheduleItem[], scheduleItem: ScheduleItem) =>
export const schedulerItemValidation = (
schedule: ScheduleItem[],
scheduleItem: ScheduleItem
) =>
new Schema({
name: [
{
@@ -162,7 +287,12 @@ export const schedulerItemValidation = (schedule: ScheduleItem[], scheduleItem:
],
cmd: [
{ required: true, message: 'Command is required' },
{ type: 'string', min: 1, max: 64, message: 'Command must be 1-64 characters' }
{
type: 'string',
min: 1,
max: 64,
message: 'Command must be 1-64 characters'
}
]
});
@@ -178,7 +308,11 @@ export const entityItemValidation = () =>
],
device_id: [
{
validator(rule: InternalRuleItem, value: string, callback: (error?: string) => void) {
validator(
rule: InternalRuleItem,
value: string,
callback: (error?: string) => void
) {
if (isNaN(parseInt(value, 16))) {
callback('Is required and must be in hex format');
}
@@ -188,7 +322,11 @@ export const entityItemValidation = () =>
],
type_id: [
{
validator(rule: InternalRuleItem, value: string, callback: (error?: string) => void) {
validator(
rule: InternalRuleItem,
value: string,
callback: (error?: string) => void
) {
if (isNaN(parseInt(value, 16))) {
callback('Is required and must be in hex format');
}
@@ -208,7 +346,11 @@ export const temperatureSensorItemValidation = () =>
});
export const isGPIOUniqueValidator = (sensors: AnalogSensor[]) => ({
validator(rule: InternalRuleItem, gpio: number, callback: (error?: string) => void) {
validator(
rule: InternalRuleItem,
gpio: number,
callback: (error?: string) => void
) {
if (sensors.find((as) => as.g === gpio)) {
callback('GPIO already in use');
} else {
@@ -217,7 +359,11 @@ export const isGPIOUniqueValidator = (sensors: AnalogSensor[]) => ({
}
});
export const analogSensorItemValidation = (sensors: AnalogSensor[], creating: boolean, platform: string) =>
export const analogSensorItemValidation = (
sensors: AnalogSensor[],
creating: boolean,
platform: string
) =>
new Schema({
n: [{ required: true, message: 'Name is required' }],
g: [
@@ -240,8 +386,17 @@ export const deviceValueItemValidation = (dv: DeviceValue) =>
v: [
{ required: true, message: 'Value is required' },
{
validator(rule: InternalRuleItem, value: unknown, callback: (error?: string) => void) {
if (typeof value === 'number' && dv.m && dv.x && (value < dv.m || value > dv.x)) {
validator(
rule: InternalRuleItem,
value: unknown,
callback: (error?: string) => void
) {
if (
typeof value === 'number' &&
dv.m &&
dv.x &&
(value < dv.m || value > dv.x)
) {
callback('Value out of range');
}
callback();