diff --git a/interface/src/api/Endpoints.ts b/interface/src/api/Endpoints.ts index c81ccc8..61a62ab 100644 --- a/interface/src/api/Endpoints.ts +++ b/interface/src/api/Endpoints.ts @@ -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"; diff --git a/interface/src/ntp/NTPStatusForm.tsx b/interface/src/ntp/NTPStatusForm.tsx index 72b4500..6b277dd 100644 --- a/interface/src/ntp/NTPStatusForm.tsx +++ b/interface/src/ntp/NTPStatusForm.tsx @@ -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 & WithTheme & AuthenticatedContextProps; -type NTPStatusFormProps = RestFormProps & WithTheme; +interface NTPStatusFormState { + settingTime: boolean; + localTime: string; + processing: boolean; +} -class NTPStatusForm extends Component { +class NTPStatusForm extends Component { + + constructor(props: NTPStatusFormProps) { + super(props); + this.state = { + settingTime: false, + localTime: '', + processing: false + }; + } + + updateLocalTime = (event: React.ChangeEvent) => { + 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 ( + + Set Time + + Enter local date and time below to set the device's time. + + + + + + + + ) + } render() { const { data, theme } = this.props + const me = this.props.authenticatedContext.me; return ( @@ -40,19 +139,10 @@ class NTPStatusForm extends Component { - - - - - - - - - - + - + @@ -60,10 +150,19 @@ class NTPStatusForm extends Component { - + - + + + + + + + + + + @@ -76,14 +175,24 @@ class NTPStatusForm extends Component { - - } variant="contained" color="secondary" onClick={this.props.loadData}> - Refresh - - + + + } variant="contained" color="secondary" onClick={this.props.loadData}> + Refresh + + + {me.admin && !isNtpActive(data) && ( + + + + )} + + {this.renderSetTimeDialog()} ); } } -export default withTheme(NTPStatusForm); +export default withAuthenticatedContext(withTheme(NTPStatusForm)); diff --git a/interface/src/ntp/TimeFormat.ts b/interface/src/ntp/TimeFormat.ts index 9319e3a..7e0bb82 100644 --- a/interface/src/ntp/TimeFormat.ts +++ b/interface/src/ntp/TimeFormat.ts @@ -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'); diff --git a/interface/src/ntp/types.ts b/interface/src/ntp/types.ts index 6ee4610..a266d12 100644 --- a/interface/src/ntp/types.ts +++ b/interface/src/ntp/types.ts @@ -17,3 +17,7 @@ export interface NTPSettings { tz_label: string; tz_format: string; } + +export interface Time { + time_utc: string; +} diff --git a/interface/src/security/UserForm.tsx b/interface/src/security/UserForm.tsx index 66bd97e..8eefa7d 100644 --- a/interface/src/security/UserForm.tsx +++ b/interface/src/security/UserForm.tsx @@ -70,12 +70,12 @@ class UserForm extends React.Component { /> - - Done - Cancel + + Done + diff --git a/interface/src/system/SystemStatusForm.tsx b/interface/src/system/SystemStatusForm.tsx index eb83ae9..3631863 100644 --- a/interface/src/system/SystemStatusForm.tsx +++ b/interface/src/system/SystemStatusForm.tsx @@ -118,12 +118,12 @@ class SystemStatusForm extends Component - + ) @@ -165,12 +165,12 @@ class SystemStatusForm extends Component - } variant="contained" onClick={this.onFactoryResetConfirmed} disabled={this.state.processing} autoFocus> - Factory Reset - + } variant="contained" onClick={this.onFactoryResetConfirmed} disabled={this.state.processing} autoFocus> + Factory Reset + ) diff --git a/lib/framework/NTPSettingsService.cpp b/lib/framework/NTPSettingsService.cpp index 40f3772..c494302 100644 --- a/lib/framework/NTPSettingsService.cpp +++ b/lib/framework/NTPSettingsService.cpp @@ -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()) { + 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); +} diff --git a/lib/framework/NTPSettingsService.h b/lib/framework/NTPSettingsService.h index 5749e3b..bf25ca4 100644 --- a/lib/framework/NTPSettingsService.h +++ b/lib/framework/NTPSettingsService.h @@ -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 { private: HttpEndpoint _httpEndpoint; FSPersistence _fsPersistence; + AsyncCallbackJsonWebHandler _timeHandler; #ifdef ESP32 void onStationModeGotIP(WiFiEvent_t event, WiFiEventInfo_t info); @@ -74,6 +78,7 @@ class NTPSettingsService : public StatefulService { void onStationModeDisconnected(const WiFiEventStationModeDisconnected& event); #endif void configureNTP(); + void configureTime(AsyncWebServerRequest* request, JsonVariant& json); }; #endif // end NTPSettingsService_h