Skip to content

Commit

Permalink
0.8.80
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
lumapu committed Feb 12, 2024
1 parent 315541e commit 31232bf
Show file tree
Hide file tree
Showing 10 changed files with 116 additions and 70 deletions.
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,19 +31,19 @@ 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/) || ✔️ || |

⚠️ **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)
Expand All @@ -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 changes: 42 additions & 13 deletions manual/User_Manual.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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)
Expand Down
7 changes: 7 additions & 0 deletions src/CHANGES.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
2 changes: 1 addition & 1 deletion src/defines.h
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
//-------------------------------------
#define VERSION_MAJOR 0
#define VERSION_MINOR 8
#define VERSION_PATCH 79
#define VERSION_PATCH 80

//-------------------------------------
typedef struct {
Expand Down
5 changes: 3 additions & 2 deletions src/hm/hmInverter.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion src/publisher/pubMqtt.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
37 changes: 21 additions & 16 deletions src/web/RestApi.h
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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))
Expand All @@ -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")];
Expand All @@ -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")])
Expand All @@ -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;
}

Expand Down Expand Up @@ -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;
}

Expand All @@ -947,14 +945,21 @@ class RestApi {
if(!mApp->isProtected(clientIP, token, false))
return false;

jsonOut[F("error")] = F(IS_PROTECTED);
jsonOut[F("error")] = F("ERR_PROTECTED");
return true;
}
}

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;
Expand Down
15 changes: 12 additions & 3 deletions src/web/html/visualization.html
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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))
}

Expand All @@ -477,15 +486,15 @@
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) {
var e = document.getElementsByName("pwrres")[0];
if(obj.success)
e.innerHTML = "{#COMMAND_RECEIVED}";
else
e.innerHTML = "{#ERROR}: " + obj["error"];
e.innerHTML = "{#ERROR}: " + getErrStr(obj.error);
}

function updatePwrAck(obj) {
Expand Down
30 changes: 0 additions & 30 deletions src/web/lang.h
Original file line number Diff line number Diff line change
Expand Up @@ -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*/
Expand Down
25 changes: 25 additions & 0 deletions src/web/lang.json
Original file line number Diff line number Diff line change
Expand Up @@ -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!"
}
]
},
Expand Down

0 comments on commit 31232bf

Please sign in to comment.