diff --git a/README.md b/README.md index ec52d9e9..6c2fdd85 100644 --- a/README.md +++ b/README.md @@ -154,7 +154,7 @@ This can be achieved by editing the 'platformio.ini' file and add/change one or * Adjust the COM port in the file "platformio.ini" for your USB-serial-converter. It occurs twice: * upload_port * monitor_port -* Select the arrow button in the status bar (PlatformIO: Upload) to compile and upload the firmware. During the compilation, all required libraries are downloaded automatically. +* Select the arrow button in the blue bottom status bar (PlatformIO: Upload) to compile and upload the firmware. During the compilation, all required libraries are downloaded automatically. * There are two videos showing these steps: * [Git Clone and compilation](https://youtu.be/9cA_esv3zeA) * [Full installation and compilation](https://youtu.be/xs6TqHn7QWM) @@ -263,6 +263,7 @@ A documentation of the Web API can be found here: [Web-API Documentation](docs/W * OpenDTU needs access to a working NTP server to get the current date & time. * If your problem persists, check the [Issues on Github](https://github.com/tbnobody/OpenDTU/issues). Please inspect not only the open issues, also the closed issues contain useful information. * Another source of information are the [Discussions](https://github.com/tbnobody/OpenDTU/discussions/) +* When flashing with VSCode Plattform.IO fails and also with ESPRESSIF tool a demo bin file cannot be flashed to the ESP32 with error message "A fatal error occurred: MD5 of file does not match data in flash!" than un-wire/unconnect ESP32 from the NRF24L01+ board. Try to flash again and rewire afterwards. ## Related Projects - [Ahoy](https://github.com/grindylow/ahoy) diff --git a/include/Configuration.h b/include/Configuration.h index 4628e8a3..2cb6135b 100644 --- a/include/Configuration.h +++ b/include/Configuration.h @@ -91,6 +91,7 @@ struct CONFIG_T { bool Mqtt_Hass_Expire; char Security_Password[WIFI_MAX_PASSWORD_STRLEN + 1]; + bool Security_AllowReadonly; }; class ConfigurationClass { diff --git a/include/WebApi.h b/include/WebApi.h index 32b15b01..298dec47 100644 --- a/include/WebApi.h +++ b/include/WebApi.h @@ -8,6 +8,7 @@ #include "WebApi_firmware.h" #include "WebApi_inverter.h" #include "WebApi_limit.h" +#include "WebApi_maintenance.h" #include "WebApi_mqtt.h" #include "WebApi_network.h" #include "WebApi_ntp.h" @@ -28,6 +29,7 @@ public: void loop(); static bool checkCredentials(AsyncWebServerRequest* request); + static bool checkCredentialsReadonly(AsyncWebServerRequest* request); private: AsyncWebServer _server; @@ -40,6 +42,7 @@ private: WebApiFirmwareClass _webApiFirmware; WebApiInverterClass _webApiInverter; WebApiLimitClass _webApiLimit; + WebApiMaintenanceClass _webApiMaintenance; WebApiMqttClass _webApiMqtt; WebApiNetworkClass _webApiNetwork; WebApiNtpClass _webApiNtp; diff --git a/include/WebApi_maintenance.h b/include/WebApi_maintenance.h new file mode 100644 index 00000000..dd791537 --- /dev/null +++ b/include/WebApi_maintenance.h @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#pragma once + +#include + +class WebApiMaintenanceClass { +public: + void init(AsyncWebServer* server); + void loop(); + +private: + void onRebootPost(AsyncWebServerRequest* request); + + AsyncWebServer* _server; +}; \ No newline at end of file diff --git a/include/WebApi_security.h b/include/WebApi_security.h index ea9dd75c..37c56fae 100644 --- a/include/WebApi_security.h +++ b/include/WebApi_security.h @@ -9,8 +9,8 @@ public: void loop(); private: - void onPasswordGet(AsyncWebServerRequest* request); - void onPasswordPost(AsyncWebServerRequest* request); + void onSecurityGet(AsyncWebServerRequest* request); + void onSecurityPost(AsyncWebServerRequest* request); void onAuthenticateGet(AsyncWebServerRequest* request); diff --git a/include/defaults.h b/include/defaults.h index dfefa7b2..cd0ac8c3 100644 --- a/include/defaults.h +++ b/include/defaults.h @@ -10,6 +10,7 @@ #define ACCESS_POINT_NAME "OpenDTU-" #define ACCESS_POINT_PASSWORD "openDTU42" #define AUTH_USERNAME "admin" +#define SECURITY_ALLOW_READONLY true #define ADMIN_TIMEOUT 180 #define WIFI_RECONNECT_TIMEOUT 15 diff --git a/src/Configuration.cpp b/src/Configuration.cpp index c66553be..3d890103 100644 --- a/src/Configuration.cpp +++ b/src/Configuration.cpp @@ -77,6 +77,7 @@ bool ConfigurationClass::write() JsonObject security = doc.createNestedObject("security"); security["password"] = config.Security_Password; + security["allow_readonly"] = config.Security_AllowReadonly; JsonArray inverters = doc.createNestedArray("inverters"); for (uint8_t i = 0; i < INV_MAX_COUNT; i++) { @@ -202,6 +203,7 @@ bool ConfigurationClass::read() JsonObject security = doc["security"]; strlcpy(config.Security_Password, security["password"] | ACCESS_POINT_PASSWORD, sizeof(config.Security_Password)); + config.Security_AllowReadonly = security["allow_readonly"] | SECURITY_ALLOW_READONLY; JsonArray inverters = doc["inverters"]; for (uint8_t i = 0; i < INV_MAX_COUNT; i++) { diff --git a/src/WebApi.cpp b/src/WebApi.cpp index 8000be9c..857067ad 100644 --- a/src/WebApi.cpp +++ b/src/WebApi.cpp @@ -25,6 +25,7 @@ void WebApiClass::init() _webApiFirmware.init(&_server); _webApiInverter.init(&_server); _webApiLimit.init(&_server); + _webApiMaintenance.init(&_server); _webApiMqtt.init(&_server); _webApiNetwork.init(&_server); _webApiNtp.init(&_server); @@ -49,6 +50,7 @@ void WebApiClass::loop() _webApiFirmware.loop(); _webApiInverter.loop(); _webApiLimit.loop(); + _webApiMaintenance.loop(); _webApiMqtt.loop(); _webApiNetwork.loop(); _webApiNtp.loop(); @@ -79,4 +81,14 @@ bool WebApiClass::checkCredentials(AsyncWebServerRequest* request) return false; } +bool WebApiClass::checkCredentialsReadonly(AsyncWebServerRequest* request) +{ + CONFIG_T& config = Configuration.get(); + if (config.Security_AllowReadonly) { + return true; + } else { + return checkCredentials(request); + } +} + WebApiClass WebApi; \ No newline at end of file diff --git a/src/WebApi_devinfo.cpp b/src/WebApi_devinfo.cpp index 93f2fa75..1a29081c 100644 --- a/src/WebApi_devinfo.cpp +++ b/src/WebApi_devinfo.cpp @@ -6,6 +6,7 @@ #include "ArduinoJson.h" #include "AsyncJson.h" #include "Hoymiles.h" +#include "WebApi.h" #include void WebApiDevInfoClass::init(AsyncWebServer* server) @@ -23,6 +24,10 @@ void WebApiDevInfoClass::loop() void WebApiDevInfoClass::onDevInfoStatus(AsyncWebServerRequest* request) { + if (!WebApi.checkCredentialsReadonly(request)) { + return; + } + AsyncJsonResponse* response = new AsyncJsonResponse(); JsonObject root = response->getRoot(); diff --git a/src/WebApi_eventlog.cpp b/src/WebApi_eventlog.cpp index 9ba3c525..6c03fb46 100644 --- a/src/WebApi_eventlog.cpp +++ b/src/WebApi_eventlog.cpp @@ -6,6 +6,7 @@ #include "ArduinoJson.h" #include "AsyncJson.h" #include "Hoymiles.h" +#include "WebApi.h" void WebApiEventlogClass::init(AsyncWebServer* server) { @@ -22,6 +23,10 @@ void WebApiEventlogClass::loop() void WebApiEventlogClass::onEventlogStatus(AsyncWebServerRequest* request) { + if (!WebApi.checkCredentialsReadonly(request)) { + return; + } + AsyncJsonResponse* response = new AsyncJsonResponse(false, 2048); JsonObject root = response->getRoot(); diff --git a/src/WebApi_limit.cpp b/src/WebApi_limit.cpp index 00880c31..3195d40e 100644 --- a/src/WebApi_limit.cpp +++ b/src/WebApi_limit.cpp @@ -24,6 +24,10 @@ void WebApiLimitClass::loop() void WebApiLimitClass::onLimitStatus(AsyncWebServerRequest* request) { + if (!WebApi.checkCredentialsReadonly(request)) { + return; + } + AsyncJsonResponse* response = new AsyncJsonResponse(); JsonObject root = response->getRoot(); diff --git a/src/WebApi_maintenance.cpp b/src/WebApi_maintenance.cpp new file mode 100644 index 00000000..9a176822 --- /dev/null +++ b/src/WebApi_maintenance.cpp @@ -0,0 +1,82 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2022 Thomas Basler and others + */ + +#include "WebApi_maintenance.h" +#include "AsyncJson.h" +#include "WebApi.h" + +void WebApiMaintenanceClass::init(AsyncWebServer* server) +{ + using std::placeholders::_1; + + _server = server; + + _server->on("/api/maintenance/reboot", HTTP_POST, std::bind(&WebApiMaintenanceClass::onRebootPost, this, _1)); +} + +void WebApiMaintenanceClass::loop() +{ +} + +void WebApiMaintenanceClass::onRebootPost(AsyncWebServerRequest* request) +{ + if (!WebApi.checkCredentials(request)) { + return; + } + + AsyncJsonResponse* response = new AsyncJsonResponse(false, MQTT_JSON_DOC_SIZE); + JsonObject retMsg = response->getRoot(); + retMsg[F("type")] = F("warning"); + + if (!request->hasParam("data", true)) { + retMsg[F("message")] = F("No values found!"); + response->setLength(); + request->send(response); + return; + } + + String json = request->getParam("data", true)->value(); + + if (json.length() > MQTT_JSON_DOC_SIZE) { + retMsg[F("message")] = F("Data too large!"); + response->setLength(); + request->send(response); + return; + } + + DynamicJsonDocument root(MQTT_JSON_DOC_SIZE); + DeserializationError error = deserializeJson(root, json); + + if (error) { + retMsg[F("message")] = F("Failed to parse data!"); + response->setLength(); + request->send(response); + return; + } + + if (!(root.containsKey("reboot"))) { + retMsg[F("message")] = F("Values are missing!"); + response->setLength(); + request->send(response); + return; + } + + if (root[F("reboot")].as()) { + retMsg[F("type")] = F("success"); + retMsg[F("message")] = F("Reboot triggered!"); + + response->setLength(); + request->send(response); + yield(); + delay(1000); + yield(); + ESP.restart(); + } else { + retMsg[F("message")] = F("Reboot cancled!"); + + response->setLength(); + request->send(response); + } +} \ No newline at end of file diff --git a/src/WebApi_mqtt.cpp b/src/WebApi_mqtt.cpp index 3083851e..5431024a 100644 --- a/src/WebApi_mqtt.cpp +++ b/src/WebApi_mqtt.cpp @@ -28,6 +28,10 @@ void WebApiMqttClass::loop() void WebApiMqttClass::onMqttStatus(AsyncWebServerRequest* request) { + if (!WebApi.checkCredentialsReadonly(request)) { + return; + } + AsyncJsonResponse* response = new AsyncJsonResponse(false, MQTT_JSON_DOC_SIZE); JsonObject root = response->getRoot(); const CONFIG_T& config = Configuration.get(); diff --git a/src/WebApi_network.cpp b/src/WebApi_network.cpp index 4c3cf2b4..c5a34846 100644 --- a/src/WebApi_network.cpp +++ b/src/WebApi_network.cpp @@ -27,6 +27,10 @@ void WebApiNetworkClass::loop() void WebApiNetworkClass::onNetworkStatus(AsyncWebServerRequest* request) { + if (!WebApi.checkCredentialsReadonly(request)) { + return; + } + AsyncJsonResponse* response = new AsyncJsonResponse(); JsonObject root = response->getRoot(); diff --git a/src/WebApi_ntp.cpp b/src/WebApi_ntp.cpp index b019d239..a2d1e936 100644 --- a/src/WebApi_ntp.cpp +++ b/src/WebApi_ntp.cpp @@ -29,6 +29,10 @@ void WebApiNtpClass::loop() void WebApiNtpClass::onNtpStatus(AsyncWebServerRequest* request) { + if (!WebApi.checkCredentialsReadonly(request)) { + return; + } + AsyncJsonResponse* response = new AsyncJsonResponse(); JsonObject root = response->getRoot(); const CONFIG_T& config = Configuration.get(); diff --git a/src/WebApi_power.cpp b/src/WebApi_power.cpp index 413bc0fd..22591c05 100644 --- a/src/WebApi_power.cpp +++ b/src/WebApi_power.cpp @@ -24,6 +24,10 @@ void WebApiPowerClass::loop() void WebApiPowerClass::onPowerStatus(AsyncWebServerRequest* request) { + if (!WebApi.checkCredentialsReadonly(request)) { + return; + } + AsyncJsonResponse* response = new AsyncJsonResponse(); JsonObject root = response->getRoot(); diff --git a/src/WebApi_security.cpp b/src/WebApi_security.cpp index 96802fbc..c345d385 100644 --- a/src/WebApi_security.cpp +++ b/src/WebApi_security.cpp @@ -15,8 +15,8 @@ void WebApiSecurityClass::init(AsyncWebServer* server) _server = server; - _server->on("/api/security/password", HTTP_GET, std::bind(&WebApiSecurityClass::onPasswordGet, this, _1)); - _server->on("/api/security/password", HTTP_POST, std::bind(&WebApiSecurityClass::onPasswordPost, this, _1)); + _server->on("/api/security/config", HTTP_GET, std::bind(&WebApiSecurityClass::onSecurityGet, this, _1)); + _server->on("/api/security/config", HTTP_POST, std::bind(&WebApiSecurityClass::onSecurityPost, this, _1)); _server->on("/api/security/authenticate", HTTP_GET, std::bind(&WebApiSecurityClass::onAuthenticateGet, this, _1)); } @@ -24,7 +24,7 @@ void WebApiSecurityClass::loop() { } -void WebApiSecurityClass::onPasswordGet(AsyncWebServerRequest* request) +void WebApiSecurityClass::onSecurityGet(AsyncWebServerRequest* request) { if (!WebApi.checkCredentials(request)) { return; @@ -35,12 +35,13 @@ void WebApiSecurityClass::onPasswordGet(AsyncWebServerRequest* request) const CONFIG_T& config = Configuration.get(); root[F("password")] = config.Security_Password; + root[F("allow_readonly")] = config.Security_AllowReadonly; response->setLength(); request->send(response); } -void WebApiSecurityClass::onPasswordPost(AsyncWebServerRequest* request) +void WebApiSecurityClass::onSecurityPost(AsyncWebServerRequest* request) { if (!WebApi.checkCredentials(request)) { return; @@ -76,7 +77,8 @@ void WebApiSecurityClass::onPasswordPost(AsyncWebServerRequest* request) return; } - if (!root.containsKey("password")) { + if (!root.containsKey("password") + && root.containsKey("allow_readonly")) { retMsg[F("message")] = F("Values are missing!"); response->setLength(); request->send(response); @@ -92,6 +94,7 @@ void WebApiSecurityClass::onPasswordPost(AsyncWebServerRequest* request) CONFIG_T& config = Configuration.get(); strlcpy(config.Security_Password, root[F("password")].as().c_str(), sizeof(config.Security_Password)); + config.Security_AllowReadonly = root[F("allow_readonly")].as(); Configuration.write(); retMsg[F("type")] = F("success"); diff --git a/src/WebApi_sysstatus.cpp b/src/WebApi_sysstatus.cpp index 2d6b2987..ce7c21a2 100644 --- a/src/WebApi_sysstatus.cpp +++ b/src/WebApi_sysstatus.cpp @@ -7,6 +7,7 @@ #include "AsyncJson.h" #include "Configuration.h" #include "NetworkSettings.h" +#include "WebApi.h" #include #include #include @@ -30,6 +31,10 @@ void WebApiSysstatusClass::loop() void WebApiSysstatusClass::onSystemStatus(AsyncWebServerRequest* request) { + if (!WebApi.checkCredentialsReadonly(request)) { + return; + } + AsyncJsonResponse* response = new AsyncJsonResponse(); JsonObject root = response->getRoot(); diff --git a/src/WebApi_vedirect.cpp b/src/WebApi_vedirect.cpp index 4af889b1..cb5b2ed5 100644 --- a/src/WebApi_vedirect.cpp +++ b/src/WebApi_vedirect.cpp @@ -27,6 +27,10 @@ void WebApiVedirectClass::loop() void WebApiVedirectClass::onVedirectStatus(AsyncWebServerRequest* request) { + if (!WebApi.checkCredentialsReadonly(request)) { + return; + } + AsyncJsonResponse* response = new AsyncJsonResponse(); JsonObject root = response->getRoot(); const CONFIG_T& config = Configuration.get(); diff --git a/src/WebApi_ws_live.cpp b/src/WebApi_ws_live.cpp index 35f94d51..0027c8f2 100644 --- a/src/WebApi_ws_live.cpp +++ b/src/WebApi_ws_live.cpp @@ -5,6 +5,7 @@ #include "WebApi_ws_live.h" #include "AsyncJson.h" #include "Configuration.h" +#include "WebApi.h" #include "defaults.h" WebApiWsLiveClass::WebApiWsLiveClass() @@ -65,6 +66,13 @@ void WebApiWsLiveClass::loop() String buffer; if (buffer) { serializeJson(root, buffer); + + if (Configuration.get().Security_AllowReadonly) { + _ws.setAuthentication("", ""); + } else { + _ws.setAuthentication(AUTH_USERNAME, Configuration.get().Security_Password); + } + _ws.textAll(buffer); } @@ -200,6 +208,10 @@ void WebApiWsLiveClass::onWebsocketEvent(AsyncWebSocket* server, AsyncWebSocketC void WebApiWsLiveClass::onLivedataStatus(AsyncWebServerRequest* request) { + if (!WebApi.checkCredentialsReadonly(request)) { + return; + } + AsyncJsonResponse* response = new AsyncJsonResponse(false, 40960U); JsonVariant root = response->getRoot(); diff --git a/src/WebApi_ws_vedirect_live.cpp b/src/WebApi_ws_vedirect_live.cpp index 3ecf31ee..b875e81a 100644 --- a/src/WebApi_ws_vedirect_live.cpp +++ b/src/WebApi_ws_vedirect_live.cpp @@ -5,6 +5,7 @@ #include "WebApi_ws_vedirect_live.h" #include "AsyncJson.h" #include "Configuration.h" +#include "WebApi.h" WebApiWsVedirectLiveClass::WebApiWsVedirectLiveClass() : _ws("/vedirectlivedata") @@ -60,6 +61,13 @@ void WebApiWsVedirectLiveClass::loop() String buffer; if (buffer) { serializeJson(root, buffer); + + if (Configuration.get().Security_AllowReadonly) { + _ws.setAuthentication("", ""); + } else { + _ws.setAuthentication(AUTH_USERNAME, Configuration.get().Security_Password); + } + _ws.textAll(buffer); } @@ -125,6 +133,9 @@ void WebApiWsVedirectLiveClass::onWebsocketEvent(AsyncWebSocket* server, AsyncWe void WebApiWsVedirectLiveClass::onLivedataStatus(AsyncWebServerRequest* request) { + if (!WebApi.checkCredentialsReadonly(request)) { + return; + } AsyncJsonResponse* response = new AsyncJsonResponse(false, 1024U); JsonVariant root = response->getRoot().as(); generateJsonResponse(root); diff --git a/webapp/README.md b/webapp/README.md index d7c36533..1729f905 100644 --- a/webapp/README.md +++ b/webapp/README.md @@ -1,4 +1,6 @@ -# opendtu +# OpenDTU web frontend + +You can run the webapp locally with `yarn dev`. If you enter the IP of your ESP in the `vite.config.ts` beforehand, all api requests will even be proxied to the real ESP. Then you can develop the webapp as if it were running directly on the ESP. The `yarn dev` also supports hot reload, i.e. as soon as you save a vue file, it is automatically reloaded in the browser. ## Project Setup diff --git a/webapp/package.json b/webapp/package.json index 4b676c88..dd160a24 100644 --- a/webapp/package.json +++ b/webapp/package.json @@ -12,7 +12,7 @@ }, "dependencies": { "@popperjs/core": "^2.11.6", - "bootstrap": "^5.2.2", + "bootstrap": "^5.2.3", "bootstrap-icons-vue": "^1.8.1", "mitt": "^3.0.0", "spark-md5": "^3.0.2", diff --git a/webapp/src/components/NavBar.vue b/webapp/src/components/NavBar.vue index 793bb420..ad546b33 100644 --- a/webapp/src/components/NavBar.vue +++ b/webapp/src/components/NavBar.vue @@ -49,6 +49,9 @@
  • Firmware Upgrade
  • +
  • + Device Reboot +