diff --git a/tasmota/include/i18n.h b/tasmota/include/i18n.h index 75588497f..6b17b534a 100644 --- a/tasmota/include/i18n.h +++ b/tasmota/include/i18n.h @@ -461,6 +461,7 @@ #define D_CMND_WEBTIME "WebTime" #define D_CMND_WEBSENSOR "WebSensor" #define D_CMND_WEBGETCONFIG "WebGetConfig" +#define D_CMND_WEBRUN "WebRun" #define D_CMND_EMULATION "Emulation" #define D_CMND_SENDMAIL "Sendmail" #define D_CMND_CORS "CORS" diff --git a/tasmota/include/tasmota.h b/tasmota/include/tasmota.h index 6586e7f2b..7467fb913 100644 --- a/tasmota/include/tasmota.h +++ b/tasmota/include/tasmota.h @@ -512,10 +512,10 @@ enum DevGroupShareItem { DGR_SHARE_POWER = 1, DGR_SHARE_LIGHT_BRI = 2, DGR_SHARE enum CommandSource { SRC_IGNORE, SRC_MQTT, SRC_RESTART, SRC_BUTTON, SRC_SWITCH, SRC_BACKLOG, SRC_SERIAL, SRC_WEBGUI, SRC_WEBCOMMAND, SRC_WEBCONSOLE, SRC_PULSETIMER, SRC_TIMER, SRC_RULE, SRC_MAXPOWER, SRC_MAXENERGY, SRC_OVERTEMP, SRC_LIGHT, SRC_KNX, SRC_DISPLAY, SRC_WEMO, SRC_HUE, SRC_RETRY, SRC_REMOTE, SRC_SHUTTER, - SRC_THERMOSTAT, SRC_CHAT, SRC_TCL, SRC_BERRY, SRC_FILE, SRC_SSERIAL, SRC_USBCONSOLE, SRC_SO47, SRC_SENSOR, SRC_MAX }; + SRC_THERMOSTAT, SRC_CHAT, SRC_TCL, SRC_BERRY, SRC_FILE, SRC_SSERIAL, SRC_USBCONSOLE, SRC_SO47, SRC_SENSOR, SRC_WEB, SRC_MAX }; const char kCommandSource[] PROGMEM = "I|MQTT|Restart|Button|Switch|Backlog|Serial|WebGui|WebCommand|WebConsole|PulseTimer|" "Timer|Rule|MaxPower|MaxEnergy|Overtemp|Light|Knx|Display|Wemo|Hue|Retry|Remote|Shutter|" - "Thermostat|Chat|TCL|Berry|File|SSerial|UsbConsole|SO47|Sensor"; + "Thermostat|Chat|TCL|Berry|File|SSerial|UsbConsole|SO47|Sensor|Web"; const uint8_t kDefaultRfCode[9] PROGMEM = { 0x21, 0x16, 0x01, 0x0E, 0x03, 0x48, 0x2E, 0x1A, 0x00 }; diff --git a/tasmota/my_user_config.h b/tasmota/my_user_config.h index d658efed2..72c8bcb3b 100644 --- a/tasmota/my_user_config.h +++ b/tasmota/my_user_config.h @@ -474,6 +474,7 @@ #define USE_ENHANCED_GUI_WIFI_SCAN // Enable Wi-Fi scan output with BSSID (+0k5 code) // #define USE_WEBSEND_RESPONSE // Enable command WebSend response message (+1k code) // #define USE_WEBGETCONFIG // Enable restoring config from external webserver (+0k6) +// #define USE_WEBRUN // Enable executing a tasmota command file from external web server (+0.4 code) // #define USE_GPIO_VIEWER // Enable GPIO Viewer to see realtime GPIO states (+6k code) // #define GV_SAMPLING_INTERVAL 100 // [GvSampling] milliseconds - Use Tasmota Scheduler (100) or Ticker (20..99,101..1000) #define USE_EMULATION_HUE // Enable Hue Bridge emulation for Alexa (+14k code, +2k mem common) diff --git a/tasmota/tasmota_xdrv_driver/xdrv_01_9_webserver.ino b/tasmota/tasmota_xdrv_driver/xdrv_01_9_webserver.ino index 44ad0508d..7d7a5d94d 100644 --- a/tasmota/tasmota_xdrv_driver/xdrv_01_9_webserver.ino +++ b/tasmota/tasmota_xdrv_driver/xdrv_01_9_webserver.ino @@ -3320,6 +3320,169 @@ bool CaptivePortal(void) /*********************************************************************************************/ +enum {QUERY_DEFAULT=0, QUERY_RUN}; +int WebQuery(char *buffer, int query_function); + +#ifdef USE_WEBRUN +char *WebRunBuffer = nullptr; +char *WebRunContext = nullptr; +bool WebRunMutex = false; + +void WebRunLoop(void) +{ + if (WebRunBuffer && !WebRunMutex && BACKLOG_EMPTY) { + WebRunMutex = true; + char *command = strtok_r(WebRunContext, "\n\r", &WebRunContext); + if (command) { + while (isspace(*command)) command++; // skip space + if (*command && ';' != *command) + ExecuteCommand(command, SRC_WEB); + } else { + free(WebRunBuffer); + WebRunBuffer = WebRunContext = nullptr; + } + WebRunMutex = false; + } +} + +void WebRunInit(const char *command_buffer) +{ + if (!WebRunBuffer) { + int len = strlen(command_buffer); + WebRunContext = WebRunBuffer = (char*)malloc(len+1); + if (WebRunBuffer) { + memcpy(WebRunBuffer, command_buffer, len); + WebRunBuffer[len] = 0; + } else { + AddLog(LOG_LEVEL_DEBUG, PSTR("WEBRUN: not enough memory")); + } + } else { + AddLog(LOG_LEVEL_DEBUG, PSTR("WEBRUN: previous not completed")); + } +} +#endif // #ifdef USE_WEBRUN + + +int WebQuery(char *buffer, int query_function = 0) +{ + // http://192.168.1.1/path GET -> Sends HTTP GET http://192.168.1.1/path + // http://192.168.1.1/path POST {"some":"message"} -> Sends HTTP POST to http://192.168.1.1/path with body {"some":"message"} + // http://192.168.1.1/path PUT [Autorization: Bearer abcdxyz] potato -> Sends HTTP PUT to http://192.168.1.1/path with authorization header and body "potato" + // http://192.168.1.1/path PATCH patchInfo -> Sends HTTP PATCH to http://192.168.1.1/path with body "potato" + + // Valid HTTP Commands: GET, POST, PUT, and PATCH + // An unlimited number of headers can be sent per request, and a body can be sent for all command types + // The body will be ignored if sending a GET command + +#if defined(ESP32) && defined(USE_WEBCLIENT_HTTPS) + HTTPClientLight http; +#else // HTTP only + WiFiClient http_client; + HTTPClient http; +#endif + + int status = WEBCMND_WRONG_PARAMETERS; + + char *temp; + const char *url = strtok_r(buffer, " ", &temp); + const char *method = strtok_r(temp, " ", &temp); + + if (url) { +#if defined(ESP32) && defined(USE_WEBCLIENT_HTTPS) + if (http.begin(UrlEncode(url))) { +#else // HTTP only + if (http.begin(http_client, UrlEncode(url))) { +#endif + char empty_body[1] = { 0 }; + char *body = empty_body; + if (temp) { // There is a body and/or header + if (temp[0] == '[') { // Header information was sent; decode it + temp += 1; + temp = strtok_r(temp, "]", &body); + bool headerFound = true; + while (headerFound) { + char *header = strtok_r(temp, ":", &temp); + if (header) { + char *headerBody = strtok_r(temp, "|", &temp); + if (headerBody) { + http.addHeader(header, headerBody); + } + else headerFound = false; + } + else headerFound = false; + } + } else { // No header information was sent, but there was a body + body = temp; + } + } + + int http_code; + if ((!method) || 0 == strcasecmp_P(method, PSTR("GET"))) { http_code = http.GET(); } + else if (0 == strcasecmp_P(method, PSTR("POST"))) { http_code = http.POST(body); } + else if (0 == strcasecmp_P(method, PSTR("PUT"))) { http_code = http.PUT(body); } + else if (0 == strcasecmp_P(method, PSTR("PATCH"))) { http_code = http.PATCH(body); } + else return status; + + if (http_code > 0) { // http_code will be negative on error +#if defined(USE_WEBSEND_RESPONSE) || defined(USE_WEBRUN) + if (http_code == HTTP_CODE_OK || http_code == HTTP_CODE_MOVED_PERMANENTLY) { + // Return received data to the user - Adds 900+ bytes to the code + String response = http.getString(); // File found at server - may need lot of ram or trigger out of memory! + const char* read = response.c_str(); +// uint32_t len = response.length() + 1; +// AddLog(LOG_LEVEL_DEBUG, PSTR("DBG: Response '%*_H' = %s"), len, (uint8_t*)read, read); +#ifdef USE_WEBRUN + if (QUERY_RUN == query_function) + WebRunInit(read); +#endif +#ifdef USE_WEBSEND_RESPONSE + char text[3] = { 0 }; // Make room foor double % + text[0] = *read++; + if (text[0] != '\0') { + Response_P(PSTR("{\"" D_CMND_WEBQUERY "\":")); + bool assume_json = (text[0] == '{') || (text[0] == '['); + if (!assume_json) { ResponseAppend_P(PSTR("\"")); } + while (text[0] != '\0') { + if (text[0] > 31) { // Remove control characters like linefeed + if ('%' == text[0]) { // Fix char string formatting for % + text[1] = '%'; + } + if (assume_json) { + if (ResponseAppend_P(text) == ResponseSize()) { break; }; + } else { + if (ResponseAppend_P(EscapeJSONString(text).c_str()) == ResponseSize()) { break; }; + } + } + text[0] = *read++; + text[1] = '\0'; + } + if (!assume_json) { ResponseAppend_P(PSTR("\"")); } + ResponseJsonEnd(); +#ifdef USE_SCRIPT + // recursive call must be possible in this case + void script_setaflg(uint8_t flg); + script_setaflg(0); +#endif // USE_SCRIPT + status = WEBCMND_VALID_RESPONSE; + } else { +#endif // USE_WEBSEND_RESPONSE + status = WEBCMND_DONE; + } + } else +#endif // USE_WEBSEND_RESPONSE || USE_WEBRUN + status = WEBCMND_DONE; + } else { + status = WEBCMND_CONNECT_FAILED; + } + http.end(); // Clean up connection data + } else { + status = WEBCMND_HOST_NOT_FOUND; + } + } + return status; +} + + int WebSend(char *buffer) { // [tasmota] POWER1 ON --> Sends http://tasmota/cm?cmnd=POWER1 ON @@ -3364,120 +3527,6 @@ int WebSend(char *buffer) return status; } -int WebQuery(char *buffer) { - // http://192.168.1.1/path GET -> Sends HTTP GET http://192.168.1.1/path - // http://192.168.1.1/path POST {"some":"message"} -> Sends HTTP POST to http://192.168.1.1/path with body {"some":"message"} - // http://192.168.1.1/path PUT [Autorization: Bearer abcdxyz] potato -> Sends HTTP PUT to http://192.168.1.1/path with authorization header and body "potato" - // http://192.168.1.1/path PATCH patchInfo -> Sends HTTP PATCH to http://192.168.1.1/path with body "potato" - - // Valid HTTP Commands: GET, POST, PUT, and PATCH - // An unlimited number of headers can be sent per request, and a body can be sent for all command types - // The body will be ignored if sending a GET command - -#if defined(ESP32) && defined(USE_WEBCLIENT_HTTPS) - HTTPClientLight http; -#else // HTTP only - WiFiClient http_client; - HTTPClient http; -#endif - - int status = WEBCMND_WRONG_PARAMETERS; - - char *temp; - char *url = strtok_r(buffer, " ", &temp); - char *method = strtok_r(temp, " ", &temp); - - if (url && method) { -#if defined(ESP32) && defined(USE_WEBCLIENT_HTTPS) - if (http.begin(UrlEncode(url))) { -#else // HTTP only - if (http.begin(http_client, UrlEncode(url))) { -#endif - char empty_body[1] = { 0 }; - char *body = empty_body; - if (temp) { // There is a body and/or header - if (temp[0] == '[') { // Header information was sent; decode it - temp += 1; - temp = strtok_r(temp, "]", &body); - bool headerFound = true; - while (headerFound) { - char *header = strtok_r(temp, ":", &temp); - if (header) { - char *headerBody = strtok_r(temp, "|", &temp); - if (headerBody) { - http.addHeader(header, headerBody); - } - else headerFound = false; - } - else headerFound = false; - } - } else { // No header information was sent, but there was a body - body = temp; - } - } - - int http_code; - if (0 == strcasecmp_P(method, PSTR("GET"))) { http_code = http.GET(); } - else if (0 == strcasecmp_P(method, PSTR("POST"))) { http_code = http.POST(body); } - else if (0 == strcasecmp_P(method, PSTR("PUT"))) { http_code = http.PUT(body); } - else if (0 == strcasecmp_P(method, PSTR("PATCH"))) { http_code = http.PATCH(body); } - else return status; - - if (http_code > 0) { // http_code will be negative on error -#ifdef USE_WEBSEND_RESPONSE - if (http_code == HTTP_CODE_OK || http_code == HTTP_CODE_MOVED_PERMANENTLY) { - // Return received data to the user - Adds 900+ bytes to the code - String response = http.getString(); // File found at server - may need lot of ram or trigger out of memory! - const char* read = response.c_str(); - -// uint32_t len = response.length() + 1; -// AddLog(LOG_LEVEL_DEBUG, PSTR("DBG: Response '%*_H' = %s"), len, (uint8_t*)read, read); - - char text[3] = { 0 }; // Make room foor double % - text[0] = *read++; - if (text[0] != '\0') { - Response_P(PSTR("{\"" D_CMND_WEBQUERY "\":")); - bool assume_json = (text[0] == '{') || (text[0] == '['); - if (!assume_json) { ResponseAppend_P(PSTR("\"")); } - while (text[0] != '\0') { - if (text[0] > 31) { // Remove control characters like linefeed - if ('%' == text[0]) { // Fix char string formatting for % - text[1] = '%'; - } - if (assume_json) { - if (ResponseAppend_P(text) == ResponseSize()) { break; }; - } else { - if (ResponseAppend_P(EscapeJSONString(text).c_str()) == ResponseSize()) { break; }; - } - } - text[0] = *read++; - text[1] = '\0'; - } - if (!assume_json) { ResponseAppend_P(PSTR("\"")); } - ResponseJsonEnd(); -#ifdef USE_SCRIPT - // recursive call must be possible in this case - void script_setaflg(uint8_t flg); - script_setaflg(0); -#endif // USE_SCRIPT - status = WEBCMND_VALID_RESPONSE; - } else { - status = WEBCMND_DONE; - } - } else -#endif // USE_WEBSEND_RESPONSE - status = WEBCMND_DONE; - } else { - status = WEBCMND_CONNECT_FAILED; - } - http.end(); // Clean up connection data - } else { - status = WEBCMND_HOST_NOT_FOUND; - } - } - return status; -} - #ifdef USE_WEBGETCONFIG int WebGetConfig(char *buffer) { // http://user:password@server:port/path/%id%.dmp : %id% will be expanded to MAC address @@ -3597,6 +3646,9 @@ const char kWebCommands[] PROGMEM = "|" // No prefix #ifdef USE_WEBGETCONFIG "|" D_CMND_WEBGETCONFIG #endif +#ifdef USE_WEBRUN + "|" D_CMND_WEBRUN +#endif #ifdef USE_CORS "|" D_CMND_CORS #endif @@ -3618,6 +3670,9 @@ void (* const WebCommand[])(void) PROGMEM = { #ifdef USE_WEBGETCONFIG , &CmndWebGetConfig #endif +#ifdef USE_WEBRUN + , &CmndWebRun +#endif #ifdef USE_CORS , &CmndCors #endif @@ -3743,6 +3798,18 @@ void CmndWebQuery(void) { } } +#ifdef USE_WEBRUN +void CmndWebRun(void) { + if (XdrvMailbox.data_len > 0) { + uint32_t result = WebQuery(XdrvMailbox.data, QUERY_RUN); + if (result != WEBCMND_VALID_RESPONSE) { + char stemp1[20]; + ResponseCmndChar(GetTextIndexed(stemp1, sizeof(stemp1), result, kWebCmndStatus)); + } + } +} +#endif // #ifdef USE_WEBRUN + #ifdef USE_WEBGETCONFIG void CmndWebGetConfig(void) { // WebGetConfig http://myserver:8000/tasmota/conf/%id%.dmp where %id% is expanded to device mac address @@ -3875,6 +3942,9 @@ bool Xdrv01(uint32_t function) switch (function) { case FUNC_LOOP: PollDnsWebserver(); +#ifdef USE_WEBRUN + WebRunLoop(); +#endif // #ifdef USE_WEBRUN #ifdef USE_EMULATION if (Settings->flag2.emulation) { PollUdp(); } #endif // USE_EMULATION