diff --git a/README.md b/README.md index 7a51a98a..16b2a256 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ Table of approaches: | Board | MI | HM | HMS/HMT | comment | HowTo start | | ------ | -- | -- | ------- | ------- | ---------- | -| [ESP8266/ESP32, C++](Getting_Started.md) | ✔️ | ✔️ | ✔️ | 👈 the most effort is spent here | [create your own DTU](https://ahoydtu.de/getting_started/) | +| [ESP8266/ESP32, C++](manual/Getting_Started.md) | ✔️ | ✔️ | ✔️ | 👈 the most effort is spent here | [create your own DTU](https://ahoydtu.de/getting_started/) | | [Arduino Nano, C++](tools/nano/NRF24_SendRcv/) | ❌ | ✔️ | ❌ | | | [Raspberry Pi, Python](tools/rpi/) | ❌ | ✔️ | ❌ | | | [Others, C/C++](tools/nano/NRF24_SendRcv/) | ❌ | ✔️ | ❌ | | @@ -39,11 +39,11 @@ Table of approaches: ⚠️ **Warning: HMS-XXXXW-2T WiFi inverters are not supported. They have a 'W' in their name and a DTU serial number on its sticker** ## Getting Started -1. [Guide how to start with a ESP module](Getting_Started.md) +1. [Guide how to start with a ESP module](manual/Getting_Started.md) 2. [ESP Webinstaller (Edge / Chrome Browser only)](https://ahoydtu.de/web_install) -3. [Ahoy Configuration ](ahoy_config.md) +3. [Ahoy Configuration ](manual/ahoy_config.md) ## Our Website [https://ahoydtu.de](https://ahoydtu.de) @@ -64,4 +64,4 @@ If you encounter any problems, use the issue tracker on Github. Provide a detail - [OpenDTU](https://github.com/tbnobody/OpenDTU) <- Our sister project ✨ for Hoymiles HM- and HMS-/HMT-series (for ESP32 only!) - [hms-mqtt-publisher](https://github.com/DennisOSRM/hms-mqtt-publisher) - <- a project which can handle WiFi inverters like HMS-XXXXW-2T \ No newline at end of file + <- a project which can handle WiFi inverters like HMS-XXXXW-2T diff --git a/manual/User_Manual.md b/manual/User_Manual.md index 53da34f4..b5cf5a7e 100644 --- a/manual/User_Manual.md +++ b/manual/User_Manual.md @@ -166,6 +166,8 @@ inverter/ctrl/limit/0 600W ### Power Limit persistent This feature was removed. The persisten limit should not be modified cyclic by a script because of potential wearout of the flash inside the inverter. + + ## Control via REST API ### Generic Information @@ -174,6 +176,46 @@ The rest API works with *JSON* POST requests. All the following instructions mus 👆 `` is the number of the specific inverter in the setup page. +### Authentication (new for versions > `0.8.79`) + +The authentication is only needed if a password was set. +To authenticate from API you have to add the following `JSON` to your request: + +```json +{ + "auth": +} +``` +`` is your DTU password in plain text. + +As Response you get the following `JSON` if successful: + +```json +{ + "success": true, + "token": "" +} +``` +Where `` is a random token with a length of 16 characters. + +For all following commands you have only to include the token into your `JSON`: +```json +{ + "token": "" +} +``` + +ℹ️ Do not pass the plain text password with each command. Authenticate once and then use the token for all following commands. The token expires once the token wasn't sent for 20 minutes. + +If the authentication fails or the token is expired you will receive the following `JSON`: + +```json +{ + "success": false, + "error": "ERR_PROTECTED" +} +``` + ### Inverter Power (On / Off) ```json @@ -245,19 +287,6 @@ The `VALUE` represents a percent number in a range of `[2.0 .. 100.0]` The `VALUE` represents watts in a range of `[1.0 .. 6553.5]` - -### Developer Information REST API (obsolete) -In the same approach as for MQTT any other SubCmd and also MainCmd can be applied and the response payload can be observed in the serial logs. Eg. request the Alarm-Data from the Alarm-Index 5 from inverter 0 will look like this: -```json -{ - "inverter":0, - "tx_request": 21, - "cmd": 17, - "payload": 5, - "payload2": 0 -} -``` - ## Zero Export Control (needs rework) * You can use the mqtt topic `/devcontrol//11` with a number as payload (eg. 300 -> 300 Watt) to set the power limit to the published number in Watt. (In regular cases the inverter will use the new set point within one intervall period; to verify this see next bullet) * You can check the inverter set point for the power limit control on the topic `//ch0/PowerLimit` 👆 This value is ALWAYS in percent of the maximum power limit of the inverter. In regular cases this value will be updated within approx. 15 seconds. (depends on request intervall) diff --git a/src/CHANGES.md b/src/CHANGES.md index dff1bb30..714dc8cc 100644 --- a/src/CHANGES.md +++ b/src/CHANGES.md @@ -1,5 +1,12 @@ # Development Changes +## 0.8.80 - 2024-02-12 +* optimize API authentication, Error-Codes #1415 +* breaking change: authentication API command changed #1415 +* breaking change: limit has to be send als `float`, `0.0 .. 100.0` #1415 +* updated documentation #1415 +* fix don't send control command twice #1426 + ## 0.8.79 - 2024-02-11 * fix `opendtufusion` build (started only once USB-console was connected) * code quality improvments diff --git a/src/defines.h b/src/defines.h index 40084c47..8913db5e 100644 --- a/src/defines.h +++ b/src/defines.h @@ -13,7 +13,7 @@ //------------------------------------- #define VERSION_MAJOR 0 #define VERSION_MINOR 8 -#define VERSION_PATCH 79 +#define VERSION_PATCH 80 //------------------------------------- typedef struct { diff --git a/src/hm/hmInverter.h b/src/hm/hmInverter.h index b6dc93fa..f608908e 100644 --- a/src/hm/hmInverter.h +++ b/src/hm/hmInverter.h @@ -163,9 +163,10 @@ class Inverter { void tickSend(std::function cb) { if(mDevControlRequest) { - if(InverterStatus::OFF != status) + if(InverterStatus::OFF != status) { cb(devControlCmd, true); - else + devControlCmd = InitDataState; + } else DPRINTLN(DBG_WARN, F("Inverter is not avail")); mDevControlRequest = false; } else if (IV_MI != ivGen) { // HM / HMS / HMT diff --git a/src/publisher/pubMqtt.h b/src/publisher/pubMqtt.h index 1b9e35ef..369d9ead 100644 --- a/src/publisher/pubMqtt.h +++ b/src/publisher/pubMqtt.h @@ -316,7 +316,7 @@ class PubMqtt { if(NULL == strstr(topic, "limit")) root[F("val")] = atoi(pyld); else - root[F("val")] = (int)(atof(pyld) * 10.0f); + root[F("val")] = atof(pyld); if(pyld[len-1] == 'W') limitAbs = true; diff --git a/src/web/RestApi.h b/src/web/RestApi.h index fadd7277..37e988c0 100644 --- a/src/web/RestApi.h +++ b/src/web/RestApi.h @@ -30,10 +30,6 @@ #define F(sl) (sl) #endif -const uint8_t acList[] = {FLD_UAC, FLD_IAC, FLD_PAC, FLD_F, FLD_PF, FLD_T, FLD_YT, FLD_YD, FLD_PDC, FLD_EFF, FLD_Q, FLD_MP}; -const uint8_t acListHmt[] = {FLD_UAC_1N, FLD_IAC_1, FLD_PAC, FLD_F, FLD_PF, FLD_T, FLD_YT, FLD_YD, FLD_PDC, FLD_EFF, FLD_Q, FLD_MP}; -const uint8_t dcList[] = {FLD_UDC, FLD_IDC, FLD_PDC, FLD_YD, FLD_YT, FLD_IRR, FLD_MP}; - template class RestApi { public: @@ -831,14 +827,16 @@ class RestApi { } bool setCtrl(JsonObject jsonIn, JsonObject jsonOut, const char *clientIP) { - if(F("auth") == jsonIn[F("cmd")]) { - if(String(jsonIn["val"]) == String(mConfig->sys.adminPwd)) - jsonOut["token"] = mApp->unlock(clientIP, false); - else { - jsonOut[F("error")] = F(AUTH_ERROR); + if(jsonIn.containsKey(F("auth"))) { + if(String(jsonIn[F("auth")]) == String(mConfig->sys.adminPwd)) { + jsonOut[F("token")] = mApp->unlock(clientIP, false); + jsonIn[F("token")] = jsonOut[F("token")]; + } else { + jsonOut[F("error")] = F("ERR_AUTH"); return false; } - return true; + if(!jsonIn.containsKey(F("cmd"))) + return true; } if(isProtected(jsonIn, jsonOut, clientIP)) @@ -847,7 +845,7 @@ class RestApi { Inverter<> *iv = mSys->getInverterByPos(jsonIn[F("id")]); bool accepted = true; if(NULL == iv) { - jsonOut[F("error")] = F(INV_INDEX_INVALID) + jsonIn[F("id")].as(); + jsonOut[F("error")] = F("ERR_INDEX"); return false; } jsonOut[F("id")] = jsonIn[F("id")]; @@ -857,7 +855,7 @@ class RestApi { else if(F("restart") == jsonIn[F("cmd")]) accepted = iv->setDevControlRequest(Restart); else if(0 == strncmp("limit_", jsonIn[F("cmd")].as(), 6)) { - iv->powerLimit[0] = jsonIn["val"]; + iv->powerLimit[0] = static_cast(jsonIn["val"].as() * 10.0); if(F("limit_persistent_relative") == jsonIn[F("cmd")]) iv->powerLimit[1] = RelativPersistent; else if(F("limit_persistent_absolute") == jsonIn[F("cmd")]) @@ -874,12 +872,12 @@ class RestApi { DPRINTLN(DBG_INFO, F("dev cmd")); iv->setDevCommand(jsonIn[F("val")].as()); } else { - jsonOut[F("error")] = F(UNKNOWN_CMD) + jsonIn["cmd"].as() + "'"; + jsonOut[F("error")] = F("ERR_UNKNOWN_CMD"); return false; } if(!accepted) { - jsonOut[F("error")] = F(INV_DOES_NOT_ACCEPT_LIMIT_AT_MOMENT); + jsonOut[F("error")] = F("ERR_LIMIT_NOT_ACCEPT"); return false; } @@ -930,7 +928,7 @@ class RestApi { iv->config->disNightCom = jsonIn[F("disnightcom")]; mApp->saveSettings(false); // without reboot } else { - jsonOut[F("error")] = F(UNKNOWN_CMD); + jsonOut[F("error")] = F("ERR_UNKNOWN_CMD"); return false; } @@ -947,7 +945,7 @@ class RestApi { if(!mApp->isProtected(clientIP, token, false)) return false; - jsonOut[F("error")] = F(IS_PROTECTED); + jsonOut[F("error")] = F("ERR_PROTECTED"); return true; } } @@ -955,6 +953,13 @@ class RestApi { return false; } + private: + constexpr static uint8_t acList[] = {FLD_UAC, FLD_IAC, FLD_PAC, FLD_F, FLD_PF, FLD_T, FLD_YT, + FLD_YD, FLD_PDC, FLD_EFF, FLD_Q, FLD_MP}; + constexpr static uint8_t acListHmt[] = {FLD_UAC_1N, FLD_IAC_1, FLD_PAC, FLD_F, FLD_PF, FLD_T, + FLD_YT, FLD_YD, FLD_PDC, FLD_EFF, FLD_Q, FLD_MP}; + constexpr static uint8_t dcList[] = {FLD_UDC, FLD_IDC, FLD_PDC, FLD_YD, FLD_YT, FLD_IRR, FLD_MP}; + private: IApp *mApp = nullptr; HMSYSTEM *mSys = nullptr; diff --git a/src/web/html/visualization.html b/src/web/html/visualization.html index 1ce4e264..81962add 100644 --- a/src/web/html/visualization.html +++ b/src/web/html/visualization.html @@ -22,6 +22,15 @@ var total = Array(6).fill(0); var tPwrAck; + function getErrStr(code) { + if("ERR_AUTH") return "{#ERR_AUTH}" + if("ERR_INDEX") return "{#ERR_INDEX}" + if("ERR_UNKNOWN_CMD") return "{#ERR_UNKNOWN_CMD}" + if("ERR_LIMIT_NOT_ACCEPT") return "{#ERR_LIMIT_NOT_ACCEPT}" + if("ERR_UNKNOWN_CMD") return "{#ERR_AUTH}" + return "n/a" + } + function parseGeneric(obj) { if(true == exeOnce){ parseNav(obj); @@ -457,7 +466,7 @@ obj.id = id obj.token = "*" obj.cmd = cmd - obj.val = Math.round(val*10) + obj.val = val getAjax("/api/ctrl", ctrlCb, "POST", JSON.stringify(obj)) } @@ -477,7 +486,7 @@ tPwrAck = window.setInterval("getAjax('/api/inverter/pwrack/" + obj.id + "', updatePwrAck)", 1000); } else - e.innerHTML = "{#ERROR}: " + obj["error"]; + e.innerHTML = "{#ERROR}: " + getErrStr(obj.error); } function ctrlCb2(obj) { @@ -485,7 +494,7 @@ if(obj.success) e.innerHTML = "{#COMMAND_RECEIVED}"; else - e.innerHTML = "{#ERROR}: " + obj["error"]; + e.innerHTML = "{#ERROR}: " + getErrStr(obj.error); } function updatePwrAck(obj) { diff --git a/src/web/lang.h b/src/web/lang.h index 1e066928..fb5506ee 100644 --- a/src/web/lang.h +++ b/src/web/lang.h @@ -24,36 +24,6 @@ #define WAS_IN_CH_12_TO_14 "Your ESP was in wifi channel 12 to 14. It may cause reboots of your AhoyDTU" #endif -#ifdef LANG_DE - #define INV_INDEX_INVALID "Wechselrichterindex ungültig; " -#else /*LANG_EN*/ - #define INV_INDEX_INVALID "inverter index invalid: " -#endif - -#ifdef LANG_DE - #define AUTH_ERROR "Authentifizierungsfehler" -#else /*LANG_EN*/ - #define AUTH_ERROR "authentication error" -#endif - -#ifdef LANG_DE - #define UNKNOWN_CMD "unbekanntes Kommando: '" -#else /*LANG_EN*/ - #define UNKNOWN_CMD "unknown cmd: '" -#endif - -#ifdef LANG_DE - #define IS_PROTECTED "nicht angemeldet, Kommando nicht möglich!" -#else /*LANG_EN*/ - #define IS_PROTECTED "not logged in, command not possible!" -#endif - -#ifdef LANG_DE - #define INV_DOES_NOT_ACCEPT_LIMIT_AT_MOMENT "Leistungsbegrenzung / Ansteuerung aktuell nicht möglich" -#else /*LANG_EN*/ - #define INV_DOES_NOT_ACCEPT_LIMIT_AT_MOMENT "inverter does not accept dev control request at this moment" -#endif - #ifdef LANG_DE #define PATH_NOT_FOUND "Pfad nicht gefunden: " #else /*LANG_EN*/ diff --git a/src/web/lang.json b/src/web/lang.json index 1e79f8bf..335f0d2a 100644 --- a/src/web/lang.json +++ b/src/web/lang.json @@ -1432,6 +1432,31 @@ "token": "INV_ACK", "en": "inverter acknowledged active power control command", "de": "Wechselrichter hat die Leistungsbegrenzung akzeptiert" + }, + { + "token": "ERR_AUTH", + "en": "authentication error", + "de": "Authentifizierungsfehler" + }, + { + "token": "ERR_INDEX", + "en": "inverter index invalid", + "de": "Wechselrichterindex ungültig" + }, + { + "token": "ERR_UNKNOWN_CMD", + "en": "unknown cmd", + "de": "unbekanntes Kommando" + }, + { + "token": "ERR_LIMIT_NOT_ACCEPT", + "en": "inverter does not accept dev control request at this moment", + "de": "Leistungsbegrenzung / Ansteuerung aktuell nicht möglich" + }, + { + "token": "ERR_PROTECTED", + "en": "not logged in, command not possible!", + "de": "nicht angemeldet, Kommando nicht möglich!" } ] },