diff --git a/interface/src/AppRouting.tsx b/interface/src/AppRouting.tsx
index 4017d9d14..4861c715d 100644
--- a/interface/src/AppRouting.tsx
+++ b/interface/src/AppRouting.tsx
@@ -47,10 +47,7 @@ const AppRouting: FC = () => {
} />
- }
- />
+ } />
{features.security && (
void;
}
-export const uploadFile = (url: string, file: File, config?: FileUploadConfig): AxiosPromise => {
+export const startUploadFile = (url: string, file: File, config?: FileUploadConfig): AxiosPromise => {
const formData = new FormData();
formData.append('file', file);
diff --git a/interface/src/api/system.ts b/interface/src/api/system.ts
index 4db11723d..f93ff4853 100644
--- a/interface/src/api/system.ts
+++ b/interface/src/api/system.ts
@@ -2,7 +2,7 @@ import { AxiosPromise } from 'axios';
import { OTASettings, SystemStatus, LogSettings, LogEntries } from '../types';
-import { AXIOS, AXIOS_BIN, FileUploadConfig, uploadFile } from './endpoints';
+import { AXIOS, AXIOS_BIN, FileUploadConfig, startUploadFile } from './endpoints';
export function readSystemStatus(timeout?: number): AxiosPromise {
return AXIOS.get('/systemStatus', { timeout });
@@ -24,8 +24,8 @@ export function updateOTASettings(otaSettings: OTASettings): AxiosPromise =>
- uploadFile('/uploadFirmware', file, config);
+export const uploadFile = (file: File, config?: FileUploadConfig): AxiosPromise =>
+ startUploadFile('/uploadFile', file, config);
export function readLogSettings(): AxiosPromise {
return AXIOS.get('/logSettings');
diff --git a/interface/src/components/upload/SingleUpload.tsx b/interface/src/components/upload/SingleUpload.tsx
index f1cb6fb18..acdc15071 100644
--- a/interface/src/components/upload/SingleUpload.tsx
+++ b/interface/src/components/upload/SingleUpload.tsx
@@ -32,7 +32,8 @@ const SingleUpload: FC = ({ onDrop, onCancel, uploading, prog
const dropzoneState = useDropzone({
onDrop,
accept: {
- 'application/octet-stream': ['.bin']
+ 'application/octet-stream': ['.bin'],
+ 'application/json': ['.json']
},
disabled: uploading,
multiple: false
diff --git a/interface/src/components/upload/useFileUpload.ts b/interface/src/components/upload/useFileUpload.ts
index b7529020f..0cc12b1aa 100644
--- a/interface/src/components/upload/useFileUpload.ts
+++ b/interface/src/components/upload/useFileUpload.ts
@@ -42,7 +42,7 @@ const useFileUpload = ({ upload }: MediaUploadOptions) => {
cancelToken: cancelToken.token
});
resetUploadingStates();
- enqueueSnackbar('Upload successful', { variant: 'success' });
+ enqueueSnackbar('File uploaded', { variant: 'success' });
} catch (error: unknown) {
if (axios.isCancel(error)) {
enqueueSnackbar('Upload aborted', { variant: 'warning' });
diff --git a/interface/src/framework/system/FirmwareFileUpload.tsx b/interface/src/framework/system/FirmwareFileUpload.tsx
deleted file mode 100644
index a0b212fdf..000000000
--- a/interface/src/framework/system/FirmwareFileUpload.tsx
+++ /dev/null
@@ -1,33 +0,0 @@
-import { AxiosPromise } from 'axios';
-import { FC } from 'react';
-
-import { FileUploadConfig } from '../../api/endpoints';
-import { MessageBox, SingleUpload, useFileUpload } from '../../components';
-
-interface UploadFirmwareProps {
- uploadFirmware: (file: File, config?: FileUploadConfig) => AxiosPromise;
-}
-
-const FirmwareFileUpload: FC = ({ uploadFirmware }) => {
- const [uploadFile, cancelUpload, uploading, uploadProgress] = useFileUpload({ upload: uploadFirmware });
-
- return (
- <>
- {!uploading && (
-
- )}
-
- >
- );
-};
-
-export default FirmwareFileUpload;
diff --git a/interface/src/framework/system/GeneralFileUpload.tsx b/interface/src/framework/system/GeneralFileUpload.tsx
new file mode 100644
index 000000000..8f084b69e
--- /dev/null
+++ b/interface/src/framework/system/GeneralFileUpload.tsx
@@ -0,0 +1,28 @@
+import { AxiosPromise } from 'axios';
+import { FC } from 'react';
+
+import { FileUploadConfig } from '../../api/endpoints';
+import { MessageBox, SingleUpload, useFileUpload } from '../../components';
+
+interface UploadFileProps {
+ uploadGeneralFile: (file: File, config?: FileUploadConfig) => AxiosPromise;
+}
+
+const GeneralFileUpload: FC = ({ uploadGeneralFile }) => {
+ const [uploadFile, cancelUpload, uploading, uploadProgress] = useFileUpload({ upload: uploadGeneralFile });
+
+ return (
+ <>
+ {!uploading && (
+
+ )}
+
+ >
+ );
+};
+
+export default GeneralFileUpload;
diff --git a/interface/src/framework/system/FirmwareRestartMonitor.tsx b/interface/src/framework/system/RestartMonitor.tsx
similarity index 89%
rename from interface/src/framework/system/FirmwareRestartMonitor.tsx
rename to interface/src/framework/system/RestartMonitor.tsx
index 7dd050c6e..3e832f302 100644
--- a/interface/src/framework/system/FirmwareRestartMonitor.tsx
+++ b/interface/src/framework/system/RestartMonitor.tsx
@@ -8,7 +8,7 @@ const RESTART_TIMEOUT = 2 * 60 * 1000;
const POLL_TIMEOUT = 2000;
const POLL_INTERVAL = 5000;
-const FirmwareRestartMonitor: FC = () => {
+const RestartMonitor: FC = () => {
const [failed, setFailed] = useState(false);
const [timeoutId, setTimeoutId] = useState();
@@ -16,7 +16,7 @@ const FirmwareRestartMonitor: FC = () => {
const poll = useRef(async () => {
try {
await SystemApi.readSystemStatus(POLL_TIMEOUT);
- document.location.href = '/firmwareUpdated';
+ document.location.href = '/fileUpdated';
} catch (error: unknown) {
if (new Date().getTime() < timeoutAt.current) {
setTimeoutId(setTimeout(poll.current, POLL_INTERVAL));
@@ -40,4 +40,4 @@ const FirmwareRestartMonitor: FC = () => {
);
};
-export default FirmwareRestartMonitor;
+export default RestartMonitor;
diff --git a/interface/src/framework/system/System.tsx b/interface/src/framework/system/System.tsx
index 553f60ad0..07cf8296d 100644
--- a/interface/src/framework/system/System.tsx
+++ b/interface/src/framework/system/System.tsx
@@ -6,7 +6,7 @@ import { Tab } from '@mui/material';
import { useRouterTab, RouterTabs, useLayoutTitle, RequireAdmin } from '../../components';
import { AuthenticatedContext } from '../../contexts/authentication';
import { FeaturesContext } from '../../contexts/features';
-import UploadFirmwareForm from './UploadFirmwareForm';
+import UploadFileForm from './UploadFileForm';
import SystemStatusForm from './SystemStatusForm';
import OTASettingsForm from './OTASettingsForm';
@@ -26,7 +26,7 @@ const System: FC = () => {
{features.ota && }
- {features.upload_firmware && }
+ {features.upload_firmware && }
} />
@@ -46,7 +46,7 @@ const System: FC = () => {
path="upload"
element={
-
+
}
/>
diff --git a/interface/src/framework/system/SystemStatusForm.tsx b/interface/src/framework/system/SystemStatusForm.tsx
index 62c333fce..52c1a3a4b 100644
--- a/interface/src/framework/system/SystemStatusForm.tsx
+++ b/interface/src/framework/system/SystemStatusForm.tsx
@@ -159,7 +159,7 @@ const SystemStatusForm: FC = () => {
Use
- {'UPLOAD FIRMWARE'}
+ {'UPLOAD'}
to apply the new firmware
diff --git a/interface/src/framework/system/UploadFileForm.tsx b/interface/src/framework/system/UploadFileForm.tsx
new file mode 100644
index 000000000..29f161310
--- /dev/null
+++ b/interface/src/framework/system/UploadFileForm.tsx
@@ -0,0 +1,26 @@
+import { FC, useRef, useState } from 'react';
+
+import * as SystemApi from '../../api/system';
+import { SectionContent } from '../../components';
+import { FileUploadConfig } from '../../api/endpoints';
+
+import GeneralFileUpload from './GeneralFileUpload';
+import RestartMonitor from './RestartMonitor';
+
+const UploadFileForm: FC = () => {
+ const [restarting, setRestarting] = useState();
+
+ const uploadFile = useRef(async (file: File, config?: FileUploadConfig) => {
+ const response = await SystemApi.uploadFile(file, config);
+ setRestarting(true);
+ return response;
+ });
+
+ return (
+
+ {restarting ? : }
+
+ );
+};
+
+export default UploadFileForm;
diff --git a/interface/src/framework/system/UploadFirmwareForm.tsx b/interface/src/framework/system/UploadFirmwareForm.tsx
deleted file mode 100644
index 5af94f0de..000000000
--- a/interface/src/framework/system/UploadFirmwareForm.tsx
+++ /dev/null
@@ -1,26 +0,0 @@
-import { FC, useRef, useState } from 'react';
-
-import * as SystemApi from '../../api/system';
-import { SectionContent } from '../../components';
-import { FileUploadConfig } from '../../api/endpoints';
-
-import FirmwareFileUpload from './FirmwareFileUpload';
-import FirmwareRestartMonitor from './FirmwareRestartMonitor';
-
-const UploadFirmwareForm: FC = () => {
- const [restarting, setRestarting] = useState();
-
- const uploadFirmware = useRef(async (file: File, config?: FileUploadConfig) => {
- const response = await SystemApi.uploadFirmware(file, config);
- setRestarting(true);
- return response;
- });
-
- return (
-
- {restarting ? : }
-
- );
-};
-
-export default UploadFirmwareForm;
diff --git a/interface/src/project/HelpInformation.tsx b/interface/src/project/HelpInformation.tsx
index 803f30b83..e081facca 100644
--- a/interface/src/project/HelpInformation.tsx
+++ b/interface/src/project/HelpInformation.tsx
@@ -36,7 +36,7 @@ const HelpInformation: FC = () => {
} else {
const json = response.data;
const a = document.createElement('a');
- const filename = 'emsesp_' + endpoint + '.txt';
+ const filename = 'emsesp_' + endpoint + '.json';
a.href = URL.createObjectURL(
new Blob([JSON.stringify(json, null, 2)], {
type: 'text/plain'
@@ -112,26 +112,30 @@ const HelpInformation: FC = () => {
{me.admin && (
<>
-
+
Export Data
-
- Download the current system information, application settings and any customizations using the buttons
- below.
+
+ Download the current system information to show EMS statistics and connected devices
+
+
+ } variant="outlined" color="secondary" onClick={() => onDownload('info')}>
+ system info
+
+
+
+ Export the application settings and any customizations to a JSON file. These files can later be uploaded
+ via the System menu.
+
+
+ Be careful when sharing the settings as the file contains passwords and other sensitive system
+ information.
- }
- variant="outlined"
- color="secondary"
- onClick={() => onDownload('info')}
- >
- system info
-
}
variant="outlined"
@@ -157,7 +161,7 @@ const HelpInformation: FC = () => {
EMS-ESP is a free and open-source project.
Please consider supporting us by giving it a
- on
+ on
{'GitHub'}
diff --git a/lib/framework/ESP8266React.cpp b/lib/framework/ESP8266React.cpp
index d3e38b9b6..e83b284cf 100644
--- a/lib/framework/ESP8266React.cpp
+++ b/lib/framework/ESP8266React.cpp
@@ -13,7 +13,7 @@ ESP8266React::ESP8266React(AsyncWebServer * server, FS * fs)
, _ntpSettingsService(server, fs, &_securitySettingsService)
, _ntpStatus(server, &_securitySettingsService)
, _otaSettingsService(server, fs, &_securitySettingsService)
- , _uploadFirmwareService(server, &_securitySettingsService)
+ , _uploadFileService(server, &_securitySettingsService)
, _mqttSettingsService(server, fs, &_securitySettingsService)
, _mqttStatus(server, &_mqttSettingsService, &_securitySettingsService)
, _authenticationService(server, &_securitySettingsService)
diff --git a/lib/framework/ESP8266React.h b/lib/framework/ESP8266React.h
index fc8f8f841..953232f78 100644
--- a/lib/framework/ESP8266React.h
+++ b/lib/framework/ESP8266React.h
@@ -16,7 +16,7 @@
#include
#include
#include
-#include
+#include
#include
#include
#include
@@ -78,7 +78,7 @@ class ESP8266React {
NTPSettingsService _ntpSettingsService;
NTPStatus _ntpStatus;
OTASettingsService _otaSettingsService;
- UploadFirmwareService _uploadFirmwareService;
+ UploadFileService _uploadFileService;
MqttSettingsService _mqttSettingsService;
MqttStatus _mqttStatus;
AuthenticationService _authenticationService;
diff --git a/lib/framework/FSPersistence.h b/lib/framework/FSPersistence.h
index f179170c0..52c96d7ed 100644
--- a/lib/framework/FSPersistence.h
+++ b/lib/framework/FSPersistence.h
@@ -30,12 +30,12 @@ class FSPersistence {
DynamicJsonDocument jsonDocument = DynamicJsonDocument(_bufferSize);
DeserializationError error = deserializeJson(jsonDocument, settingsFile);
if (error == DeserializationError::Ok && jsonDocument.is()) {
- // jsonDocument.shrinkToFit(); // added by proddy
JsonObject jsonObject = jsonDocument.as();
// debug added by Proddy
#if defined(EMSESP_DEBUG)
#if defined(EMSESP_USE_SERIAL)
+ Serial.println();
Serial.printf("Reading file: %s: ", _filePath);
serializeJson(jsonDocument, Serial);
Serial.println();
@@ -49,9 +49,17 @@ class FSPersistence {
settingsFile.close();
}
- // If we reach here we have not been successful in loading the config,
- // hard-coded emergency defaults are now applied.
+// If we reach here we have not been successful in loading the config,
+// hard-coded emergency defaults are now applied.
+#if defined(EMSESP_DEBUG)
+#if defined(EMSESP_USE_SERIAL)
+ Serial.println();
+ Serial.printf("Applying defaults for %s: ", _filePath);
+ Serial.println();
+#endif
+#endif
applyDefaults();
+ writeToFS(); // added to make sure the initial file is created
}
bool writeToFS() {
diff --git a/lib/framework/UploadFileService.cpp b/lib/framework/UploadFileService.cpp
new file mode 100644
index 000000000..51bdf1656
--- /dev/null
+++ b/lib/framework/UploadFileService.cpp
@@ -0,0 +1,122 @@
+#include
+
+using namespace std::placeholders; // for `_1` etc
+
+static bool is_firmware = false;
+
+UploadFileService::UploadFileService(AsyncWebServer * server, SecurityManager * securityManager)
+ : _securityManager(securityManager) {
+ server->on(UPLOAD_FILE_PATH,
+ HTTP_POST,
+ std::bind(&UploadFileService::uploadComplete, this, _1),
+ std::bind(&UploadFileService::handleUpload, this, _1, _2, _3, _4, _5, _6));
+}
+
+void UploadFileService::handleUpload(AsyncWebServerRequest * request, const String & filename, size_t index, uint8_t * data, size_t len, bool final) {
+ // quit if not authorized
+ Authentication authentication = _securityManager->authenticateRequest(request);
+ if (!AuthenticationPredicates::IS_ADMIN(authentication)) {
+ handleError(request, 403); // send the forbidden response
+ return;
+ }
+
+ // at init
+ if (!index) {
+ // check details of the file, to see if its a valid bin or json file
+ std::string fname(filename.c_str());
+ auto position = fname.find_last_of(".");
+ std::string extension = fname.substr(position + 1);
+ size_t fsize = request->contentLength();
+
+ Serial.printf("Received filename: %s, len: %d, index: %d, ext: %s, fsize: %d", filename.c_str(), len, index, extension.c_str(), fsize);
+ Serial.println();
+
+ if ((extension == "bin") && (fsize > 1500000)) {
+ is_firmware = true;
+ } else if (extension == "json") {
+ is_firmware = false;
+ } else {
+ is_firmware = false;
+ return; // not support file type
+ }
+
+ if (is_firmware) {
+ // it's firmware - initialize the ArduinoOTA updater
+ if (Update.begin(fsize)) {
+ request->onDisconnect(UploadFileService::handleEarlyDisconnect); // success, let's make sure we end the update if the client hangs up
+ } else {
+#if defined(EMSESP_USE_SERIAL)
+ Update.printError(Serial);
+#endif
+ handleError(request, 500); // failed to begin, send an error response
+ }
+ } else {
+ // its a normal file, open a new temp file to write the contents too
+ request->_tempFile = LITTLEFS.open(TEMP_FILENAME_PATH, "w");
+ }
+ }
+
+ if (!is_firmware) {
+ if (len) {
+ request->_tempFile.write(data, len); // stream the incoming chunk to the opened file
+ }
+
+ } else {
+ // if we haven't delt with an error, continue with the firmware update
+ if (!request->_tempObject) {
+ if (Update.write(data, len) != len) {
+#if defined(EMSESP_USE_SERIAL)
+ Update.printError(Serial);
+#endif
+ handleError(request, 500);
+ }
+ if (final) {
+ if (!Update.end(true)) {
+#if defined(EMSESP_USE_SERIAL)
+ Update.printError(Serial);
+#endif
+ handleError(request, 500);
+ }
+ }
+ }
+ }
+}
+
+void UploadFileService::uploadComplete(AsyncWebServerRequest * request) {
+ // did we complete uploading a json file?
+ if (request->_tempFile) {
+ request->_tempFile.close(); // close the file handle as the upload is now done
+ request->onDisconnect(RestartService::restartNow);
+ AsyncWebServerResponse * response = request->beginResponse(200);
+ request->send(response);
+ return;
+ }
+
+ // check if it was a firmware upgrade
+ // if no error, send the success response
+ if (is_firmware && !request->_tempObject) {
+ request->onDisconnect(RestartService::restartNow);
+ AsyncWebServerResponse * response = request->beginResponse(200);
+ request->send(response);
+ return;
+ }
+
+ handleError(request, 403); // send the forbidden response
+}
+
+void UploadFileService::handleError(AsyncWebServerRequest * request, int code) {
+ // if we have had an error already, do nothing
+ if (request->_tempObject) {
+ return;
+ }
+
+ // send the error code to the client and record the error code in the temp object
+ request->_tempObject = new int(code);
+ AsyncWebServerResponse * response = request->beginResponse(code);
+ request->send(response);
+}
+
+void UploadFileService::handleEarlyDisconnect() {
+ is_firmware = false;
+ Update.abort();
+}
diff --git a/lib/framework/UploadFirmwareService.h b/lib/framework/UploadFileService.h
similarity index 67%
rename from lib/framework/UploadFirmwareService.h
rename to lib/framework/UploadFileService.h
index ec52fc9ee..fba0df0dd 100644
--- a/lib/framework/UploadFirmwareService.h
+++ b/lib/framework/UploadFileService.h
@@ -1,20 +1,23 @@
-#ifndef UploadFirmwareService_h
-#define UploadFirmwareService_h
+#ifndef UploadFileService_h
+#define UploadFileService_h
#include
#include
#include
+#include
+
#include
#include
#include
-#define UPLOAD_FIRMWARE_PATH "/rest/uploadFirmware"
+#define UPLOAD_FILE_PATH "/rest/uploadFile"
+#define TEMP_FILENAME_PATH "/tmp_upload"
-class UploadFirmwareService {
+class UploadFileService {
public:
- UploadFirmwareService(AsyncWebServer * server, SecurityManager * securityManager);
+ UploadFileService(AsyncWebServer * server, SecurityManager * securityManager);
private:
SecurityManager * _securityManager;
diff --git a/lib/framework/UploadFirmwareService.cpp b/lib/framework/UploadFirmwareService.cpp
deleted file mode 100644
index 18a20aecc..000000000
--- a/lib/framework/UploadFirmwareService.cpp
+++ /dev/null
@@ -1,68 +0,0 @@
-#include
-
-using namespace std::placeholders; // for `_1` etc
-
-UploadFirmwareService::UploadFirmwareService(AsyncWebServer * server, SecurityManager * securityManager)
- : _securityManager(securityManager) {
- server->on(UPLOAD_FIRMWARE_PATH,
- HTTP_POST,
- std::bind(&UploadFirmwareService::uploadComplete, this, _1),
- std::bind(&UploadFirmwareService::handleUpload, this, _1, _2, _3, _4, _5, _6));
-}
-
-void UploadFirmwareService::handleUpload(AsyncWebServerRequest * request, const String & filename, size_t index, uint8_t * data, size_t len, bool final) {
- if (!index) {
- Authentication authentication = _securityManager->authenticateRequest(request);
- if (AuthenticationPredicates::IS_ADMIN(authentication)) {
- if (Update.begin(request->contentLength())) {
- // success, let's make sure we end the update if the client hangs up
- request->onDisconnect(UploadFirmwareService::handleEarlyDisconnect);
- } else {
- // failed to begin, send an error response
- Update.printError(Serial);
- handleError(request, 500);
- }
- } else {
- // send the forbidden response
- handleError(request, 403);
- }
- }
-
- // if we haven't delt with an error, continue with the update
- if (!request->_tempObject) {
- if (Update.write(data, len) != len) {
- Update.printError(Serial);
- handleError(request, 500);
- }
- if (final) {
- if (!Update.end(true)) {
- Update.printError(Serial);
- handleError(request, 500);
- }
- }
- }
-}
-
-void UploadFirmwareService::uploadComplete(AsyncWebServerRequest * request) {
- // if no error, send the success response
- if (!request->_tempObject) {
- request->onDisconnect(RestartService::restartNow);
- AsyncWebServerResponse * response = request->beginResponse(200);
- request->send(response);
- }
-}
-
-void UploadFirmwareService::handleError(AsyncWebServerRequest * request, int code) {
- // if we have had an error already, do nothing
- if (request->_tempObject) {
- return;
- }
- // send the error code to the client and record the error code in the temp object
- request->_tempObject = new int(code);
- AsyncWebServerResponse * response = request->beginResponse(code);
- request->send(response);
-}
-
-void UploadFirmwareService::handleEarlyDisconnect() {
- Update.abort();
-}
diff --git a/lib_standalone/ESP8266React.h b/lib_standalone/ESP8266React.h
index 2b1bd5039..84b945a8a 100644
--- a/lib_standalone/ESP8266React.h
+++ b/lib_standalone/ESP8266React.h
@@ -17,6 +17,13 @@
#include
#include
+#define AP_SETTINGS_FILE "/config/apSettings.json"
+#define MQTT_SETTINGS_FILE "/config/mqttSettings.json"
+#define NETWORK_SETTINGS_FILE "/config/networkSettings.json"
+#define NTP_SETTINGS_FILE "/config/ntpSettings.json"
+#define EMSESP_SETTINGS_FILE "/config/emsespSettings.json"
+#define OTA_SETTINGS_FILE "/config/otaSettings.json"
+
class DummySettings {
public:
uint8_t tx_mode = 1;
@@ -26,6 +33,7 @@ class DummySettings {
uint32_t syslog_mark_interval = 0;
String syslog_host = "192.168.1.4";
uint16_t syslog_port = 514;
+ bool enableMDNS = false;
uint8_t master_thermostat = 0;
bool shower_timer = true;
bool shower_alert = false;
diff --git a/mock-api/server.js b/mock-api/server.js
index 9f3666962..33764ab66 100644
--- a/mock-api/server.js
+++ b/mock-api/server.js
@@ -249,7 +249,7 @@ const SYSTEM_STATUS_ENDPOINT = REST_ENDPOINT_ROOT + 'systemStatus'
const SECURITY_SETTINGS_ENDPOINT = REST_ENDPOINT_ROOT + 'securitySettings'
const RESTART_ENDPOINT = REST_ENDPOINT_ROOT + 'restart'
const FACTORY_RESET_ENDPOINT = REST_ENDPOINT_ROOT + 'factoryReset'
-const UPLOAD_FIRMWARE_ENDPOINT = REST_ENDPOINT_ROOT + 'uploadFirmware'
+const UPLOAD_FILE_ENDPOINT = REST_ENDPOINT_ROOT + 'uploadFile'
const SIGN_IN_ENDPOINT = REST_ENDPOINT_ROOT + 'signIn'
const GENERATE_TOKEN_ENDPOINT = REST_ENDPOINT_ROOT + 'generateToken'
const system_status = {
@@ -270,8 +270,8 @@ const system_status = {
security_settings = {
jwt_secret: 'naughty!',
users: [
- { id: 'admin', username: 'admin', password: 'admin', admin: true },
- { id: 'guest', username: 'guest', password: 'guest', admin: false },
+ { username: 'admin', password: 'admin', admin: true },
+ { username: 'guest', password: 'guest', admin: false },
],
}
const features = {
@@ -829,7 +829,7 @@ rest_server.post(RESTART_ENDPOINT, (req, res) => {
rest_server.post(FACTORY_RESET_ENDPOINT, (req, res) => {
res.sendStatus(200)
})
-rest_server.post(UPLOAD_FIRMWARE_ENDPOINT, (req, res) => {
+rest_server.post(UPLOAD_FILE_ENDPOINT, (req, res) => {
res.sendStatus(200)
})
rest_server.post(SIGN_IN_ENDPOINT, (req, res) => {
diff --git a/src/emsesp.cpp b/src/emsesp.cpp
index 4e37ee919..17dc3e889 100644
--- a/src/emsesp.cpp
+++ b/src/emsesp.cpp
@@ -1374,7 +1374,7 @@ void EMSESP::start() {
// start the file system
#ifndef EMSESP_STANDALONE
if (!LITTLEFS.begin(true)) {
- Serial.println("LITTLEFS Mount Failed. EMS-ESP stopped.");
+ Serial.println("LITTLEFS Mount failed. EMS-ESP stopped.");
return;
}
#endif
@@ -1383,6 +1383,12 @@ void EMSESP::start() {
webLogService.begin(); // start web log service. now we can start capturing logs to the web log
LOG_INFO(F("Last system reset reason Core0: %s, Core1: %s"), system_.reset_reason(0).c_str(), system_.reset_reason(1).c_str());
+ // do any system upgrades
+ if (system_.check_upgrade()) {
+ LOG_INFO(F("System will be restarted to apply upgrade"));
+ system_.system_restart();
+ };
+
webSettingsService.begin(); // load EMS-ESP Application settings...
system_.reload_settings(); // ... and store some of the settings locally
webCustomizationService.begin(); // load the customizations
@@ -1392,8 +1398,6 @@ void EMSESP::start() {
console_.start_telnet();
}
- system_.check_upgrade(); // do any system upgrades
-
// start all the EMS-ESP services
mqtt_.start(); // mqtt init
system_.start(); // starts commands, led, adc, button, network, syslog & uart
diff --git a/src/system.cpp b/src/system.cpp
index f0f03fad3..5e6cb16e4 100644
--- a/src/system.cpp
+++ b/src/system.cpp
@@ -889,10 +889,51 @@ void System::show_system(uuid::console::Shell & shell) {
#endif
}
-// upgrade from previous versions of EMS-ESP
-// returns true if an upgrade was done
+// handle upgrades from previous versions
+// or managing an uploaded files to replace settings files
+// returns true if we need a reboot
bool System::check_upgrade() {
- return false;
+ bool reboot_required = false;
+
+#ifndef EMSESP_STANDALONE
+ // see if we have a temp file, if so try and read it
+ File new_file = LITTLEFS.open(TEMP_FILENAME_PATH);
+ if (new_file) {
+ DynamicJsonDocument jsonDocument = DynamicJsonDocument(FS_BUFFER_SIZE);
+ DeserializationError error = deserializeJson(jsonDocument, new_file);
+ if (error == DeserializationError::Ok && jsonDocument.is()) {
+ JsonObject input = jsonDocument.as();
+ // see what type of file it is, either settings or customization. anything else is ignored
+ std::string settings_type = input["type"];
+ if (settings_type == "settings") {
+ // It's a settings file. Parse each section separately. If it's system related it will require a reboot
+ reboot_required = saveSettings(NETWORK_SETTINGS_FILE, "Network", input);
+ reboot_required |= saveSettings(AP_SETTINGS_FILE, "AP", input);
+ reboot_required |= saveSettings(MQTT_SETTINGS_FILE, "MQTT", input);
+ reboot_required |= saveSettings(NTP_SETTINGS_FILE, "NTP", input);
+ reboot_required |= saveSettings(SECURITY_SETTINGS_FILE, "Security", input);
+ reboot_required |= saveSettings(EMSESP_SETTINGS_FILE, "Settings", input);
+ } else if (settings_type == "customizations") {
+ // it's a customization file, just replace it and there's no need to reboot
+ LOG_INFO(F("Applying new customizations"));
+ new_file.close();
+ LITTLEFS.remove(EMSESP_CUSTOMIZATION_FILE);
+ LITTLEFS.rename(TEMP_FILENAME_PATH, EMSESP_CUSTOMIZATION_FILE);
+ return false; // no reboot required
+ } else {
+ LOG_ERROR(F("Unrecognized file uploaded"));
+ }
+ } else {
+ LOG_ERROR(F("Unrecognized file uploaded, not json"));
+ }
+
+ // close (just in case) and remove the file
+ new_file.close();
+ LITTLEFS.remove(TEMP_FILENAME_PATH);
+ }
+#endif
+
+ return reboot_required;
}
// list commands
@@ -900,191 +941,67 @@ bool System::command_commands(const char * value, const int8_t id, JsonObject &
return Command::list(EMSdevice::DeviceType::SYSTEM, output);
}
+// convert settings file into json object
+void System::extractSettings(const char * filename, const char * section, JsonObject & output) {
+#ifndef EMSESP_STANDALONE
+ File settingsFile = LITTLEFS.open(filename);
+ if (settingsFile) {
+ DynamicJsonDocument jsonDocument = DynamicJsonDocument(EMSESP_JSON_SIZE_XLARGE_DYN);
+ DeserializationError error = deserializeJson(jsonDocument, settingsFile);
+ if (error == DeserializationError::Ok && jsonDocument.is()) {
+ JsonObject jsonObject = jsonDocument.as();
+ JsonObject node = output.createNestedObject(section);
+ for (JsonPair kvp : jsonObject) {
+ node[kvp.key()] = kvp.value();
+ }
+ }
+ }
+ settingsFile.close();
+#endif
+}
+
+// save settings file using input from a json object
+bool System::saveSettings(const char * filename, const char * section, JsonObject & input) {
+#ifndef EMSESP_STANDALONE
+ JsonObject section_json = input[section];
+ if (section_json) {
+ File section_file = LITTLEFS.open(filename, "w");
+ if (section_file) {
+ LOG_INFO(F("Applying new %s settings"), section);
+ serializeJson(section_json, section_file);
+ section_file.close();
+ return true; // reboot required
+ }
+ }
+#endif
+ return false; // not found
+}
+
// export all settings to JSON text
+// we need to keep the original format so the import/upload works as we just replace files
// http://ems-esp/api/system/settings
-// value and id are ignored
-// note: ssid and passwords are excluded
bool System::command_settings(const char * value, const int8_t id, JsonObject & output) {
- output["label"] = "settings";
+ output["type"] = "settings";
JsonObject node = output.createNestedObject("System");
node["version"] = EMSESP_APP_VERSION;
- EMSESP::esp8266React.getNetworkSettingsService()->read([&](NetworkSettings & settings) {
- node = output.createNestedObject("Network");
- node["hostname"] = settings.hostname;
- node["static_ip_config"] = settings.staticIPConfig;
- node["enableIPv6"] = settings.enableIPv6;
- node["low_bandwidth"] = settings.bandwidth20;
- node["disable_sleep"] = settings.nosleep;
- JsonUtils::writeIP(node, "local_ip", settings.localIP);
- JsonUtils::writeIP(node, "gateway_ip", settings.gatewayIP);
- JsonUtils::writeIP(node, "subnet_mask", settings.subnetMask);
- JsonUtils::writeIP(node, "dns_ip_1", settings.dnsIP1);
- JsonUtils::writeIP(node, "dns_ip_2", settings.dnsIP2);
- });
-
-#ifndef EMSESP_STANDALONE
- EMSESP::esp8266React.getAPSettingsService()->read([&](APSettings & settings) {
- node = output.createNestedObject("AP");
- const char * pM[] = {"always", "disconnected", "never"};
- node["provision_mode"] = pM[settings.provisionMode];
- node["security"] = settings.password.length() ? "wpa2" : "open";
- node["ssid"] = settings.ssid;
- node["local_ip"] = settings.localIP.toString();
- node["gateway_ip"] = settings.gatewayIP.toString();
- node["subnet_mask"] = settings.subnetMask.toString();
- node["channel"] = settings.channel;
- node["ssid_hidden"] = settings.ssidHidden;
- node["max_clients"] = settings.maxClients;
- });
-#endif
-
- EMSESP::esp8266React.getMqttSettingsService()->read([&](MqttSettings & settings) {
- node = output.createNestedObject("MQTT");
- node["enabled"] = settings.enabled;
- node["host"] = settings.host;
- node["port"] = settings.port;
- node["username"] = settings.username;
- node["client_id"] = settings.clientId;
- node["keep_alive"] = settings.keepAlive;
- node["clean_session"] = settings.cleanSession;
- node["base"] = settings.base;
- node["discovery_prefix"] = settings.discovery_prefix;
- node["nested_format"] = settings.nested_format;
- node["ha_enabled"] = settings.ha_enabled;
- node["mqtt_qos"] = settings.mqtt_qos;
- node["mqtt_retain"] = settings.mqtt_retain;
- node["publish_time_boiler"] = settings.publish_time_boiler;
- node["publish_time_thermostat"] = settings.publish_time_thermostat;
- node["publish_time_solar"] = settings.publish_time_solar;
- node["publish_time_mixer"] = settings.publish_time_mixer;
- node["publish_time_other"] = settings.publish_time_other;
- node["publish_time_sensor"] = settings.publish_time_sensor;
- node["publish_single"] = settings.publish_single;
- node["publish_2_command"] = settings.publish_single2cmd;
- node["send_response"] = settings.send_response;
- });
-
-#ifndef EMSESP_STANDALONE
- EMSESP::esp8266React.getNTPSettingsService()->read([&](NTPSettings & settings) {
- node = output.createNestedObject("NTP");
- node["enabled"] = settings.enabled;
- node["server"] = settings.server;
- node["tz_label"] = settings.tzLabel;
- node["tz_format"] = settings.tzFormat;
- });
-
- EMSESP::esp8266React.getOTASettingsService()->read([&](OTASettings & settings) {
- node = output.createNestedObject("OTA");
- node["enabled"] = settings.enabled;
- node["port"] = settings.port;
- });
-#endif
-
- EMSESP::webSettingsService.read([&](WebSettings & settings) {
- node = output.createNestedObject("Settings");
-
- node["board_profile"] = settings.board_profile;
- node["tx_mode"] = settings.tx_mode;
- node["ems_bus_id"] = settings.ems_bus_id;
-
- node["syslog_enabled"] = settings.syslog_enabled;
- node["syslog_level"] = settings.syslog_level;
- node["syslog_mark_interval"] = settings.syslog_mark_interval;
- node["syslog_host"] = settings.syslog_host;
- node["syslog_port"] = settings.syslog_port;
-
- node["master_thermostat"] = settings.master_thermostat;
-
- node["shower_timer"] = settings.shower_timer;
- node["shower_alert"] = settings.shower_alert;
- if (settings.shower_alert) {
- node["shower_alert_coldshot"] = settings.shower_alert_coldshot / 1000; // seconds
- node["shower_alert_trigger"] = settings.shower_alert_trigger / 60000; // minutes
- }
-
- node["rx_gpio"] = settings.rx_gpio;
- node["tx_gpio"] = settings.tx_gpio;
- node["dallas_gpio"] = settings.dallas_gpio;
- node["pbutton_gpio"] = settings.pbutton_gpio;
- node["led_gpio"] = settings.led_gpio;
-
- node["hide_led"] = settings.hide_led;
- node["notoken_api"] = settings.notoken_api;
- node["readonly_mode"] = settings.readonly_mode;
-
- node["fahrenheit"] = settings.fahrenheit;
- node["dallas_parasite"] = settings.dallas_parasite;
- node["bool_format"] = settings.bool_format;
- node["bool_dashboard"] = settings.bool_dashboard;
- node["enum_format"] = settings.enum_format;
- node["analog_enabled"] = settings.analog_enabled;
- node["telnet_enabled"] = settings.telnet_enabled;
-
- node["phy_type"] = settings.phy_type;
- node["eth_power"] = settings.eth_power;
- node["eth_phy_addr"] = settings.eth_phy_addr;
- node["eth_clock_mode"] = settings.eth_clock_mode;
- });
+ extractSettings(NETWORK_SETTINGS_FILE, "Network", output);
+ extractSettings(AP_SETTINGS_FILE, "AP", output);
+ extractSettings(MQTT_SETTINGS_FILE, "MQTT", output);
+ extractSettings(NTP_SETTINGS_FILE, "NTP", output);
+ extractSettings(OTA_SETTINGS_FILE, "OTA", output);
+ extractSettings(SECURITY_SETTINGS_FILE, "Security", output);
+ extractSettings(EMSESP_SETTINGS_FILE, "Settings", output);
return true;
}
// http://ems-esp/api/system/customizations
+// we need to keep the original format so the import/upload works as we just replace file
bool System::command_customizations(const char * value, const int8_t id, JsonObject & output) {
- output["label"] = "customizations";
-
- JsonObject node = output.createNestedObject("Customizations");
-
- EMSESP::webCustomizationService.read([&](WebCustomization & settings) {
- // sensors
- JsonArray sensorsJson = node.createNestedArray("sensors");
- for (const auto & sensor : settings.sensorCustomizations) {
- JsonObject sensorJson = sensorsJson.createNestedObject();
- sensorJson["id"] = sensor.id; // key
- sensorJson["name"] = sensor.name; // n
- sensorJson["offset"] = sensor.offset; // o
- }
-
- JsonArray analogJson = node.createNestedArray("analogs");
- for (const AnalogCustomization & sensor : settings.analogCustomizations) {
- JsonObject sensorJson = analogJson.createNestedObject();
- sensorJson["gpio"] = sensor.gpio;
- sensorJson["name"] = sensor.name;
- if (EMSESP::system_.enum_format() == ENUM_FORMAT_INDEX) {
- sensorJson["type"] = sensor.type;
- } else {
- sensorJson["type"] = FL_(enum_sensortype)[sensor.type];
- }
- if (sensor.type == AnalogSensor::AnalogType::ADC) {
- sensorJson["offset"] = sensor.offset;
- sensorJson["factor"] = sensor.factor;
- sensorJson["uom"] = EMSdevice::uom_to_string(sensor.uom);
- } else if (sensor.type == AnalogSensor::AnalogType::COUNTER || sensor.type == AnalogSensor::AnalogType::TIMER
- || sensor.type == AnalogSensor::AnalogType::RATE) {
- sensorJson["factor"] = sensor.factor;
- sensorJson["uom"] = EMSdevice::uom_to_string(sensor.uom);
- } else if (sensor.type >= AnalogSensor::AnalogType::PWM_0) {
- sensorJson["frequency"] = sensor.factor;
- sensorJson["factor"] = sensor.factor;
- }
- }
-
- // masked entities
- JsonArray mask_entitiesJson = node.createNestedArray("masked_entities");
- for (const auto & entityCustomization : settings.entityCustomizations) {
- JsonObject entityJson = mask_entitiesJson.createNestedObject();
- entityJson["product_id"] = entityCustomization.product_id;
- entityJson["device_id"] = entityCustomization.device_id;
-
- JsonArray mask_entityJson = entityJson.createNestedArray("entities");
- for (std::string entity_id : entityCustomization.entity_ids) {
- mask_entityJson.add(entity_id);
- }
- }
- });
-
+ output["type"] = "customizations";
+ extractSettings(EMSESP_CUSTOMIZATION_FILE, "Customizations", output);
return true;
}
diff --git a/src/system.h b/src/system.h
index 80178e1e3..63010652b 100644
--- a/src/system.h
+++ b/src/system.h
@@ -84,6 +84,9 @@ class System {
void button_init(bool refresh);
void commands_init();
+ static void extractSettings(const char * filename, const char * section, JsonObject & output);
+ static bool saveSettings(const char * filename, const char * section, JsonObject & input);
+
static bool is_valid_gpio(uint8_t pin);
static bool load_board_profile(std::vector & data, const std::string & board_profile);
diff --git a/src/test/test.cpp b/src/test/test.cpp
index 66f70989f..3d320bb47 100644
--- a/src/test/test.cpp
+++ b/src/test/test.cpp
@@ -1614,6 +1614,7 @@ void Test::listDir(fs::FS & fs, const char * dirname, uint8_t levels) {
if (levels) {
listDir(fs, file.name(), levels - 1);
}
+ Serial.println();
} else {
Serial.print(" FILE: ");
Serial.print(file.name());
@@ -1638,6 +1639,7 @@ void Test::debug(uuid::console::Shell & shell, const std::string & cmd) {
#ifndef EMSESP_STANDALONE
if (command == "ls") {
listDir(LITTLEFS, "/", 3);
+ Serial.println();
}
#endif
}
diff --git a/src/version.h b/src/version.h
index b411d73c8..c77300d6d 100644
--- a/src/version.h
+++ b/src/version.h
@@ -1 +1 @@
-#define EMSESP_APP_VERSION "3.4.0b16"
+#define EMSESP_APP_VERSION "3.4.0b17"