This commit is contained in:
Proddy
2023-02-25 14:06:40 +01:00
parent 45609e30ea
commit 2ede730cfb
15 changed files with 719 additions and 66 deletions

View File

@@ -3,10 +3,10 @@ import { Link, useLocation } from 'react-router-dom';
import { ListItem, ListItemButton, ListItemIcon, ListItemText, SvgIconProps } from '@mui/material'; import { ListItem, ListItemButton, ListItemIcon, ListItemText, SvgIconProps } from '@mui/material';
import { grey } from '@mui/material/colors';
import { routeMatches } from 'utils'; import { routeMatches } from 'utils';
import { grey } from '@mui/material/colors';
interface LayoutMenuItemProps { interface LayoutMenuItemProps {
icon: React.ComponentType<SvgIconProps>; icon: React.ComponentType<SvgIconProps>;
label: string; label: string;
@@ -17,13 +17,15 @@ interface LayoutMenuItemProps {
const LayoutMenuItem: FC<LayoutMenuItemProps> = ({ icon: Icon, label, to, disabled }) => { const LayoutMenuItem: FC<LayoutMenuItemProps> = ({ icon: Icon, label, to, disabled }) => {
const { pathname } = useLocation(); const { pathname } = useLocation();
const selected = routeMatches(to, pathname);
return ( return (
<ListItem disablePadding selected={routeMatches(to, pathname)}> <ListItem disablePadding>
<ListItemButton component={Link} to={to} disabled={disabled}> <ListItemButton component={Link} to={to} disabled={disabled} selected={selected}>
<ListItemIcon sx={{ color: grey[500] }}> <ListItemIcon sx={{ color: selected ? '#90caf9' : grey[500] }}>
<Icon /> <Icon />
</ListItemIcon> </ListItemIcon>
<ListItemText>{label}</ListItemText> <ListItemText sx={{ color: selected ? '#90caf9' : grey[100] }}>{label}</ListItemText>
</ListItemButton> </ListItemButton>
</ListItem> </ListItem>
); );

View File

@@ -307,7 +307,15 @@ const de: Translation = {
BLOCK_NAVIGATE_1: 'You have unsaved changes', // TODO translate BLOCK_NAVIGATE_1: 'You have unsaved changes', // TODO translate
BLOCK_NAVIGATE_2: 'If you navigate to a different page, your unsaved changes will be lost. Are you sure you want to leave this page?', // TODO translate BLOCK_NAVIGATE_2: 'If you navigate to a different page, your unsaved changes will be lost. Are you sure you want to leave this page?', // TODO translate
STAY: 'Stay', // TODO translate STAY: 'Stay', // TODO translate
LEAVE: 'Leave' // TODO translate LEAVE: 'Leave', // TODO translate
SCHEDULER: 'Scheduler', // TODO translate
SCHEDULER_HELP_1: 'Add custom scheduled commands to automate EMS-ESP.', // TODO translate
SCHEDULER_HELP_2: 'The NTP service needs to be active if using the calendar.', // TODO translate
SCHEDULE: 'Schedule', // TODO translate
TIME: 'Time', // TODO translate
TIMER: 'Timer', // TODO translate
WEEKLY: 'Weekly', // TODO translate
SCHEDULE_SAVED: 'Schedule updated' // TODO translate
}; };
export default de; export default de;

View File

@@ -307,7 +307,15 @@ const en: Translation = {
BLOCK_NAVIGATE_1: 'You have unsaved changes', BLOCK_NAVIGATE_1: 'You have unsaved changes',
BLOCK_NAVIGATE_2: 'If you navigate to a different page, your unsaved changes will be lost. Are you sure you want to leave this page?', BLOCK_NAVIGATE_2: 'If you navigate to a different page, your unsaved changes will be lost. Are you sure you want to leave this page?',
STAY: 'Stay', STAY: 'Stay',
LEAVE: 'Leave' LEAVE: 'Leave',
SCHEDULER: 'Scheduler',
SCHEDULER_HELP_1: 'Add custom scheduled commands to automate EMS-ESP.',
SCHEDULER_HELP_2: 'The NTP service needs to be active if using the calendar.',
SCHEDULE: 'Schedule',
TIME: 'Time',
TIMER: 'Timer',
WEEKLY: 'Weekly',
SCHEDULE_SAVED: 'Schedule updated'
}; };
export default en; export default en;

View File

@@ -307,7 +307,15 @@ const fr: Translation = {
BLOCK_NAVIGATE_1: 'You have unsaved changes', // TODO translate BLOCK_NAVIGATE_1: 'You have unsaved changes', // TODO translate
BLOCK_NAVIGATE_2: 'If you navigate to a different page, your unsaved changes will be lost. Are you sure you want to leave this page?', // TODO translate BLOCK_NAVIGATE_2: 'If you navigate to a different page, your unsaved changes will be lost. Are you sure you want to leave this page?', // TODO translate
STAY: 'Stay', // TODO translate STAY: 'Stay', // TODO translate
LEAVE: 'Leave' // TODO translate LEAVE: 'Leave', // TODO translate
SCHEDULER: 'Scheduler', // TODO translate
SCHEDULER_HELP_1: 'Add custom scheduled commands to automate EMS-ESP.', // TODO translate
SCHEDULER_HELP_2: 'The NTP service needs to be active if using the calendar.', // TODO translate
SCHEDULE: 'Schedule', // TODO translate
TIME: 'Time', // TODO translate
TIMER: 'Timer', // TODO translate
WEEKLY: 'Weekly', // TODO translate
SCHEDULE_SAVED: 'Schedule updated' // TODO translate
}; };
export default fr; export default fr;

View File

@@ -307,7 +307,15 @@ const nl: Translation = {
BLOCK_NAVIGATE_1: 'You have unsaved changes', // TODO translate BLOCK_NAVIGATE_1: 'You have unsaved changes', // TODO translate
BLOCK_NAVIGATE_2: 'If you navigate to a different page, your unsaved changes will be lost. Are you sure you want to leave this page?', // TODO translate BLOCK_NAVIGATE_2: 'If you navigate to a different page, your unsaved changes will be lost. Are you sure you want to leave this page?', // TODO translate
STAY: 'Stay', // TODO translate STAY: 'Stay', // TODO translate
LEAVE: 'Leave' // TODO translate LEAVE: 'Leave', // TODO translate
SCHEDULER: 'Scheduler', // TODO translate
SCHEDULER_HELP_1: 'Add custom scheduled commands to automate EMS-ESP.', // TODO translate
SCHEDULER_HELP_2: 'The NTP service needs to be active if using the calendar.', // TODO translate
SCHEDULE: 'Schedule', // TODO translate
TIME: 'Time', // TODO translate
TIMER: 'Timer', // TODO translate
WEEKLY: 'Weekly', // TODO translate
SCHEDULE_SAVED: 'Schedule updated' // TODO translate
}; };
export default nl; export default nl;

View File

@@ -307,7 +307,15 @@ const no: Translation = {
BLOCK_NAVIGATE_1: 'You have unsaved changes', // TODO translate BLOCK_NAVIGATE_1: 'You have unsaved changes', // TODO translate
BLOCK_NAVIGATE_2: 'If you navigate to a different page, your unsaved changes will be lost. Are you sure you want to leave this page?', // TODO translate BLOCK_NAVIGATE_2: 'If you navigate to a different page, your unsaved changes will be lost. Are you sure you want to leave this page?', // TODO translate
STAY: 'Stay', // TODO translate STAY: 'Stay', // TODO translate
LEAVE: 'Leave' // TODO translate LEAVE: 'Leave', // TODO translate
SCHEDULER: 'Scheduler', // TODO translate
SCHEDULER_HELP_1: 'Add custom scheduled commands to automate EMS-ESP.', // TODO translate
SCHEDULER_HELP_2: 'The NTP service needs to be active if using the calendar.', // TODO translate
SCHEDULE: 'Schedule', // TODO translate
TIME: 'Time', // TODO translate
TIMER: 'Timer', // TODO translate
WEEKLY: 'Weekly', // TODO translate
SCHEDULE_SAVED: 'Schedule updated' // TODO translate
}; };
export default no; export default no;

View File

@@ -307,7 +307,15 @@ const pl: BaseTranslation = {
BLOCK_NAVIGATE_1: 'You have unsaved changes', // TODO translate BLOCK_NAVIGATE_1: 'You have unsaved changes', // TODO translate
BLOCK_NAVIGATE_2: 'If you navigate to a different page, your unsaved changes will be lost. Are you sure you want to leave this page?', // TODO translate BLOCK_NAVIGATE_2: 'If you navigate to a different page, your unsaved changes will be lost. Are you sure you want to leave this page?', // TODO translate
STAY: 'Stay', // TODO translate STAY: 'Stay', // TODO translate
LEAVE: 'Leave' // TODO translate LEAVE: 'Leave', // TODO translate
SCHEDULER: 'Scheduler', // TODO translate
SCHEDULER_HELP_1: 'Add custom scheduled commands to automate EMS-ESP.', // TODO translate
SCHEDULER_HELP_2: 'The NTP service needs to be active if using the calendar.', // TODO translate
SCHEDULE: 'Schedule', // TODO translate SCHEDULE: 'Schedule', // TODO translate
TIME: 'Time', // TODO translate
TIMER: 'Timer', // TODO translate
WEEKLY: 'Weekly', // TODO translate
SCHEDULE_SAVED: 'Schedule updated' // TODO translate
}; };
export default pl; export default pl;

View File

@@ -307,7 +307,15 @@ const sv: Translation = {
BLOCK_NAVIGATE_1: 'You have unsaved changes', // TODO translate BLOCK_NAVIGATE_1: 'You have unsaved changes', // TODO translate
BLOCK_NAVIGATE_2: 'If you navigate to a different page, your unsaved changes will be lost. Are you sure you want to leave this page?', // TODO translate BLOCK_NAVIGATE_2: 'If you navigate to a different page, your unsaved changes will be lost. Are you sure you want to leave this page?', // TODO translate
STAY: 'Stay', // TODO translate STAY: 'Stay', // TODO translate
LEAVE: 'Leave' // TODO translate LEAVE: 'Leave', // TODO translate
SCHEDULER: 'Scheduler', // TODO translate
SCHEDULER_HELP_1: 'Add custom scheduled commands to automate EMS-ESP.', // TODO translate
SCHEDULER_HELP_2: 'The NTP service needs to be active if using the calendar.', // TODO translate
SCHEDULE: 'Schedule', // TODO translate
TIME: 'Time', // TODO translate
TIMER: 'Timer', // TODO translate
WEEKLY: 'Weekly', // TODO translate
SCHEDULE_SAVED: 'Schedule updated' // TODO translate
}; };
export default sv; export default sv;

View File

@@ -9,6 +9,7 @@ import { useI18nContext } from 'i18n/i18n-react';
import SettingsApplication from './SettingsApplication'; import SettingsApplication from './SettingsApplication';
import SettingsCustomization from './SettingsCustomization'; import SettingsCustomization from './SettingsCustomization';
import SettingsScheduler from './SettingsScheduler';
const Settings: FC = () => { const Settings: FC = () => {
const { LL } = useI18nContext(); const { LL } = useI18nContext();
@@ -21,10 +22,12 @@ const Settings: FC = () => {
<RouterTabs value={routerTab}> <RouterTabs value={routerTab}>
<Tab value="application" label={LL.APPLICATION_SETTINGS()} /> <Tab value="application" label={LL.APPLICATION_SETTINGS()} />
<Tab value="customization" label={LL.CUSTOMIZATIONS()} /> <Tab value="customization" label={LL.CUSTOMIZATIONS()} />
<Tab value="scheduler" label={LL.SCHEDULER()} />
</RouterTabs> </RouterTabs>
<Routes> <Routes>
<Route path="application" element={<SettingsApplication />} /> <Route path="application" element={<SettingsApplication />} />
<Route path="customization" element={<SettingsCustomization />} /> <Route path="customization" element={<SettingsCustomization />} />
<Route path="scheduler" element={<SettingsScheduler />} />
<Route path="/*" element={<Navigate replace to="application" />} /> <Route path="/*" element={<Navigate replace to="application" />} />
</Routes> </Routes>
</> </>

View File

@@ -104,6 +104,7 @@ const SettingsCustomization: FC = () => {
.th { .th {
border-bottom: 1px solid #565656; border-bottom: 1px solid #565656;
font-weight: 500; font-weight: 500;
height: 32px;
} }
&:nth-of-type(1) .th { &:nth-of-type(1) .th {
@@ -352,6 +353,7 @@ const SettingsCustomization: FC = () => {
variant="outlined" variant="outlined"
fullWidth fullWidth
value={selectedDevice} value={selectedDevice}
disabled={numChanges !== 0}
onChange={changeSelectedDevice} onChange={changeSelectedDevice}
margin="normal" margin="normal"
select select
@@ -495,11 +497,7 @@ const SettingsCustomization: FC = () => {
<Header> <Header>
<HeaderRow> <HeaderRow>
<HeaderCell stiff>{LL.OPTIONS()}</HeaderCell> <HeaderCell stiff>{LL.OPTIONS()}</HeaderCell>
<HeaderCell resize> <HeaderCell resize>{LL.NAME(1)}</HeaderCell>
<Button fullWidth style={{ fontSize: '14px', justifyContent: 'flex-start' }}>
{LL.NAME(1)}
</Button>
</HeaderCell>
<HeaderCell stiff>{LL.MIN()}</HeaderCell> <HeaderCell stiff>{LL.MIN()}</HeaderCell>
<HeaderCell stiff>{LL.MAX()}</HeaderCell> <HeaderCell stiff>{LL.MAX()}</HeaderCell>
<HeaderCell resize>{LL.VALUE(0)}</HeaderCell> <HeaderCell resize>{LL.VALUE(0)}</HeaderCell>
@@ -522,7 +520,7 @@ const SettingsCustomization: FC = () => {
if (de.m & DeviceEntityMask.DV_WEB_EXCLUDE) { if (de.m & DeviceEntityMask.DV_WEB_EXCLUDE) {
de.m = de.m & ~DeviceEntityMask.DV_FAVORITE; de.m = de.m & ~DeviceEntityMask.DV_FAVORITE;
} }
setMasks(['']); setMasks(['']); // forces a refresh
}} }}
> >
<ToggleButton value="8" disabled={(de.m & 0x81) !== 0 || de.n === undefined}> <ToggleButton value="8" disabled={(de.m & 0x81) !== 0 || de.n === undefined}>
@@ -609,6 +607,14 @@ const SettingsCustomization: FC = () => {
<Box flexGrow={1}> <Box flexGrow={1}>
{numChanges !== 0 && ( {numChanges !== 0 && (
<ButtonRow> <ButtonRow>
<Button
startIcon={<CancelIcon />}
variant="outlined"
color="secondary"
onClick={() => devices && fetchDeviceEntities(devices.devices[selectedDevice].i)}
>
{LL.CANCEL()}
</Button>
<Button <Button
startIcon={<WarningIcon color="warning" />} startIcon={<WarningIcon color="warning" />}
variant="contained" variant="contained"
@@ -639,10 +645,9 @@ const SettingsCustomization: FC = () => {
const renderEditDialog = () => { const renderEditDialog = () => {
if (deviceEntity) { if (deviceEntity) {
const de = deviceEntity;
return ( return (
<Dialog open={!!deviceEntity} onClose={() => setDeviceEntity(undefined)}> <Dialog open={!!deviceEntity} onClose={() => setDeviceEntity(undefined)}>
<DialogTitle>{LL.EDIT() + ' ' + LL.ENTITY() + ' "' + de.id + '"'}</DialogTitle> <DialogTitle>{LL.EDIT() + ' ' + LL.ENTITY() + ' "' + deviceEntity.id + '"'}</DialogTitle>
<DialogContent dividers> <DialogContent dividers>
<Box color="warning.main" mb={2}> <Box color="warning.main" mb={2}>
<Typography variant="body2"> <Typography variant="body2">
@@ -660,7 +665,9 @@ const SettingsCustomization: FC = () => {
onChange={updateValue(setDeviceEntity)} onChange={updateValue(setDeviceEntity)}
/> />
</Grid> </Grid>
{typeof de.v === 'number' && de.w && !(de.m & DeviceEntityMask.DV_READONLY) && ( {typeof deviceEntity.v === 'number' &&
deviceEntity.w &&
!(deviceEntity.m & DeviceEntityMask.DV_READONLY) && (
<> <>
<Grid item> <Grid item>
<TextField <TextField

View File

@@ -0,0 +1,510 @@
import { FC, useState, useEffect, useCallback } from 'react';
import { unstable_useBlocker as useBlocker } from 'react-router-dom';
import {
Button,
Typography,
Box,
Dialog,
DialogActions,
DialogContent,
DialogTitle,
ToggleButton,
ToggleButtonGroup,
Checkbox,
TextField
} 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 RemoveIcon from '@mui/icons-material/RemoveCircleOutline';
import CheckIcon from '@mui/icons-material/Check';
import WarningIcon from '@mui/icons-material/Warning';
import CancelIcon from '@mui/icons-material/Cancel';
import DoneIcon from '@mui/icons-material/Done';
import { ButtonRow, FormLoader, BlockFormControlLabel, SectionContent, BlockNavigation } from 'components';
import * as EMSESP from './api';
import { extractErrorMessage, updateValue } from 'utils';
import { ScheduleItem, ScheduleFlag } from './types';
import { useI18nContext } from 'i18n/i18n-react';
export const APIURL = window.location.origin + '/api/';
const SettingsScheduler: FC = () => {
const { LL, locale } = useI18nContext();
const { enqueueSnackbar } = useSnackbar();
const [numChanges, setNumChanges] = useState<number>(0);
const blocker = useBlocker(numChanges !== 0);
const [errorMessage, setErrorMessage] = useState<string>();
const emptySchedule = {
id: '',
active: false,
deleted: false,
flags: 0,
time: '',
cmd: '',
value: '',
description: ''
};
const [schedule, setSchedule] = useState<ScheduleItem[]>([emptySchedule]);
const [scheduleItem, setScheduleItem] = useState<ScheduleItem>();
const [dow, setDow] = useState<string[]>([]);
function getDayNames() {
const formatter = new Intl.DateTimeFormat(locale, { weekday: 'short', timeZone: 'UTC' });
const days = [2, 3, 4, 5, 6, 7, 1].map((day) => {
const dd = day < 10 ? `0${day}` : day;
return new Date(`2017-01-${dd}T00:00:00+00:00`);
});
return days.map((date) => formatter.format(date));
}
// eslint-disable-next-line
const [flags, setFlags] = useState(() => ['']);
useEffect(() => {
setNumChanges(getNumChanges());
});
const schedule_theme = useTheme({
Table: `
--data-table-library_grid-template-columns: 32px 324px 72px repeat(1, minmax(100px, 1fr)) 100px 100px;
`,
BaseRow: `
font-size: 14px;
.td {
height: 32px;
}
`,
BaseCell: `
&:nth-of-type(1) {
text-align: center;
},
&:nth-of-type(2) {
text-align: center;
},
&:nth-of-type(3) {
text-align: center;
},
`,
HeaderRow: `
text-transform: uppercase;
background-color: black;
color: #90CAF9;
.th {
border-bottom: 1px solid #565656;
font-weight: 500;
height: 36px;
}
`,
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;
}
`
});
const fetchSchedule = useCallback(async () => {
try {
setOriginalSchedule((await EMSESP.readSchedule()).data);
} catch (error) {
setErrorMessage(extractErrorMessage(error, LL.PROBLEM_LOADING()));
}
setDow(getDayNames());
}, [LL]);
useEffect(() => {
fetchSchedule();
}, [fetchSchedule]);
const setOriginalSchedule = (data: ScheduleItem[]) => {
setSchedule(
data.map((si) => ({
...si,
o_active: si.active,
o_deleted: si.deleted,
o_flags: si.flags,
o_time: si.time,
o_cmd: si.cmd,
o_value: si.value,
o_description: si.description
}))
);
};
const getFlagNumber = (newFlag: string[]) => {
let new_flag = 0;
for (const entry of newFlag) {
new_flag |= Number(entry);
}
return new_flag;
};
const getFlagString = (f: number) => {
let new_flags: string[] = [];
if ((f & 1) === 1) {
new_flags.push('1');
}
if ((f & 2) === 2) {
new_flags.push('2');
}
if ((f & 4) === 4) {
new_flags.push('4');
}
if ((f & 8) === 8) {
new_flags.push('8');
}
if ((f & 16) === 16) {
new_flags.push('16');
}
if ((f & 32) === 32) {
new_flags.push('32');
}
if ((f & 64) === 64) {
new_flags.push('64');
}
if ((f & 128) === 128) {
new_flags.push('128');
}
return new_flags;
};
function hasScheduleChanged(si: ScheduleItem) {
return (
(si?.description || '') !== (si?.o_description || '') ||
si.active !== si.o_active ||
si.deleted !== si.o_deleted ||
si.flags !== si.o_flags ||
si.time !== si.o_time ||
si.cmd !== si.o_cmd ||
si.value !== si.o_value
);
}
const getNumChanges = () => {
if (!schedule) {
return 0;
}
return schedule.filter((si) => hasScheduleChanged(si)).length;
};
const saveSchedule = async () => {
if (schedule) {
try {
const response = await EMSESP.writeSchedule({
schedule: schedule
.filter((si) => !si.deleted)
.map((new_si) => {
return {
id: new_si.id,
active: new_si.active,
flags: new_si.flags,
time: new_si.time,
cmd: new_si.cmd,
value: new_si.value,
description: new_si.description
};
})
});
if (response.status === 200) {
enqueueSnackbar(LL.SCHEDULE_SAVED(), { variant: 'success' });
} else {
enqueueSnackbar(LL.PROBLEM_UPDATING(), { variant: 'error' });
}
} catch (error) {
enqueueSnackbar(extractErrorMessage(error, LL.PROBLEM_UPDATING()), { variant: 'error' });
}
setOriginalSchedule(schedule);
}
};
function showFlag(si: ScheduleItem, flag: number) {
let text = '';
if ((flag & ScheduleFlag.SCHEDULE_MON) === ScheduleFlag.SCHEDULE_MON) {
text = dow[0];
}
if ((flag & ScheduleFlag.SCHEDULE_TUE) === ScheduleFlag.SCHEDULE_TUE) {
text = dow[1];
}
if ((flag & ScheduleFlag.SCHEDULE_WED) === ScheduleFlag.SCHEDULE_WED) {
text = dow[2];
}
if ((flag & ScheduleFlag.SCHEDULE_THU) === ScheduleFlag.SCHEDULE_THU) {
text = dow[3];
}
if ((flag & ScheduleFlag.SCHEDULE_FRI) === ScheduleFlag.SCHEDULE_FRI) {
text = dow[4];
}
if ((flag & ScheduleFlag.SCHEDULE_SAT) === ScheduleFlag.SCHEDULE_SAT) {
text = dow[5];
}
if ((flag & ScheduleFlag.SCHEDULE_SUN) === ScheduleFlag.SCHEDULE_SUN) {
text = dow[6];
}
if ((flag & ScheduleFlag.SCHEDULE_TIMER) === ScheduleFlag.SCHEDULE_TIMER) {
text = LL.TIMER();
}
return (
<Typography variant="button" sx={{ fontSize: 10 }} color={(si.flags & flag) === flag ? 'primary' : 'grey'}>
{text}
</Typography>
);
}
const editScheduleItem = (si: ScheduleItem) => {
if (si.description === undefined) {
si.description = '';
}
setScheduleItem(si);
};
const updateScheduleItem = () => {
if (scheduleItem) {
setSchedule((prevState) => {
const newState = prevState.map((obj) => {
if (obj.id === scheduleItem.id) {
return {
...obj,
active: scheduleItem.active,
time: scheduleItem.time,
cmd: scheduleItem.cmd,
value: scheduleItem.value,
description: scheduleItem.description
};
}
return obj;
});
return newState;
});
}
setScheduleItem(undefined);
};
const renderSchedule = () => {
if (!schedule) {
return <FormLoader errorMessage={errorMessage} />;
}
return (
<Table data={{ nodes: schedule.filter((si) => !si.deleted) }} theme={schedule_theme} layout={{ custom: true }}>
{(tableList: any) => (
<>
<Header>
<HeaderRow>
<HeaderCell stiff>
<CheckIcon sx={{ fontSize: 16, verticalAlign: 'middle' }} />
</HeaderCell>
<HeaderCell stiff>{LL.SCHEDULE()}</HeaderCell>
<HeaderCell stiff>{LL.TIME()}</HeaderCell>
<HeaderCell stiff>{LL.COMMAND()}</HeaderCell>
<HeaderCell stiff>{LL.VALUE(0)}</HeaderCell>
<HeaderCell resize>{LL.DESCRIPTION()}</HeaderCell>
</HeaderRow>
</Header>
<Body>
{tableList.map((si: ScheduleItem) => (
<Row key={si.id} item={si} onClick={() => editScheduleItem(si)}>
<Cell stiff>
<Checkbox
size="small"
checked={si.active}
onChange={() => {
si.active = !si.active;
setFlags(['']); // forces refresh
}}
/>
</Cell>
<Cell stiff>
<ToggleButtonGroup
size="small"
color="secondary"
value={getFlagString(si.flags)}
onChange={(event, flag) => {
si.flags = getFlagNumber(flag);
if (si.flags & ScheduleFlag.SCHEDULE_TIMER) {
si.flags = ScheduleFlag.SCHEDULE_TIMER;
}
setFlags(['']); // forces refresh
}}
>
<ToggleButton value="1">{showFlag(si, ScheduleFlag.SCHEDULE_MON)}</ToggleButton>
<ToggleButton value="2">{showFlag(si, ScheduleFlag.SCHEDULE_TUE)}</ToggleButton>
<ToggleButton value="4">{showFlag(si, ScheduleFlag.SCHEDULE_WED)}</ToggleButton>
<ToggleButton value="8">{showFlag(si, ScheduleFlag.SCHEDULE_THU)}</ToggleButton>
<ToggleButton value="16">{showFlag(si, ScheduleFlag.SCHEDULE_FRI)}</ToggleButton>
<ToggleButton value="32">{showFlag(si, ScheduleFlag.SCHEDULE_SAT)}</ToggleButton>
<ToggleButton value="64">{showFlag(si, ScheduleFlag.SCHEDULE_SUN)}</ToggleButton>
<ToggleButton value="128">{showFlag(si, ScheduleFlag.SCHEDULE_TIMER)}</ToggleButton>
</ToggleButtonGroup>
</Cell>
<Cell>{si.time}</Cell>
<Cell>{si.cmd}</Cell>
<Cell>{si.value}</Cell>
<Cell>{si.description}</Cell>
</Row>
))}
</Body>
</>
)}
</Table>
);
};
const removeScheduleItem = (si: ScheduleItem) => {
si.deleted = true;
setScheduleItem(si);
updateScheduleItem();
};
const renderEditSchedule = () => {
if (scheduleItem) {
return (
<Dialog open={!!scheduleItem} onClose={() => setScheduleItem(undefined)}>
<DialogTitle>
{LL.EDIT() +
' ' +
((scheduleItem.flags & ScheduleFlag.SCHEDULE_TIMER) === ScheduleFlag.SCHEDULE_TIMER
? LL.TIMER()
: LL.WEEKLY()) +
' ' +
LL.SCHEDULE()}
</DialogTitle>
<DialogContent dividers>
<TextField
name="description"
label={LL.DESCRIPTION()}
value={scheduleItem.description}
fullWidth
autoFocus
margin="normal"
sx={{ width: '60ch' }}
onChange={updateValue(setScheduleItem)}
/>
<BlockFormControlLabel
control={<Checkbox checked={scheduleItem.active} onChange={updateValue(setScheduleItem)} name="active" />}
label={LL.ACTIVE()}
/>
<TextField
name="time"
type="time"
label={LL.TIME()}
value={scheduleItem.time}
margin="normal"
onChange={updateValue(setScheduleItem)}
/>
<TextField
name="command"
label={LL.COMMAND()}
fullWidth
value={scheduleItem.cmd}
margin="normal"
onChange={updateValue(setScheduleItem)}
/>
<TextField
name="value"
label={LL.VALUE(1)}
multiline
margin="normal"
fullWidth
value={scheduleItem.value}
onChange={updateValue(setScheduleItem)}
/>
</DialogContent>
<DialogActions>
<Box flexGrow={1} sx={{ '& button': { mt: 0 } }}>
<Button
startIcon={<RemoveIcon />}
variant="outlined"
color="error"
onClick={() => removeScheduleItem(scheduleItem)}
>
{LL.REMOVE()}
</Button>
</Box>
<Button
startIcon={<CancelIcon />}
variant="outlined"
onClick={() => setScheduleItem(undefined)}
color="secondary"
>
{LL.CANCEL()}
</Button>
<Button
startIcon={<DoneIcon />}
variant="outlined"
type="submit"
onClick={() => updateScheduleItem()}
color="primary"
>
{LL.UPDATE()}
</Button>
</DialogActions>
</Dialog>
);
}
};
return (
<SectionContent title={LL.SCHEDULER()} titleGutter>
{blocker ? <BlockNavigation blocker={blocker} /> : null}
<Box mb={2} color="warning.main">
<Typography variant="body2">{LL.SCHEDULER_HELP_1()}</Typography>
<Typography variant="body2">{LL.SCHEDULER_HELP_2()}</Typography>
</Box>
{renderSchedule()}
{renderEditSchedule()}
<Box display="flex" flexWrap="wrap">
<Box flexGrow={1}>
{numChanges !== 0 && (
<ButtonRow>
<Button startIcon={<CancelIcon />} variant="outlined" onClick={() => fetchSchedule()} color="secondary">
{LL.CANCEL()}
</Button>
<Button
startIcon={<WarningIcon color="warning" />}
variant="contained"
color="info"
onClick={() => saveSchedule()}
>
{LL.APPLY_CHANGES(numChanges)}
</Button>
</ButtonRow>
)}
</Box>
</Box>
</SectionContent>
);
};
export default SettingsScheduler;

View File

@@ -16,7 +16,9 @@ import {
WriteValue, WriteValue,
WriteSensor, WriteSensor,
WriteAnalog, WriteAnalog,
SensorData SensorData,
ScheduleItem,
Schedule
} from './types'; } from './types';
export function restart(): AxiosPromise<void> { export function restart(): AxiosPromise<void> {
@@ -94,3 +96,11 @@ export function getSettings(): AxiosPromise<void> {
export function getCustomizations(): AxiosPromise<void> { export function getCustomizations(): AxiosPromise<void> {
return AXIOS.get('/getCustomizations'); return AXIOS.get('/getCustomizations');
} }
export function readSchedule(): AxiosPromise<ScheduleItem[]> {
return AXIOS.get('/getSchedule');
}
export function writeSchedule(schedule: Schedule): AxiosPromise<void> {
return AXIOS.post('/writeSchedule', schedule);
}

View File

@@ -303,3 +303,36 @@ export enum DeviceEntityMask {
DV_FAVORITE = 8, DV_FAVORITE = 8,
DV_DELETED = 128 DV_DELETED = 128
} }
export interface ScheduleItem {
id: string; // unique index
active: boolean;
deleted?: boolean; // optional
flags: number;
time: string;
cmd: string;
value: string;
description?: string; // optional
o_active?: boolean;
o_deleted?: boolean;
o_flags?: number;
o_time?: string;
o_cmd?: string;
o_value?: string;
o_description?: string;
}
export interface Schedule {
schedule: ScheduleItem[];
}
export enum ScheduleFlag {
SCHEDULE_MON = 1,
SCHEDULE_TUE = 2,
SCHEDULE_WED = 4,
SCHEDULE_THU = 8,
SCHEDULE_FRI = 16,
SCHEDULE_SAT = 32,
SCHEDULE_SUN = 64,
SCHEDULE_TIMER = 128
}

View File

@@ -15,24 +15,3 @@ export const formatDateTime = (dateTime: string) => {
export const formatLocalDateTime = (date: Date) => { export const formatLocalDateTime = (date: Date) => {
return new Date(date.getTime() - date.getTimezoneOffset() * 60000).toISOString().slice(0, -1).substr(0, 19); return new Date(date.getTime() - date.getTimezoneOffset() * 60000).toISOString().slice(0, -1).substr(0, 19);
}; };
// export const pluralize = (count: number, noun: string) =>
// `${Intl.NumberFormat().format(count)} ${noun}${count !== 1 ? 's' : ''}`;
// export const formatDurationSec = (duration_sec: number) => {
// if (duration_sec === 0) {
// return ' ';
// }
// const roundTowardsZero = duration_sec > 0 ? Math.floor : Math.ceil;
// return (
// ', ' +
// roundTowardsZero(duration_sec / 86400) +
// 'd ' +
// (roundTowardsZero(duration_sec / 3600) % 24) +
// 'h ' +
// (roundTowardsZero(duration_sec / 60) % 60) +
// 'm ' +
// (roundTowardsZero(duration_sec) % 60) +
// 's'
// );
// };

View File

@@ -320,6 +320,7 @@ const EMSESP_WRITE_SENSOR_ENDPOINT = REST_ENDPOINT_ROOT + 'writeSensor';
const EMSESP_WRITE_ANALOG_ENDPOINT = REST_ENDPOINT_ROOT + 'writeAnalog'; const EMSESP_WRITE_ANALOG_ENDPOINT = REST_ENDPOINT_ROOT + 'writeAnalog';
const EMSESP_CUSTOM_ENTITIES_ENDPOINT = REST_ENDPOINT_ROOT + 'customEntities'; const EMSESP_CUSTOM_ENTITIES_ENDPOINT = REST_ENDPOINT_ROOT + 'customEntities';
const EMSESP_RESET_CUSTOMIZATIONS_ENDPOINT = REST_ENDPOINT_ROOT + 'resetCustomizations'; const EMSESP_RESET_CUSTOMIZATIONS_ENDPOINT = REST_ENDPOINT_ROOT + 'resetCustomizations';
const EMSESP_WRITE_SCHEDULE_ENDPOINT = REST_ENDPOINT_ROOT + 'writeSchedule';
settings = { settings = {
locale: 'en', locale: 'en',
@@ -598,6 +599,46 @@ const emsesp_devicedata_4 = {
] ]
}; };
// SCHEDULE
let emsesp_schedule = [
{
id: '1',
active: true,
flags: 31,
time: '07:30',
cmd: 'hc1/mode',
value: 'day',
description: 'Turn on central heating in morning'
},
{
id: '2',
active: true,
flags: 31,
time: '23:00',
cmd: 'hc1/mode',
value: 'night',
description: 'Turn off central heating for the night'
},
{
id: '3',
active: true,
flags: 128,
time: '00:01',
cmd: 'thermostat/hc2/seltemp',
value: '20',
description: 'Force thermostat temperature to 20 degrees every minute'
},
{
id: '4',
active: false,
flags: 85,
time: '04:00',
cmd: 'system/restart',
value: '',
description: 'auto restart EMS-EPS at 4am every other day'
}
];
// CUSTOMIZATIONS // CUSTOMIZATIONS
const emsesp_deviceentities_1 = [ const emsesp_deviceentities_1 = [
@@ -1020,6 +1061,12 @@ rest_server.post(EMSESP_CUSTOM_ENTITIES_ENDPOINT, (req, res) => {
res.sendStatus(200); res.sendStatus(200);
}); });
rest_server.post(EMSESP_WRITE_SCHEDULE_ENDPOINT, (req, res) => {
console.log('write schedule');
console.log(req.body.schedule);
res.sendStatus(200);
});
rest_server.post(EMSESP_WRITE_VALUE_ENDPOINT, (req, res) => { rest_server.post(EMSESP_WRITE_VALUE_ENDPOINT, (req, res) => {
const devicevalue = req.body.devicevalue; const devicevalue = req.body.devicevalue;
const id = req.body.id; const id = req.body.id;
@@ -1308,11 +1355,17 @@ rest_server.get(GET_SETTINGS_ENDPOINT, (req, res) => {
const GET_CUSTOMIZATIONS_ENDPOINT = REST_ENDPOINT_ROOT + 'getCustomizations'; const GET_CUSTOMIZATIONS_ENDPOINT = REST_ENDPOINT_ROOT + 'getCustomizations';
rest_server.get(GET_CUSTOMIZATIONS_ENDPOINT, (req, res) => { rest_server.get(GET_CUSTOMIZATIONS_ENDPOINT, (req, res) => {
console.log('Customizations:'); console.log('Customization');
// not implemented yet // not implemented yet
res.sendStatus(200); res.sendStatus(200);
}); });
const GET_SCHEDULE_ENDPOINT = REST_ENDPOINT_ROOT + 'getSchedule';
rest_server.get(GET_SCHEDULE_ENDPOINT, (req, res) => {
console.log('Sending Schedule data');
res.json(emsesp_schedule);
});
// start server // start server
const expressServer = rest_server.listen(port, () => const expressServer = rest_server.listen(port, () =>
console.log(`EMS-ESP REST API server running on http://localhost:${port}/api`) console.log(`EMS-ESP REST API server running on http://localhost:${port}/api`)