Allow time & date to be configured manually when NTP is inactive (#153)
* Allow time to be configured manually when NTP is not active * Standarize on primary button on the outside of dialog boxes
This commit is contained in:
parent
003fd8af28
commit
55511e0611
@ -3,6 +3,7 @@ import { ENDPOINT_ROOT } from './Env';
|
|||||||
export const FEATURES_ENDPOINT = ENDPOINT_ROOT + "features";
|
export const FEATURES_ENDPOINT = ENDPOINT_ROOT + "features";
|
||||||
export const NTP_STATUS_ENDPOINT = ENDPOINT_ROOT + "ntpStatus";
|
export const NTP_STATUS_ENDPOINT = ENDPOINT_ROOT + "ntpStatus";
|
||||||
export const NTP_SETTINGS_ENDPOINT = ENDPOINT_ROOT + "ntpSettings";
|
export const NTP_SETTINGS_ENDPOINT = ENDPOINT_ROOT + "ntpSettings";
|
||||||
|
export const TIME_ENDPOINT = ENDPOINT_ROOT + "time";
|
||||||
export const AP_SETTINGS_ENDPOINT = ENDPOINT_ROOT + "apSettings";
|
export const AP_SETTINGS_ENDPOINT = ENDPOINT_ROOT + "apSettings";
|
||||||
export const AP_STATUS_ENDPOINT = ENDPOINT_ROOT + "apStatus";
|
export const AP_STATUS_ENDPOINT = ENDPOINT_ROOT + "apStatus";
|
||||||
export const SCAN_NETWORKS_ENDPOINT = ENDPOINT_ROOT + "scanNetworks";
|
export const SCAN_NETWORKS_ENDPOINT = ENDPOINT_ROOT + "scanNetworks";
|
||||||
|
@ -2,7 +2,8 @@ import React, { Component, Fragment } from 'react';
|
|||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
|
|
||||||
import { WithTheme, withTheme } from '@material-ui/core/styles';
|
import { WithTheme, withTheme } from '@material-ui/core/styles';
|
||||||
import { Avatar, Divider, List, ListItem, ListItemAvatar, ListItemText } from '@material-ui/core';
|
import { Avatar, Divider, List, ListItem, ListItemAvatar, ListItemText, Button } from '@material-ui/core';
|
||||||
|
import { Dialog, DialogTitle, DialogContent, DialogActions, Box, TextField } from '@material-ui/core';
|
||||||
|
|
||||||
import SwapVerticalCircleIcon from '@material-ui/icons/SwapVerticalCircle';
|
import SwapVerticalCircleIcon from '@material-ui/icons/SwapVerticalCircle';
|
||||||
import AccessTimeIcon from '@material-ui/icons/AccessTime';
|
import AccessTimeIcon from '@material-ui/icons/AccessTime';
|
||||||
@ -11,18 +12,116 @@ import UpdateIcon from '@material-ui/icons/Update';
|
|||||||
import AvTimerIcon from '@material-ui/icons/AvTimer';
|
import AvTimerIcon from '@material-ui/icons/AvTimer';
|
||||||
import RefreshIcon from '@material-ui/icons/Refresh';
|
import RefreshIcon from '@material-ui/icons/Refresh';
|
||||||
|
|
||||||
import { RestFormProps, FormActions, FormButton, HighlightAvatar } from '../components';
|
import { RestFormProps, FormButton, HighlightAvatar } from '../components';
|
||||||
|
|
||||||
import { isNtpActive, ntpStatusHighlight, ntpStatus } from './NTPStatus';
|
import { isNtpActive, ntpStatusHighlight, ntpStatus } from './NTPStatus';
|
||||||
import { formatIsoDateTime } from './TimeFormat';
|
import { formatIsoDateTime, formatLocalDateTime } from './TimeFormat';
|
||||||
import { NTPStatus } from './types';
|
import { NTPStatus, Time } from './types';
|
||||||
|
import { redirectingAuthorizedFetch, withAuthenticatedContext, AuthenticatedContextProps } from '../authentication';
|
||||||
|
import { TIME_ENDPOINT } from '../api';
|
||||||
|
|
||||||
type NTPStatusFormProps = RestFormProps<NTPStatus> & WithTheme;
|
type NTPStatusFormProps = RestFormProps<NTPStatus> & WithTheme & AuthenticatedContextProps;
|
||||||
|
|
||||||
class NTPStatusForm extends Component<NTPStatusFormProps> {
|
interface NTPStatusFormState {
|
||||||
|
settingTime: boolean;
|
||||||
|
localTime: string;
|
||||||
|
processing: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
class NTPStatusForm extends Component<NTPStatusFormProps, NTPStatusFormState> {
|
||||||
|
|
||||||
|
constructor(props: NTPStatusFormProps) {
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
settingTime: false,
|
||||||
|
localTime: '',
|
||||||
|
processing: false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
updateLocalTime = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
this.setState({ localTime: event.target.value });
|
||||||
|
}
|
||||||
|
|
||||||
|
openSetTime = () => {
|
||||||
|
this.setState({ localTime: formatLocalDateTime(moment()), settingTime: true, });
|
||||||
|
}
|
||||||
|
|
||||||
|
closeSetTime = () => {
|
||||||
|
this.setState({ settingTime: false });
|
||||||
|
}
|
||||||
|
|
||||||
|
createAdjustedTime = (): Time => {
|
||||||
|
const currentLocalTime = moment(this.props.data.time_local);
|
||||||
|
const newLocalTime = moment(this.state.localTime);
|
||||||
|
newLocalTime.subtract(currentLocalTime.utcOffset())
|
||||||
|
newLocalTime.milliseconds(0);
|
||||||
|
newLocalTime.utc();
|
||||||
|
return {
|
||||||
|
time_utc: newLocalTime.format()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
configureTime = () => {
|
||||||
|
this.setState({ processing: true });
|
||||||
|
redirectingAuthorizedFetch(TIME_ENDPOINT,
|
||||||
|
{
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify(this.createAdjustedTime()),
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(response => {
|
||||||
|
if (response.status === 200) {
|
||||||
|
this.props.enqueueSnackbar("Time set successfully", { variant: 'success' });
|
||||||
|
this.setState({ processing: false, settingTime: false }, this.props.loadData);
|
||||||
|
} else {
|
||||||
|
throw Error("Error setting time, status code: " + response.status);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
this.props.enqueueSnackbar(error.message || "Problem setting the time", { variant: 'error' });
|
||||||
|
this.setState({ processing: false, settingTime: false });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
renderSetTimeDialog() {
|
||||||
|
return (
|
||||||
|
<Dialog
|
||||||
|
open={this.state.settingTime}
|
||||||
|
onClose={this.closeSetTime}
|
||||||
|
>
|
||||||
|
<DialogTitle>Set Time</DialogTitle>
|
||||||
|
<DialogContent dividers>
|
||||||
|
<Box mb={2}>Enter local date and time below to set the device's time.</Box>
|
||||||
|
<TextField
|
||||||
|
label="Local Time"
|
||||||
|
type="datetime-local"
|
||||||
|
value={this.state.localTime}
|
||||||
|
onChange={this.updateLocalTime}
|
||||||
|
disabled={this.state.processing}
|
||||||
|
variant="outlined"
|
||||||
|
fullWidth
|
||||||
|
InputLabelProps={{
|
||||||
|
shrink: true,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions>
|
||||||
|
<Button variant="contained" onClick={this.closeSetTime} color="secondary">
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button startIcon={<AccessTimeIcon />} variant="contained" onClick={this.configureTime} disabled={this.state.processing} color="primary" autoFocus>
|
||||||
|
Set Time
|
||||||
|
</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</Dialog>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { data, theme } = this.props
|
const { data, theme } = this.props
|
||||||
|
const me = this.props.authenticatedContext.me;
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<List>
|
<List>
|
||||||
@ -40,19 +139,10 @@ class NTPStatusForm extends Component<NTPStatusFormProps> {
|
|||||||
<ListItem>
|
<ListItem>
|
||||||
<ListItemAvatar>
|
<ListItemAvatar>
|
||||||
<Avatar>
|
<Avatar>
|
||||||
<AccessTimeIcon />
|
<DNSIcon />
|
||||||
</Avatar>
|
</Avatar>
|
||||||
</ListItemAvatar>
|
</ListItemAvatar>
|
||||||
<ListItemText primary="Local Time" secondary={formatIsoDateTime(data.time_local)} />
|
<ListItemText primary="NTP Server" secondary={data.server} />
|
||||||
</ListItem>
|
|
||||||
<Divider variant="inset" component="li" />
|
|
||||||
<ListItem>
|
|
||||||
<ListItemAvatar>
|
|
||||||
<Avatar>
|
|
||||||
<SwapVerticalCircleIcon />
|
|
||||||
</Avatar>
|
|
||||||
</ListItemAvatar>
|
|
||||||
<ListItemText primary="UTC Time" secondary={formatIsoDateTime(data.time_utc)} />
|
|
||||||
</ListItem>
|
</ListItem>
|
||||||
<Divider variant="inset" component="li" />
|
<Divider variant="inset" component="li" />
|
||||||
</Fragment>
|
</Fragment>
|
||||||
@ -60,10 +150,19 @@ class NTPStatusForm extends Component<NTPStatusFormProps> {
|
|||||||
<ListItem>
|
<ListItem>
|
||||||
<ListItemAvatar>
|
<ListItemAvatar>
|
||||||
<Avatar>
|
<Avatar>
|
||||||
<DNSIcon />
|
<AccessTimeIcon />
|
||||||
</Avatar>
|
</Avatar>
|
||||||
</ListItemAvatar>
|
</ListItemAvatar>
|
||||||
<ListItemText primary="NTP Server" secondary={data.server} />
|
<ListItemText primary="Local Time" secondary={formatIsoDateTime(data.time_local)} />
|
||||||
|
</ListItem>
|
||||||
|
<Divider variant="inset" component="li" />
|
||||||
|
<ListItem>
|
||||||
|
<ListItemAvatar>
|
||||||
|
<Avatar>
|
||||||
|
<SwapVerticalCircleIcon />
|
||||||
|
</Avatar>
|
||||||
|
</ListItemAvatar>
|
||||||
|
<ListItemText primary="UTC Time" secondary={formatIsoDateTime(data.time_utc)} />
|
||||||
</ListItem>
|
</ListItem>
|
||||||
<Divider variant="inset" component="li" />
|
<Divider variant="inset" component="li" />
|
||||||
<ListItem>
|
<ListItem>
|
||||||
@ -76,14 +175,24 @@ class NTPStatusForm extends Component<NTPStatusFormProps> {
|
|||||||
</ListItem>
|
</ListItem>
|
||||||
<Divider variant="inset" component="li" />
|
<Divider variant="inset" component="li" />
|
||||||
</List>
|
</List>
|
||||||
<FormActions>
|
<Box display="flex" flexWrap="wrap">
|
||||||
<FormButton startIcon={<RefreshIcon />} variant="contained" color="secondary" onClick={this.props.loadData}>
|
<Box flexGrow={1} padding={1}>
|
||||||
Refresh
|
<FormButton startIcon={<RefreshIcon />} variant="contained" color="secondary" onClick={this.props.loadData}>
|
||||||
</FormButton>
|
Refresh
|
||||||
</FormActions>
|
</FormButton>
|
||||||
|
</Box>
|
||||||
|
{me.admin && !isNtpActive(data) && (
|
||||||
|
<Box flexWrap="none" padding={1} whiteSpace="nowrap">
|
||||||
|
<Button onClick={this.openSetTime} variant="contained" color="primary" startIcon={<AccessTimeIcon />}>
|
||||||
|
Set Time
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
{this.renderSetTimeDialog()}
|
||||||
</Fragment>
|
</Fragment>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default withTheme(NTPStatusForm);
|
export default withAuthenticatedContext(withTheme(NTPStatusForm));
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
import moment from 'moment';
|
import moment, { Moment } from 'moment';
|
||||||
|
|
||||||
export const formatIsoDateTime = (isoDateString: string) => moment.parseZone(isoDateString).format('ll @ HH:mm:ss');
|
export const formatIsoDateTime = (isoDateString: string) => moment.parseZone(isoDateString).format('ll @ HH:mm:ss');
|
||||||
|
|
||||||
|
export const formatLocalDateTime = (moment: Moment) => moment.format('YYYY-MM-DDTHH:mm');
|
||||||
|
@ -17,3 +17,7 @@ export interface NTPSettings {
|
|||||||
tz_label: string;
|
tz_label: string;
|
||||||
tz_format: string;
|
tz_format: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface Time {
|
||||||
|
time_utc: string;
|
||||||
|
}
|
||||||
|
@ -70,12 +70,12 @@ class UserForm extends React.Component<UserFormProps> {
|
|||||||
/>
|
/>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
<DialogActions>
|
<DialogActions>
|
||||||
<FormButton variant="contained" color="primary" type="submit" onClick={this.submit}>
|
|
||||||
Done
|
|
||||||
</FormButton>
|
|
||||||
<FormButton variant="contained" color="secondary" onClick={onCancelEditing}>
|
<FormButton variant="contained" color="secondary" onClick={onCancelEditing}>
|
||||||
Cancel
|
Cancel
|
||||||
</FormButton>
|
</FormButton>
|
||||||
|
<FormButton variant="contained" color="primary" type="submit" onClick={this.submit}>
|
||||||
|
Done
|
||||||
|
</FormButton>
|
||||||
</DialogActions>
|
</DialogActions>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
</ValidatorForm>
|
</ValidatorForm>
|
||||||
|
@ -118,12 +118,12 @@ class SystemStatusForm extends Component<SystemStatusFormProps, SystemStatusForm
|
|||||||
Are you sure you want to restart the device?
|
Are you sure you want to restart the device?
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
<DialogActions>
|
<DialogActions>
|
||||||
<Button startIcon={<PowerSettingsNewIcon />} variant="contained" onClick={this.onRestartConfirmed} disabled={this.state.processing} color="primary" autoFocus>
|
|
||||||
Restart
|
|
||||||
</Button>
|
|
||||||
<Button variant="contained" onClick={this.onRestartRejected} color="secondary">
|
<Button variant="contained" onClick={this.onRestartRejected} color="secondary">
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button startIcon={<PowerSettingsNewIcon />} variant="contained" onClick={this.onRestartConfirmed} disabled={this.state.processing} color="primary" autoFocus>
|
||||||
|
Restart
|
||||||
|
</Button>
|
||||||
</DialogActions>
|
</DialogActions>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
)
|
)
|
||||||
@ -165,12 +165,12 @@ class SystemStatusForm extends Component<SystemStatusFormProps, SystemStatusForm
|
|||||||
Are you sure you want to reset the device to its factory defaults?
|
Are you sure you want to reset the device to its factory defaults?
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
<DialogActions>
|
<DialogActions>
|
||||||
<ErrorButton startIcon={<SettingsBackupRestoreIcon />} variant="contained" onClick={this.onFactoryResetConfirmed} disabled={this.state.processing} autoFocus>
|
|
||||||
Factory Reset
|
|
||||||
</ErrorButton>
|
|
||||||
<Button variant="contained" onClick={this.onFactoryResetRejected} color="secondary">
|
<Button variant="contained" onClick={this.onFactoryResetRejected} color="secondary">
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
|
<ErrorButton startIcon={<SettingsBackupRestoreIcon />} variant="contained" onClick={this.onFactoryResetConfirmed} disabled={this.state.processing} autoFocus>
|
||||||
|
Factory Reset
|
||||||
|
</ErrorButton>
|
||||||
</DialogActions>
|
</DialogActions>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
)
|
)
|
||||||
|
@ -2,7 +2,12 @@
|
|||||||
|
|
||||||
NTPSettingsService::NTPSettingsService(AsyncWebServer* server, FS* fs, SecurityManager* securityManager) :
|
NTPSettingsService::NTPSettingsService(AsyncWebServer* server, FS* fs, SecurityManager* securityManager) :
|
||||||
_httpEndpoint(NTPSettings::read, NTPSettings::update, this, server, NTP_SETTINGS_SERVICE_PATH, securityManager),
|
_httpEndpoint(NTPSettings::read, NTPSettings::update, this, server, NTP_SETTINGS_SERVICE_PATH, securityManager),
|
||||||
_fsPersistence(NTPSettings::read, NTPSettings::update, this, fs, NTP_SETTINGS_FILE) {
|
_fsPersistence(NTPSettings::read, NTPSettings::update, this, fs, NTP_SETTINGS_FILE),
|
||||||
|
_timeHandler(TIME_PATH,
|
||||||
|
std::bind(&NTPSettingsService::configureTime, this, std::placeholders::_1, std::placeholders::_2)) {
|
||||||
|
_timeHandler.setMethod(HTTP_POST);
|
||||||
|
_timeHandler.setMaxContentLength(MAX_TIME_SIZE);
|
||||||
|
server->addHandler(&_timeHandler);
|
||||||
#ifdef ESP32
|
#ifdef ESP32
|
||||||
WiFi.onEvent(
|
WiFi.onEvent(
|
||||||
std::bind(&NTPSettingsService::onStationModeDisconnected, this, std::placeholders::_1, std::placeholders::_2),
|
std::bind(&NTPSettingsService::onStationModeDisconnected, this, std::placeholders::_1, std::placeholders::_2),
|
||||||
@ -54,6 +59,30 @@ void NTPSettingsService::configureNTP() {
|
|||||||
configTime(_state.tzFormat.c_str(), _state.server.c_str());
|
configTime(_state.tzFormat.c_str(), _state.server.c_str());
|
||||||
#endif
|
#endif
|
||||||
} else {
|
} else {
|
||||||
|
#ifdef ESP32
|
||||||
|
setenv("TZ", _state.tzFormat.c_str(), 1);
|
||||||
|
tzset();
|
||||||
|
#elif defined(ESP8266)
|
||||||
|
setTZ(_state.tzFormat.c_str());
|
||||||
|
#endif
|
||||||
sntp_stop();
|
sntp_stop();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void NTPSettingsService::configureTime(AsyncWebServerRequest* request, JsonVariant& json) {
|
||||||
|
if (!sntp_enabled() && json.is<JsonObject>()) {
|
||||||
|
String timeUtc = json["time_utc"];
|
||||||
|
struct tm tm = {0};
|
||||||
|
char* s = strptime(timeUtc.c_str(), "%Y-%m-%dT%H:%M:%SZ", &tm);
|
||||||
|
if (s != nullptr) {
|
||||||
|
time_t time = mktime(&tm);
|
||||||
|
struct timeval now = {.tv_sec = time};
|
||||||
|
settimeofday(&now, nullptr);
|
||||||
|
AsyncWebServerResponse* response = request->beginResponse(200);
|
||||||
|
request->send(response);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
AsyncWebServerResponse* response = request->beginResponse(400);
|
||||||
|
request->send(response);
|
||||||
|
}
|
||||||
|
@ -30,6 +30,9 @@
|
|||||||
#define NTP_SETTINGS_FILE "/config/ntpSettings.json"
|
#define NTP_SETTINGS_FILE "/config/ntpSettings.json"
|
||||||
#define NTP_SETTINGS_SERVICE_PATH "/rest/ntpSettings"
|
#define NTP_SETTINGS_SERVICE_PATH "/rest/ntpSettings"
|
||||||
|
|
||||||
|
#define MAX_TIME_SIZE 256
|
||||||
|
#define TIME_PATH "/rest/time"
|
||||||
|
|
||||||
class NTPSettings {
|
class NTPSettings {
|
||||||
public:
|
public:
|
||||||
bool enabled;
|
bool enabled;
|
||||||
@ -62,6 +65,7 @@ class NTPSettingsService : public StatefulService<NTPSettings> {
|
|||||||
private:
|
private:
|
||||||
HttpEndpoint<NTPSettings> _httpEndpoint;
|
HttpEndpoint<NTPSettings> _httpEndpoint;
|
||||||
FSPersistence<NTPSettings> _fsPersistence;
|
FSPersistence<NTPSettings> _fsPersistence;
|
||||||
|
AsyncCallbackJsonWebHandler _timeHandler;
|
||||||
|
|
||||||
#ifdef ESP32
|
#ifdef ESP32
|
||||||
void onStationModeGotIP(WiFiEvent_t event, WiFiEventInfo_t info);
|
void onStationModeGotIP(WiFiEvent_t event, WiFiEventInfo_t info);
|
||||||
@ -74,6 +78,7 @@ class NTPSettingsService : public StatefulService<NTPSettings> {
|
|||||||
void onStationModeDisconnected(const WiFiEventStationModeDisconnected& event);
|
void onStationModeDisconnected(const WiFiEventStationModeDisconnected& event);
|
||||||
#endif
|
#endif
|
||||||
void configureNTP();
|
void configureNTP();
|
||||||
|
void configureTime(AsyncWebServerRequest* request, JsonVariant& json);
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif // end NTPSettingsService_h
|
#endif // end NTPSettingsService_h
|
||||||
|
Loading…
Reference in New Issue
Block a user