Browse Source

0.8.80

* 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
pull/1433/head
lumapu 8 months ago
parent
commit
31232bfd80
  1. 8
      README.md
  2. 55
      manual/User_Manual.md
  3. 7
      src/CHANGES.md
  4. 2
      src/defines.h
  5. 5
      src/hm/hmInverter.h
  6. 2
      src/publisher/pubMqtt.h
  7. 37
      src/web/RestApi.h
  8. 15
      src/web/html/visualization.html
  9. 30
      src/web/lang.h
  10. 25
      src/web/lang.json

8
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
<- a project which can handle WiFi inverters like HMS-XXXXW-2T

55
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
👆 `<INVERTER_ID>` 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": <PASSOWRD>
}
```
`<PASSWORD>` is your DTU password in plain text.
As Response you get the following `JSON` if successful:
```json
{
"success": true,
"token": "<TOKEN>"
}
```
Where `<TOKEN>` 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": "<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 `<TOPIC>/devcontrol/<INVERTER_ID>/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 `<TOPIC>/<INVERTER_NAME_FROM_SETUP>/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)

7
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

2
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 {

5
src/hm/hmInverter.h

@ -163,9 +163,10 @@ class Inverter {
void tickSend(std::function<void(uint8_t cmd, bool isDevControl)> 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

2
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;

37
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 HMSYSTEM>
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<String>();
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<const char*>(), 6)) {
iv->powerLimit[0] = jsonIn["val"];
iv->powerLimit[0] = static_cast<uint16_t>(jsonIn["val"].as<float>() * 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<int>());
} else {
jsonOut[F("error")] = F(UNKNOWN_CMD) + jsonIn["cmd"].as<String>() + "'";
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;

15
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) {

30
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*/

25
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&ouml;glich"
},
{
"token": "ERR_PROTECTED",
"en": "not logged in, command not possible!",
"de": "nicht angemeldet, Kommando nicht m&ouml;glich!"
}
]
},

Loading…
Cancel
Save