mirror of
https://github.com/emsesp/EMS-ESP32.git
synced 2025-12-06 07:49:52 +03:00
Scheduler #701
This commit is contained in:
@@ -3,10 +3,10 @@ import { Link, useLocation } from 'react-router-dom';
|
||||
|
||||
import { ListItem, ListItemButton, ListItemIcon, ListItemText, SvgIconProps } from '@mui/material';
|
||||
|
||||
import { grey } from '@mui/material/colors';
|
||||
|
||||
import { routeMatches } from 'utils';
|
||||
|
||||
import { grey } from '@mui/material/colors';
|
||||
|
||||
interface LayoutMenuItemProps {
|
||||
icon: React.ComponentType<SvgIconProps>;
|
||||
label: string;
|
||||
@@ -17,13 +17,15 @@ interface LayoutMenuItemProps {
|
||||
const LayoutMenuItem: FC<LayoutMenuItemProps> = ({ icon: Icon, label, to, disabled }) => {
|
||||
const { pathname } = useLocation();
|
||||
|
||||
const selected = routeMatches(to, pathname);
|
||||
|
||||
return (
|
||||
<ListItem disablePadding selected={routeMatches(to, pathname)}>
|
||||
<ListItemButton component={Link} to={to} disabled={disabled}>
|
||||
<ListItemIcon sx={{ color: grey[500] }}>
|
||||
<ListItem disablePadding>
|
||||
<ListItemButton component={Link} to={to} disabled={disabled} selected={selected}>
|
||||
<ListItemIcon sx={{ color: selected ? '#90caf9' : grey[500] }}>
|
||||
<Icon />
|
||||
</ListItemIcon>
|
||||
<ListItemText>{label}</ListItemText>
|
||||
<ListItemText sx={{ color: selected ? '#90caf9' : grey[100] }}>{label}</ListItemText>
|
||||
</ListItemButton>
|
||||
</ListItem>
|
||||
);
|
||||
|
||||
@@ -307,7 +307,15 @@ const de: Translation = {
|
||||
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
|
||||
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;
|
||||
|
||||
@@ -307,7 +307,15 @@ const en: Translation = {
|
||||
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?',
|
||||
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;
|
||||
|
||||
@@ -307,7 +307,15 @@ const fr: Translation = {
|
||||
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
|
||||
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;
|
||||
|
||||
@@ -307,7 +307,15 @@ const nl: Translation = {
|
||||
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
|
||||
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;
|
||||
|
||||
@@ -307,7 +307,15 @@ const no: Translation = {
|
||||
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
|
||||
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;
|
||||
|
||||
@@ -307,7 +307,15 @@ const pl: BaseTranslation = {
|
||||
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
|
||||
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;
|
||||
|
||||
@@ -307,7 +307,15 @@ const sv: Translation = {
|
||||
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
|
||||
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;
|
||||
|
||||
@@ -9,6 +9,7 @@ import { useI18nContext } from 'i18n/i18n-react';
|
||||
|
||||
import SettingsApplication from './SettingsApplication';
|
||||
import SettingsCustomization from './SettingsCustomization';
|
||||
import SettingsScheduler from './SettingsScheduler';
|
||||
|
||||
const Settings: FC = () => {
|
||||
const { LL } = useI18nContext();
|
||||
@@ -21,10 +22,12 @@ const Settings: FC = () => {
|
||||
<RouterTabs value={routerTab}>
|
||||
<Tab value="application" label={LL.APPLICATION_SETTINGS()} />
|
||||
<Tab value="customization" label={LL.CUSTOMIZATIONS()} />
|
||||
<Tab value="scheduler" label={LL.SCHEDULER()} />
|
||||
</RouterTabs>
|
||||
<Routes>
|
||||
<Route path="application" element={<SettingsApplication />} />
|
||||
<Route path="customization" element={<SettingsCustomization />} />
|
||||
<Route path="scheduler" element={<SettingsScheduler />} />
|
||||
<Route path="/*" element={<Navigate replace to="application" />} />
|
||||
</Routes>
|
||||
</>
|
||||
|
||||
@@ -104,6 +104,7 @@ const SettingsCustomization: FC = () => {
|
||||
.th {
|
||||
border-bottom: 1px solid #565656;
|
||||
font-weight: 500;
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
&:nth-of-type(1) .th {
|
||||
@@ -352,6 +353,7 @@ const SettingsCustomization: FC = () => {
|
||||
variant="outlined"
|
||||
fullWidth
|
||||
value={selectedDevice}
|
||||
disabled={numChanges !== 0}
|
||||
onChange={changeSelectedDevice}
|
||||
margin="normal"
|
||||
select
|
||||
@@ -495,11 +497,7 @@ const SettingsCustomization: FC = () => {
|
||||
<Header>
|
||||
<HeaderRow>
|
||||
<HeaderCell stiff>{LL.OPTIONS()}</HeaderCell>
|
||||
<HeaderCell resize>
|
||||
<Button fullWidth style={{ fontSize: '14px', justifyContent: 'flex-start' }}>
|
||||
{LL.NAME(1)}
|
||||
</Button>
|
||||
</HeaderCell>
|
||||
<HeaderCell resize>{LL.NAME(1)}</HeaderCell>
|
||||
<HeaderCell stiff>{LL.MIN()}</HeaderCell>
|
||||
<HeaderCell stiff>{LL.MAX()}</HeaderCell>
|
||||
<HeaderCell resize>{LL.VALUE(0)}</HeaderCell>
|
||||
@@ -522,7 +520,7 @@ const SettingsCustomization: FC = () => {
|
||||
if (de.m & DeviceEntityMask.DV_WEB_EXCLUDE) {
|
||||
de.m = de.m & ~DeviceEntityMask.DV_FAVORITE;
|
||||
}
|
||||
setMasks(['']);
|
||||
setMasks(['']); // forces a refresh
|
||||
}}
|
||||
>
|
||||
<ToggleButton value="8" disabled={(de.m & 0x81) !== 0 || de.n === undefined}>
|
||||
@@ -609,6 +607,14 @@ const SettingsCustomization: FC = () => {
|
||||
<Box flexGrow={1}>
|
||||
{numChanges !== 0 && (
|
||||
<ButtonRow>
|
||||
<Button
|
||||
startIcon={<CancelIcon />}
|
||||
variant="outlined"
|
||||
color="secondary"
|
||||
onClick={() => devices && fetchDeviceEntities(devices.devices[selectedDevice].i)}
|
||||
>
|
||||
{LL.CANCEL()}
|
||||
</Button>
|
||||
<Button
|
||||
startIcon={<WarningIcon color="warning" />}
|
||||
variant="contained"
|
||||
@@ -639,10 +645,9 @@ const SettingsCustomization: FC = () => {
|
||||
|
||||
const renderEditDialog = () => {
|
||||
if (deviceEntity) {
|
||||
const de = deviceEntity;
|
||||
return (
|
||||
<Dialog open={!!deviceEntity} onClose={() => setDeviceEntity(undefined)}>
|
||||
<DialogTitle>{LL.EDIT() + ' ' + LL.ENTITY() + ' "' + de.id + '"'}</DialogTitle>
|
||||
<DialogTitle>{LL.EDIT() + ' ' + LL.ENTITY() + ' "' + deviceEntity.id + '"'}</DialogTitle>
|
||||
<DialogContent dividers>
|
||||
<Box color="warning.main" mb={2}>
|
||||
<Typography variant="body2">
|
||||
@@ -660,28 +665,30 @@ const SettingsCustomization: FC = () => {
|
||||
onChange={updateValue(setDeviceEntity)}
|
||||
/>
|
||||
</Grid>
|
||||
{typeof de.v === 'number' && de.w && !(de.m & DeviceEntityMask.DV_READONLY) && (
|
||||
<>
|
||||
<Grid item>
|
||||
<TextField
|
||||
name="mi"
|
||||
label={LL.MIN()}
|
||||
value={deviceEntity.mi}
|
||||
sx={{ width: '8ch' }}
|
||||
onChange={updateValue(setDeviceEntity)}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<TextField
|
||||
name="ma"
|
||||
label={LL.MAX()}
|
||||
value={deviceEntity.ma}
|
||||
sx={{ width: '8ch' }}
|
||||
onChange={updateValue(setDeviceEntity)}
|
||||
/>
|
||||
</Grid>
|
||||
</>
|
||||
)}
|
||||
{typeof deviceEntity.v === 'number' &&
|
||||
deviceEntity.w &&
|
||||
!(deviceEntity.m & DeviceEntityMask.DV_READONLY) && (
|
||||
<>
|
||||
<Grid item>
|
||||
<TextField
|
||||
name="mi"
|
||||
label={LL.MIN()}
|
||||
value={deviceEntity.mi}
|
||||
sx={{ width: '8ch' }}
|
||||
onChange={updateValue(setDeviceEntity)}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<TextField
|
||||
name="ma"
|
||||
label={LL.MAX()}
|
||||
value={deviceEntity.ma}
|
||||
sx={{ width: '8ch' }}
|
||||
onChange={updateValue(setDeviceEntity)}
|
||||
/>
|
||||
</Grid>
|
||||
</>
|
||||
)}
|
||||
</Grid>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
|
||||
510
interface/src/project/SettingsScheduler.tsx
Normal file
510
interface/src/project/SettingsScheduler.tsx
Normal 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;
|
||||
@@ -16,7 +16,9 @@ import {
|
||||
WriteValue,
|
||||
WriteSensor,
|
||||
WriteAnalog,
|
||||
SensorData
|
||||
SensorData,
|
||||
ScheduleItem,
|
||||
Schedule
|
||||
} from './types';
|
||||
|
||||
export function restart(): AxiosPromise<void> {
|
||||
@@ -94,3 +96,11 @@ export function getSettings(): AxiosPromise<void> {
|
||||
export function getCustomizations(): AxiosPromise<void> {
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -303,3 +303,36 @@ export enum DeviceEntityMask {
|
||||
DV_FAVORITE = 8,
|
||||
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
|
||||
}
|
||||
|
||||
@@ -15,24 +15,3 @@ export const formatDateTime = (dateTime: string) => {
|
||||
export const formatLocalDateTime = (date: Date) => {
|
||||
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'
|
||||
// );
|
||||
// };
|
||||
|
||||
@@ -320,6 +320,7 @@ const EMSESP_WRITE_SENSOR_ENDPOINT = REST_ENDPOINT_ROOT + 'writeSensor';
|
||||
const EMSESP_WRITE_ANALOG_ENDPOINT = REST_ENDPOINT_ROOT + 'writeAnalog';
|
||||
const EMSESP_CUSTOM_ENTITIES_ENDPOINT = REST_ENDPOINT_ROOT + 'customEntities';
|
||||
const EMSESP_RESET_CUSTOMIZATIONS_ENDPOINT = REST_ENDPOINT_ROOT + 'resetCustomizations';
|
||||
const EMSESP_WRITE_SCHEDULE_ENDPOINT = REST_ENDPOINT_ROOT + 'writeSchedule';
|
||||
|
||||
settings = {
|
||||
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
|
||||
|
||||
const emsesp_deviceentities_1 = [
|
||||
@@ -1020,6 +1061,12 @@ rest_server.post(EMSESP_CUSTOM_ENTITIES_ENDPOINT, (req, res) => {
|
||||
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) => {
|
||||
const devicevalue = req.body.devicevalue;
|
||||
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';
|
||||
rest_server.get(GET_CUSTOMIZATIONS_ENDPOINT, (req, res) => {
|
||||
console.log('Customizations:');
|
||||
console.log('Customization');
|
||||
// not implemented yet
|
||||
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
|
||||
const expressServer = rest_server.listen(port, () =>
|
||||
console.log(`EMS-ESP REST API server running on http://localhost:${port}/api`)
|
||||
|
||||
Reference in New Issue
Block a user