// inspired by https://github.com/MoritzLerch/tesla-pv-display #ifndef Powerwall_h #define Powerwall_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: String powerwall_ip; String tesla_email; String tesla_password; String authCookie; String cts1; String cts2; public: Powerwall(); String getAuthCookie(); String GetRequest(String url, String authCookie); String GetRequest(String url); String AuthCookie(); 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; } 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(PWL_LOGLVL, PSTR("PWL: requesting new auth Cookie from %s"), powerwall_ip.c_str()); String apiLoginURL = "/api/login/Basic"; 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; while (retry < PW_RETRIES) { int32_t res = ssl_client.connect(powerwall_ip.c_str(), 443); if (res) { break; } delay(100); retry++; } if (retry >= PW_RETRIES) { return ("CONN-FAIL"); } AddLog(PWL_LOGLVL, PSTR("PWL: connected")); String dataString = "{\"username\":\"customer\",\"email\":\"" + tesla_email + "\",\"password\":\"" + tesla_password + "\",\"force_sm_off\":false}"; String payload = String("POST ") + apiLoginURL + " HTTP/1.1\r\n" + "Host: " + powerwall_ip + "\r\n" + "Connection: close" + "\r\n" + "Content-Type: application/json" + "\r\n" + "Content-Length: " + dataString.length() + "\r\n" + "\r\n" + dataString + "\r\n\r\n"; AddLog(PWL_LOGLVL, PSTR("PWL: payload: %s"),payload.c_str()); 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; 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[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(PWL_LOGLVL, PSTR("PWL: token: %s"), str_value); ssl_client.stop(); return str_value; } /** * This function does a GET-request on the local powerwall web server. * This is mainly used here to do API requests. * HTTP/1.0 is used because some responses are so big that this would encounter * chunked transfer encoding in HTTP/1.1 (https://en.wikipedia.org/wiki/Chunked_transfer_encoding) * * @param url relative URL on the Powerwall * @param authCookie optional, but recommended * @returns content of request */ String Powerwall::GetRequest(String url, String in_authCookie) { 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(PWL_LOGLVL, PSTR("PWL: doing GET-request to %s - %s"), powerwall_ip.c_str(), url.c_str()); int retry = 0; while ((!ssl_client.connect(powerwall_ip.c_str(), 443)) && (retry < PW_RETRIES)) { delay(100); //Serial.print("."); retry++; } if (retry >= PW_RETRIES) { return ("CONN-FAIL"); } AddLog(PWL_LOGLVL, PSTR("PWL: connected")); // HTTP/1.0 is used because of Chunked transfer encoding 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"; ssl_client.println(request); AddLog(PWL_LOGLVL, PSTR("PWL: request: %s"), request.c_str()); 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; } } } timeout--; delay(10); if (!timeout) { break; } } 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; } /** * 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())); } #endif