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:
		| @@ -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> | ||||
| @@ -40,19 +139,10 @@ class NTPStatusForm extends Component<NTPStatusFormProps> { | ||||
|               <ListItem> | ||||
|                 <ListItemAvatar> | ||||
|                   <Avatar> | ||||
|                     <AccessTimeIcon /> | ||||
|                     <DNSIcon /> | ||||
|                   </Avatar> | ||||
|                 </ListItemAvatar> | ||||
|                 <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)} /> | ||||
|                 <ListItemText primary="NTP Server" secondary={data.server} /> | ||||
|               </ListItem> | ||||
|               <Divider variant="inset" component="li" /> | ||||
|             </Fragment> | ||||
| @@ -60,10 +150,19 @@ class NTPStatusForm extends Component<NTPStatusFormProps> { | ||||
|           <ListItem> | ||||
|             <ListItemAvatar> | ||||
|               <Avatar> | ||||
|                 <DNSIcon /> | ||||
|                 <AccessTimeIcon /> | ||||
|               </Avatar> | ||||
|             </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> | ||||
|           <Divider variant="inset" component="li" /> | ||||
|           <ListItem> | ||||
| @@ -76,14 +175,24 @@ class NTPStatusForm extends Component<NTPStatusFormProps> { | ||||
|           </ListItem> | ||||
|           <Divider variant="inset" component="li" /> | ||||
|         </List> | ||||
|         <FormActions> | ||||
|           <FormButton startIcon={<RefreshIcon />} variant="contained" color="secondary" onClick={this.props.loadData}> | ||||
|             Refresh | ||||
|           </FormButton> | ||||
|         </FormActions> | ||||
|         <Box display="flex" flexWrap="wrap"> | ||||
|           <Box flexGrow={1} padding={1}> | ||||
|             <FormButton startIcon={<RefreshIcon />} variant="contained" color="secondary" onClick={this.props.loadData}> | ||||
|               Refresh | ||||
|             </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> | ||||
|     ); | ||||
|   } | ||||
| } | ||||
|  | ||||
| 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 | ||||
|   | ||||
		Reference in New Issue
	
	Block a user