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 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";
|
||||
|
@ -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));
|
||||
|
@ -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');
|
||||
|
@ -17,3 +17,7 @@ export interface NTPSettings {
|
||||
tz_label: string;
|
||||
tz_format: string;
|
||||
}
|
||||
|
||||
export interface Time {
|
||||
time_utc: string;
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
)
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user