diff --git a/tasmota/include/Powerwall.h b/tasmota/include/Powerwall.h index e81c0ab5e..518d2dde4 100755 --- a/tasmota/include/Powerwall.h +++ b/tasmota/include/Powerwall.h @@ -1,17 +1,26 @@ -// inspred by https://github.com/MoritzLerch/tesla-pv-display +// inspired by https://github.com/MoritzLerch/tesla-pv-display #ifndef Powerwall_h #define Powerwall_h -// include libraries -#include "WiFiClientSecureLightBearSSL.h" + +#define PW_RETRIES 2 + +#define PWL_LOGLVL LOG_LEVEL_DEBUG + +// include libraries from email client +// standard ssl does not work at all +ESP_SSLClient ssl_client; +WiFiClientImpl basic_client; class Powerwall { private: - const char* powerwall_ip; + String powerwall_ip; String tesla_email; String tesla_password; String authCookie; + String cts1; + String cts2; public: Powerwall(); @@ -19,57 +28,132 @@ class Powerwall { String GetRequest(String url, String authCookie); String GetRequest(String url); String AuthCookie(); - void resetAuthCookie(); + String Pwl_test(String); }; +#ifndef POWERWALL_IP_CONFIG + #define POWERWALL_IP_CONFIG "192.168.188.60" +#endif + +#ifndef TESLA_EMAIL + #define TESLA_EMAIL "email" +#endif + +#ifndef TESLA_PASSWORD + #define TESLA_PASSWORD "password" +#endif + +#ifndef TESLA_POWERWALL_CTS1 + #define TESLA_POWERWALL_CTS1 "cts1" +#endif + +#ifndef TESLA_POWERWALL_CTS2 + #define TESLA_POWERWALL_CTS2 "cts2" +#endif + Powerwall::Powerwall() { powerwall_ip = POWERWALL_IP_CONFIG; tesla_email = TESLA_EMAIL; tesla_password = TESLA_PASSWORD; authCookie = ""; + cts1 = TESLA_POWERWALL_CTS1; + cts2 = TESLA_POWERWALL_CTS2; } String Powerwall::AuthCookie() { return authCookie; } -void Powerwall::resetAuthCookie() { - authCookie = ""; + +String Powerwall::Pwl_test(String ip) { + AddLog(PWL_LOGLVL, PSTR("PWL: try to open %s"), ip.c_str()); + + ssl_client.setInsecure(); + /** Call setDebugLevel(level) to set the debug + * esp_ssl_debug_none = 0 + * esp_ssl_debug_error = 1 + * esp_ssl_debug_warn = 2 + * esp_ssl_debug_info = 3 + * esp_ssl_debug_dump = 4 + */ + ssl_client.setDebugLevel(0); + + // Set the receive and transmit buffers size in bytes for memory allocation (512 to 16384). + // For server that does not support SSL fragment size negotiation, leave this setting the default value + // by not set any buffer size or set the rx buffer size to maximum SSL record size (16384) and 512 for tx buffer size. + //ssl_client.setBufferSizes(1024 /* rx */, 512 /* tx */); + + // Assign the basic client + // Due to the basic_client pointer is assigned, to avoid dangling pointer, basic_client should be existed + // as long as it was used by ssl_client for transportation. + ssl_client.setClient(&basic_client); + + int retry = 0; + while (retry < PW_RETRIES) { + int32_t res = ssl_client.connect(ip.c_str(), 443); + if (res) { + break; + } + delay(100); + retry++; + } + + if (retry >= PW_RETRIES) { + AddLog(PWL_LOGLVL, PSTR("PWL: failed")); + } else { + AddLog(PWL_LOGLVL, PSTR("PWL: connected")); + } + + ssl_client.stop(); + + return "\n"; } + +void pHexdump(uint8_t *sbuff, uint32_t slen) { + char cbuff[slen*3+10]; + char *cp = cbuff; + *cp++ = '>'; + *cp++ = ' '; + for (uint32_t cnt = 0; cnt < slen; cnt ++) { + sprintf_P(cp, PSTR("%02x "), sbuff[cnt]); + cp += 3; + } + AddLog(PWL_LOGLVL, PSTR("PWL: response: %s"), cbuff); + +} + + /** * This function returns a string with the authToken based on the basic login endpoint of * the powerwall in combination with the credentials from the secrets.h * @returns authToken to be used in an authCookie */ String Powerwall::getAuthCookie() { - AddLog(LOG_LEVEL_DEBUG, PSTR("PWL: requesting new auth Cookie from %s"), powerwall_ip); + AddLog(PWL_LOGLVL, PSTR("PWL: requesting new auth Cookie from %s"), powerwall_ip.c_str()); String apiLoginURL = "/api/login/Basic"; -#ifdef ESP32 - WiFiClientSecure *httpsClient = new WiFiClientSecure; -#else - // BearSSL::WiFiClientSecure_light *httpsClient = new BearSSL::WiFiClientSecure_light(1024,1024); - WiFiClientSecure *httpsClient = new WiFiClientSecure; -#endif - httpsClient->setInsecure(); - httpsClient->setTimeout(10000); + ssl_client.setInsecure(); + //ssl_client.setBufferSizes(4096 /* rx */, 512 /* tx */); + ssl_client.setTimeout(3000); + ssl_client.setClient(&basic_client); + ssl_client.setDebugLevel(3); int retry = 0; - -#define PW_RETRIES 5 - while ((!httpsClient->connect(powerwall_ip, 443)) && (retry < PW_RETRIES)) { + while (retry < PW_RETRIES) { + int32_t res = ssl_client.connect(powerwall_ip.c_str(), 443); + if (res) { + break; + } delay(100); - Serial.print("."); retry++; } if (retry >= PW_RETRIES) { - delete httpsClient; return ("CONN-FAIL"); } - AddLog(LOG_LEVEL_DEBUG, PSTR("PWL: connected")); + AddLog(PWL_LOGLVL, PSTR("PWL: connected")); String dataString = "{\"username\":\"customer\",\"email\":\"" + tesla_email + "\",\"password\":\"" + tesla_password + "\",\"force_sm_off\":false}"; @@ -80,31 +164,113 @@ String Powerwall::getAuthCookie() { "Content-Length: " + dataString.length() + "\r\n" + "\r\n" + dataString + "\r\n\r\n"; - httpsClient->println(payload); + AddLog(PWL_LOGLVL, PSTR("PWL: payload: %s"),payload.c_str()); - while (httpsClient->connected()) { - String response = httpsClient->readStringUntil('\n'); - if (response == "\r") { - break; + ssl_client.println(payload); + + uint8_t flag = 0; + + uint8_t string[1200]; + uint32_t dlen; + uint32_t timeout = 30; + while (ssl_client.connected()) { + if (ssl_client.available()) { + dlen = ssl_client.available(); + AddLog(PWL_LOGLVL, PSTR("PWL: available: %d"), dlen); + String response = ""; +#if 1 + if (!flag) { + char c = ssl_client.peek(); + AddLog(PWL_LOGLVL, PSTR("PWL: peek: %c"), c); + if (c != 'H') { + AddLog(PWL_LOGLVL, PSTR("PWL: wrong response: %c"), c); + ssl_client.stop(); + return ""; + } else { + //basic_client.read(string, 17); + //ssl_client.read(string, 17); + const char *cp = ssl_client.peekBuffer(); + //ssl_client.peekBytes(string, 17); + //ssl_client.peekConsume(17); + //string[17] = 0; + //pHexdump(string, 17); + AddLog(PWL_LOGLVL, PSTR("PWL: 1. response: %s"), cp); + cp = strchr(cp, '{'); + if (cp) { + char *cp1 = strchr(cp, '}'); + if (cp1) { + *(cp1 + 1) = 0; + AddLog(PWL_LOGLVL, PSTR("PWL: json: %s"), cp); + char str_value[256]; + str_value[0] = 0; + float fv; + JsonParser parser((char*)cp); + JsonParserObject obj = parser.getRootObject(); + uint32_t res = JsonParsePath(&obj, "token", '#', &fv, str_value, sizeof(str_value)); + + AddLog(PWL_LOGLVL, PSTR("PWL: token: %s"), str_value); + + ssl_client.stop(); + return str_value; + } + } + } + flag = 1; + } + response = ssl_client.readStringUntil('\n'); + AddLog(PWL_LOGLVL, PSTR("PWL: response: %s"), response.c_str()); + #else + ssl_client.read(string, dlen); + pHexdump(string, dlen); + #endif + char *cp = (char*)response.c_str(); + if (!strncmp_P(cp, PSTR("HTTP"), 4)) { + char *sp = strchr(cp, ' '); + if (sp) { + sp++; + uint16_t result = strtol(sp, 0, 10); + if (result != 200) { + ssl_client.stop(); + return ""; + } else { + // break; + } + } + } + if (response == "\r") { + break; + } + } + timeout--; + delay(100); + AddLog(PWL_LOGLVL, PSTR("PWL: timeout: %d"), timeout); + if (!timeout) { + ssl_client.stop(); + return ""; } } - String jsonInput = httpsClient->readStringUntil('\n'); + String jsonInput; + dlen = ssl_client.available(); + if (ssl_client.connected() && dlen) { + ssl_client.read(string, dlen); + string[dlen] = 0; + jsonInput = (char*)string; + AddLog(PWL_LOGLVL, PSTR("PWL: jsonInput %s"),jsonInput.c_str()); + } - char str_value[128]; + char str_value[256]; str_value[0] = 0; float fv; JsonParser parser((char*)jsonInput.c_str()); JsonParserObject obj = parser.getRootObject(); uint32_t res = JsonParsePath(&obj, "token", '#', &fv, str_value, sizeof(str_value)); - AddLog(LOG_LEVEL_DEBUG, PSTR("PWL: token: %s"), str_value); + AddLog(PWL_LOGLVL, PSTR("PWL: token: %s"), str_value); - authCookie = str_value; - - delete httpsClient; + ssl_client.stop(); - return authCookie; + return str_value; } /** @@ -117,64 +283,138 @@ String Powerwall::getAuthCookie() { * @param authCookie optional, but recommended * @returns content of request */ -String Powerwall::GetRequest(String url, String authCookie) { -#ifdef ESP32 - WiFiClientSecure *httpsClient = new WiFiClientSecure; -#else - //BearSSL::WiFiClientSecure_light *httpsClient = new BearSSL::WiFiClientSecure_light(1024,1024); - WiFiClientSecure *httpsClient = new WiFiClientSecure; -#endif - httpsClient->setInsecure(); - httpsClient->setTimeout(10000); +String Powerwall::GetRequest(String url, String in_authCookie) { - if (authCookie == "") { - getAuthCookie(); + + AddLog(PWL_LOGLVL, PSTR("PWL: cookie %s"), in_authCookie.c_str()); + + ssl_client.setInsecure(); + ssl_client.setTimeout(5000); + ssl_client.setClient(&basic_client); + //ssl_client.setBufferSizes(4096 /* rx */, 512 /* tx */); + ssl_client.setBufferSizes(16384, 512); + + + if (in_authCookie == "") { + authCookie = getAuthCookie(); } - AddLog(LOG_LEVEL_DEBUG, PSTR("PWL: doing GET-request to %s%s"), powerwall_ip, url.c_str()); + AddLog(PWL_LOGLVL, PSTR("PWL: doing GET-request to %s - %s"), powerwall_ip.c_str(), url.c_str()); int retry = 0; - while ((!httpsClient->connect(powerwall_ip, 443)) && (retry < 15)) { + while ((!ssl_client.connect(powerwall_ip.c_str(), 443)) && (retry < PW_RETRIES)) { delay(100); - Serial.print("."); + //Serial.print("."); retry++; } - if (retry >= 15) { - delete httpsClient; + if (retry >= PW_RETRIES) { return ("CONN-FAIL"); } + AddLog(PWL_LOGLVL, PSTR("PWL: connected")); + // HTTP/1.0 is used because of Chunked transfer encoding - httpsClient->print(String("GET ") + url + " HTTP/1.0" + "\r\n" + + String request = "GET " + url + " HTTP/1.0" + "\r\n" + "Host: " + powerwall_ip + "\r\n" + "Cookie: " + "AuthCookie" + "=" + authCookie + "\r\n" + - "Connection: close\r\n\r\n"); + "Connection: close\r\n\r\n"; + + ssl_client.println(request); + + AddLog(PWL_LOGLVL, PSTR("PWL: request: %s"), request.c_str()); - while (httpsClient->connected()) { - String response = httpsClient->readStringUntil('\n'); - char *cp = (char*)response.c_str(); - if (!strncmp_P(cp, PSTR("HTTP"), 4)) { - char *sp = strchr(cp, ' '); - if (sp) { - sp++; - uint16_t result = strtol(sp, 0, 10); - AddLog(LOG_LEVEL_DEBUG, PSTR("PWL: result %d"), result); - // in case of error 401, get new cookie - if (result == 401) { - authCookie = ""; - resetAuthCookie(); + uint32_t timeout = 500; + int32_t chunked = 0; + while (ssl_client.connected()) { + if (ssl_client.available()) { + String response = ssl_client.readStringUntil('\n'); + AddLog(PWL_LOGLVL, PSTR("PWL: result %s"), response.c_str()); + if (chunked == -2) { + // process chunc size + chunked = strtol(response.c_str(), 0, 16); + AddLog(PWL_LOGLVL, PSTR("PWL: chunc size %d"), chunked); + break; + } + char *cp = (char*)response.c_str(); + if (!strncmp_P(cp, PSTR("HTTP"), 4)) { + char *sp = strchr(cp, ' '); + if (sp) { + sp++; + uint16_t result = strtol(sp, 0, 10); + AddLog(PWL_LOGLVL, PSTR("PWL: result %d"), result); + // in case of error 401, get new cookie + if (result == 401) { + authCookie = ""; + } else if (result != 200) { + ssl_client.stop(); + return "\n"; + } + } + } + if (!strncmp_P(cp, PSTR("Transfer-Encoding: chunked"), 26)) { + chunked = -1; + AddLog(PWL_LOGLVL, PSTR("PWL: chunked %d"), chunked); + } + + if (response == "\r") { + if (chunked) { + // skip + chunked = -2; + } else { + break; } } } - if (response == "\r") { + timeout--; + delay(10); + if (!timeout) { break; } } - String result = httpsClient->readStringUntil('\n'); - delete httpsClient; + String result = "\r"; + + timeout = 100; + char *string = (char*)calloc(4096,1); + if (string) { + char *cp = string; + while (ssl_client.connected()) { + uint16_t dlen; + dlen = ssl_client.available(); + if (dlen) { + ssl_client.read((uint8_t*)cp, dlen); + cp += dlen; + *cp = 0; + } + delay(10); + timeout--; + if (!timeout) { + break; + } + } + AddLog(PWL_LOGLVL, PSTR("PWL: result %s"), string); + result = string; + free(string); + } + ssl_client.stop(); + + // custom replace + result.replace(cts1, "PW_CTS1"); + + result.replace(cts2, "PW_CTS2"); + + // shrink data size because it exceeds json parser maxsize + result.replace("communication_time", "ct"); + result.replace("instant", "i"); + result.replace("apparent", "a"); + result.replace("reactive", "r"); + + result.replace("nominal_full_pack_energy", "f_p_e"); + result.replace("nominal_energy_remaining", "n_e_r"); + result.replace("backup_reserve_percent", "b_r_p"); + return result; } @@ -182,6 +422,30 @@ String Powerwall::GetRequest(String url, String authCookie) { * this is getting called if there was no provided authCookie in powerwallGetRequest(String url, String authCookie) */ String Powerwall::GetRequest(String url) { + if (url[0] == '@') { + if (url[1] == 'D') { + // define vars + //AddLog(PWL_LOGLVL, PSTR("PWL: %s - %s - %s"), powerwall_ip.c_str(), tesla_email.c_str(), tesla_password.c_str()); + url = url.substring(2); + uint16_t pos = strcspn(url.c_str(), ","); + powerwall_ip = url.substring(0, pos); + url = url.substring(pos + 1); + pos = strcspn(url.c_str(), ","); + tesla_email = url.substring(0, pos); + tesla_password = url.substring(pos + 1); + //AddLog(PWL_LOGLVL, PSTR("PWL: %s - %s - %s"), powerwall_ip.c_str(), tesla_email.c_str(), tesla_password.c_str()); + return ""; + } if (url[1] == 'C') { + url = url.substring(2); + uint16_t pos = strcspn(url.c_str(), ","); + cts1 = url.substring(0, pos); + cts2 = url.substring(pos + 1); + return ""; + } else { + url = url.substring(1); + return Pwl_test(url); + } + } return (GetRequest(url, getAuthCookie())); } diff --git a/tasmota/tasmota_xdrv_driver/xdrv_10_scripter.ino b/tasmota/tasmota_xdrv_driver/xdrv_10_scripter.ino index 254f03e10..636c163df 100755 --- a/tasmota/tasmota_xdrv_driver/xdrv_10_scripter.ino +++ b/tasmota/tasmota_xdrv_driver/xdrv_10_scripter.ino @@ -352,6 +352,7 @@ void alt_eeprom_readBytes(uint32_t adr, uint32_t len, uint8_t *buf) { #include #ifdef TESLA_POWERWALL +#include "SSLClient/ESP_SSLClient.h" #include "include/powerwall.h" #endif @@ -773,7 +774,7 @@ typedef struct { SCRIPT_MEM glob_script_mem; -uint32_t Plugin_Query(uint16_t, uint8_t); +uint32_t Plugin_Query(uint16_t, uint8_t, char *); void script_setaflg(uint8_t flg) { glob_script_mem.tasm_cmd_activ = flg; @@ -845,8 +846,8 @@ int32_t play_wave(char *path); #if defined(USE_BINPLUGINS) && !defined(USE_SML_M) SML_TABLE *get_sml_table(void) { - if (Plugin_Query(53, 0)) { - return (SML_TABLE*)Plugin_Query(53, 1); + if (Plugin_Query(53, 0, 0)) { + return (SML_TABLE*)Plugin_Query(53, 1, 0); } else { return 0; } @@ -2585,19 +2586,23 @@ uint32_t match_vars(char *dvnam, TS_FLOAT **fp, char **sp, uint32_t *ind) { if (slen == olen && *cp == dvnam[0]) { if (!strncmp(cp, dvnam, olen)) { uint16_t index = vtp[count].index; - if (vtp[count].bits.is_string == 0) { - if (vtp[count].bits.is_filter) { - // error - return 0; + if (vtp[count].bits.global > 0) { + if (vtp[count].bits.is_string == 0) { + if (vtp[count].bits.is_filter) { + // error + return 0; + } else { + *fp = &glob_script_mem.fvars[index]; + *ind = count; + return NUM_RES; + } } else { - *fp = &glob_script_mem.fvars[index]; + *sp = glob_script_mem.glob_snp + (index * glob_script_mem.max_ssize); *ind = count; - return NUM_RES; + return STR_RES; } } else { - *sp = glob_script_mem.glob_snp + (index * glob_script_mem.max_ssize); - *ind = count; - return STR_RES; + return 0; } } } @@ -2839,15 +2844,15 @@ char *isvar(char *lp, uint8_t *vtype, struct T_INDEX *tind, TS_FLOAT *fp, char * } const char *term="\n\r ])=+-/*%>4095) { AddLog(LOG_LEVEL_INFO, PSTR("PWL: result overflow: %d"), result.length()); } @@ -13160,9 +13171,16 @@ uint32_t script_i2c(uint8_t sel, uint16_t val, uint32_t val1) { switch (sel) { case 0: glob_script_mem.script_i2c_addr = val; -#if defined(ESP32) && defined(USE_I2C_BUS2) +#ifdef ESP32 if (val1 == 0) glob_script_mem.script_i2c_wire = &Wire; - else glob_script_mem.script_i2c_wire = &Wire1; + else { +#if defined(USE_I2C_BUS2) + glob_script_mem.script_i2c_wire = &Wire1; +#else + glob_script_mem.script_i2c_wire = &Wire; +#endif + } + #else glob_script_mem.script_i2c_wire = &Wire; #endif