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:
rjwats 2020-06-16 22:24:15 +01:00 committed by GitHub
parent 003fd8af28
commit 55511e0611
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 187 additions and 37 deletions

View File

@ -3,6 +3,7 @@ import { ENDPOINT_ROOT } from './Env';
export const FEATURES_ENDPOINT = ENDPOINT_ROOT + "features";
export const NTP_STATUS_ENDPOINT = ENDPOINT_ROOT + "ntpStatus";
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_STATUS_ENDPOINT = ENDPOINT_ROOT + "apStatus";
export const SCAN_NETWORKS_ENDPOINT = ENDPOINT_ROOT + "scanNetworks";

View File

@ -2,7 +2,8 @@ import React, { Component, Fragment } from 'react';
import moment from 'moment';
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 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 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 { formatIsoDateTime } from './TimeFormat';
import { NTPStatus } from './types';
import { formatIsoDateTime, formatLocalDateTime } from './TimeFormat';
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() {
const { data, theme } = this.props
const me = this.props.authenticatedContext.me;
return (
<Fragment>
<List>
@ -37,6 +136,17 @@ class NTPStatusForm extends Component<NTPStatusFormProps> {
<Divider variant="inset" component="li" />
{isNtpActive(data) && (
<Fragment>
<ListItem>
<ListItemAvatar>
<Avatar>
<DNSIcon />
</Avatar>
</ListItemAvatar>
<ListItemText primary="NTP Server" secondary={data.server} />
</ListItem>
<Divider variant="inset" component="li" />
</Fragment>
)}
<ListItem>
<ListItemAvatar>
<Avatar>
@ -55,17 +165,6 @@ class NTPStatusForm extends Component<NTPStatusFormProps> {
<ListItemText primary="UTC Time" secondary={formatIsoDateTime(data.time_utc)} />
</ListItem>
<Divider variant="inset" component="li" />
</Fragment>
)}
<ListItem>
<ListItemAvatar>
<Avatar>
<DNSIcon />
</Avatar>
</ListItemAvatar>
<ListItemText primary="NTP Server" secondary={data.server} />
</ListItem>
<Divider variant="inset" component="li" />
<ListItem>
<ListItemAvatar>
<Avatar>
@ -76,14 +175,24 @@ class NTPStatusForm extends Component<NTPStatusFormProps> {
</ListItem>
<Divider variant="inset" component="li" />
</List>
<FormActions>
<Box display="flex" flexWrap="wrap">
<Box flexGrow={1} padding={1}>
<FormButton startIcon={<RefreshIcon />} variant="contained" color="secondary" onClick={this.props.loadData}>
Refresh
</FormButton>
</FormActions>
</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>
);
}
}
export default withTheme(NTPStatusForm);
export default withAuthenticatedContext(withTheme(NTPStatusForm));

View File

@ -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 formatLocalDateTime = (moment: Moment) => moment.format('YYYY-MM-DDTHH:mm');

View File

@ -17,3 +17,7 @@ export interface NTPSettings {
tz_label: string;
tz_format: string;
}
export interface Time {
time_utc: string;
}

View File

@ -70,12 +70,12 @@ class UserForm extends React.Component<UserFormProps> {
/>
</DialogContent>
<DialogActions>
<FormButton variant="contained" color="primary" type="submit" onClick={this.submit}>
Done
</FormButton>
<FormButton variant="contained" color="secondary" onClick={onCancelEditing}>
Cancel
</FormButton>
<FormButton variant="contained" color="primary" type="submit" onClick={this.submit}>
Done
</FormButton>
</DialogActions>
</Dialog>
</ValidatorForm>

View File

@ -118,12 +118,12 @@ class SystemStatusForm extends Component<SystemStatusFormProps, SystemStatusForm
Are you sure you want to restart the device?
</DialogContent>
<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">
Cancel
</Button>
<Button startIcon={<PowerSettingsNewIcon />} variant="contained" onClick={this.onRestartConfirmed} disabled={this.state.processing} color="primary" autoFocus>
Restart
</Button>
</DialogActions>
</Dialog>
)
@ -165,12 +165,12 @@ class SystemStatusForm extends Component<SystemStatusFormProps, SystemStatusForm
Are you sure you want to reset the device to its factory defaults?
</DialogContent>
<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">
Cancel
</Button>
<ErrorButton startIcon={<SettingsBackupRestoreIcon />} variant="contained" onClick={this.onFactoryResetConfirmed} disabled={this.state.processing} autoFocus>
Factory Reset
</ErrorButton>
</DialogActions>
</Dialog>
)

View File

@ -2,7 +2,12 @@
NTPSettingsService::NTPSettingsService(AsyncWebServer* server, FS* fs, SecurityManager* 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
WiFi.onEvent(
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());
#endif
} else {
#ifdef ESP32
setenv("TZ", _state.tzFormat.c_str(), 1);
tzset();
#elif defined(ESP8266)
setTZ(_state.tzFormat.c_str());
#endif
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);
}

View File

@ -30,6 +30,9 @@
#define NTP_SETTINGS_FILE "/config/ntpSettings.json"
#define NTP_SETTINGS_SERVICE_PATH "/rest/ntpSettings"
#define MAX_TIME_SIZE 256
#define TIME_PATH "/rest/time"
class NTPSettings {
public:
bool enabled;
@ -62,6 +65,7 @@ class NTPSettingsService : public StatefulService<NTPSettings> {
private:
HttpEndpoint<NTPSettings> _httpEndpoint;
FSPersistence<NTPSettings> _fsPersistence;
AsyncCallbackJsonWebHandler _timeHandler;
#ifdef ESP32
void onStationModeGotIP(WiFiEvent_t event, WiFiEventInfo_t info);
@ -74,6 +78,7 @@ class NTPSettingsService : public StatefulService<NTPSettings> {
void onStationModeDisconnected(const WiFiEventStationModeDisconnected& event);
#endif
void configureNTP();
void configureTime(AsyncWebServerRequest* request, JsonVariant& json);
};
#endif // end NTPSettingsService_h