diff --git a/src/command.cpp b/src/command.cpp index eb1d46041..1bb3f878c 100644 --- a/src/command.cpp +++ b/src/command.cpp @@ -204,7 +204,7 @@ uint8_t Command::process(const char * path, const bool is_admin, const JsonObjec return return_code; } -std::string Command::return_code_string(const uint8_t return_code) { +const char * Command::return_code_string(const uint8_t return_code) { switch (return_code) { case CommandRet::ERROR: return "Error"; @@ -380,9 +380,9 @@ uint8_t Command::call(const uint8_t device_type, const char * cmd, const char * if (return_code != CommandRet::OK) { char error[100]; if (single_command) { - snprintf(error, sizeof(error), "Command '%s' failed (%s)", cmd, FL_(cmdRet)[return_code]); + snprintf(error, sizeof(error), "Command '%s' failed (%s)", cmd, return_code_string(return_code)); } else { - snprintf(error, sizeof(error), "Command '%s: %s' failed (%s)", cmd, value, FL_(cmdRet)[return_code]); + snprintf(error, sizeof(error), "Command '%s: %s' failed (%s)", cmd, value, return_code_string(return_code)); } output.clear(); output["message"] = error; diff --git a/src/command.h b/src/command.h index c718f9224..45827b644 100644 --- a/src/command.h +++ b/src/command.h @@ -51,8 +51,6 @@ enum CommandRet : uint8_t { INVALID // 5 - invalid (tag) }; -MAKE_ENUM_FIXED(cmdRet, "fail", "ok", "not found", "error", "not allowed", "invalid") - using cmd_function_p = std::function; using cmd_json_function_p = std::function; @@ -138,7 +136,7 @@ class Command { static const char * parse_command_string(const char * command, int8_t & id); - static std::string return_code_string(const uint8_t return_code); + static const char * return_code_string(const uint8_t return_code); private: static uuid::log::Logger logger_; diff --git a/src/emsesp.cpp b/src/emsesp.cpp index 3e84cff4f..2978fbc7c 100644 --- a/src/emsesp.cpp +++ b/src/emsesp.cpp @@ -1719,7 +1719,6 @@ void EMSESP::loop() { analogsensor_.loop(); // read analog sensor values publish_all_loop(); // with HA messages in parts to avoid flooding the mqtt queue mqtt_.loop(); // sends out anything in the MQTT queue - webSchedulerService.loop(); // handle any scheduled jobs webModulesService.loop(); // loop through the external library modules // force a query on the EMS devices to fetch latest data at a set interval (1 min) diff --git a/src/modbus.cpp b/src/modbus.cpp index bd6e06612..fafd54958 100644 --- a/src/modbus.cpp +++ b/src/modbus.cpp @@ -440,9 +440,9 @@ ModbusMessage Modbus::handleWrite(const ModbusMessage & request) { sizeof(error), "Modbus write command failed with error: %s (%s)", (const char *)output["message"], - Command::return_code_string(return_code).c_str()); + Command::return_code_string(return_code)); } else { - snprintf(error, sizeof(error), "Modbus write command failed with error code (%s)", Command::return_code_string(return_code).c_str()); + snprintf(error, sizeof(error), "Modbus write command failed with error code (%s)", Command::return_code_string(return_code)); } LOG_ERROR(error); response.setError(request.getServerID(), request.getFunctionCode(), ILLEGAL_DATA_VALUE); diff --git a/src/mqtt.cpp b/src/mqtt.cpp index 91f578182..2833c1a16 100644 --- a/src/mqtt.cpp +++ b/src/mqtt.cpp @@ -285,13 +285,9 @@ void Mqtt::on_message(const char * topic, const uint8_t * payload, size_t len) { if (return_code != CommandRet::OK) { char error[100]; if (output.size()) { - snprintf(error, - sizeof(error), - "MQTT command failed with error %s (%s)", - (const char *)output["message"], - Command::return_code_string(return_code).c_str()); + snprintf(error, sizeof(error), "MQTT command failed with error %s (%s)", (const char *)output["message"], Command::return_code_string(return_code)); } else { - snprintf(error, sizeof(error), "MQTT command failed with error %s", Command::return_code_string(return_code).c_str()); + snprintf(error, sizeof(error), "MQTT command failed with error %s", Command::return_code_string(return_code)); } LOG_ERROR(error); Mqtt::queue_publish("response", error); diff --git a/src/web/WebDataService.cpp b/src/web/WebDataService.cpp index 073812506..46c2d26a1 100644 --- a/src/web/WebDataService.cpp +++ b/src/web/WebDataService.cpp @@ -259,7 +259,7 @@ void WebDataService::write_device_value(AsyncWebServerRequest * request, JsonVar // write debug if (return_code != CommandRet::OK) { - EMSESP::logger().err("Write command failed %s (%s)", (const char *)output["message"], Command::return_code_string(return_code).c_str()); + EMSESP::logger().err("Write command failed %s (%s)", (const char *)output["message"], Command::return_code_string(return_code)); } else { #if defined(EMSESP_DEBUG) EMSESP::logger().debug("Write command successful"); @@ -292,7 +292,7 @@ void WebDataService::write_device_value(AsyncWebServerRequest * request, JsonVar return_code = Command::call(device_type, cmd, Helpers::render_value(s, data.as(), 1), true, id, output); } if (return_code != CommandRet::OK) { - EMSESP::logger().err("Write command failed %s (%s)", (const char *)output["message"], Command::return_code_string(return_code).c_str()); + EMSESP::logger().err("Write command failed %s (%s)", (const char *)output["message"], Command::return_code_string(return_code)); } else { #if defined(EMSESP_DEBUG) EMSESP::logger().debug("Write command successful"); diff --git a/src/web/WebSchedulerService.cpp b/src/web/WebSchedulerService.cpp index 2be883a8a..34560eb6f 100644 --- a/src/web/WebSchedulerService.cpp +++ b/src/web/WebSchedulerService.cpp @@ -38,6 +38,7 @@ void WebSchedulerService::begin() { char topic[Mqtt::MQTT_TOPIC_MAX_SIZE]; snprintf(topic, sizeof(topic), "%s/#", F_(scheduler)); Mqtt::subscribe(EMSdevice::DeviceType::SCHEDULER, topic, nullptr); // use empty function callback + xTaskCreate((TaskFunction_t)scheduler_task, "scheduler_task", 4096, NULL, 3, NULL); } // this creates the scheduler file, saving it to the FS @@ -330,65 +331,60 @@ bool WebSchedulerService::has_commands() { } // execute scheduled command -bool WebSchedulerService::command(const char * name, const char * cmd, const char * data) { +bool WebSchedulerService::command(const char * name, const std::string & command, const std::string & data) { + std::string cmd = Helpers::toLower(command); + // check http commands. e.g. // tasmota(get): http:///cm?cmnd=power%20ON // shelly(get): http:///relais/0?turn=on - const char * c = strchr(cmd, '{'); - if (c) { - // parse json - JsonDocument doc; - int httpResult = 0; - if (DeserializationError::Ok == deserializeJson(doc, c)) { - HTTPClient http; - String url = doc["url"]; - if (http.begin(url)) { - // It's an HTTP call + // parse json + JsonDocument doc; + if (DeserializationError::Ok == deserializeJson(doc, cmd)) { + HTTPClient http; + int httpResult = 0; + String url = doc["url"]; + if (http.begin(url)) { + // It's an HTTP call - // add any given headers - for (JsonPair p : doc["header"].as()) { - http.addHeader(p.key().c_str(), p.value().as().c_str()); - } - String value = doc["value"] | data; // extract value if its in the command, or take the data - String method = doc["method"] | "GET"; // default GET - - // if there is data, force a POST - if (value.length()) { - if (value.startsWith("{")) { - http.addHeader("Content-Type", "application/json"); // auto-set to JSON - } - httpResult = http.POST(value); - } else { - // no value, but check if it still a POST request - if (method == "POST") { - httpResult = http.POST(value); - } else { - httpResult = http.GET(); // normal GET - } - } - - http.end(); - - // check HTTP return code - if (httpResult != 200) { - char error[100]; - snprintf(error, sizeof(error), "Schedule %s: URL command failed with http code %d", name, httpResult); - emsesp::EMSESP::logger().warning(error); - return false; - } -#if defined(EMSESP_DEBUG) - char msg[100]; - snprintf(msg, sizeof(msg), "Schedule %s: URL command successful with http code %d", name, httpResult); - emsesp::EMSESP::logger().debug(msg); -#endif - return true; + // add any given headers + for (JsonPair p : doc["header"].as()) { + http.addHeader(p.key().c_str(), p.value().as().c_str()); } + String value = doc["value"] | data.c_str(); // extract value if its in the command, or take the data + String method = doc["method"] | "GET"; // default GET + + // if there is data, force a POST + if (value.length() || method == "post") { // we have all lowercase + if (value.startsWith("{")) { + http.addHeader("Content-Type", "application/json"); // auto-set to JSON + } + httpResult = http.POST(value); + } else { + httpResult = http.GET(); // normal GET + } + + http.end(); + + // check HTTP return code + if (httpResult != 200) { + char error[100]; + snprintf(error, sizeof(error), "Schedule %s: URL command failed with http code %d", name, httpResult); + emsesp::EMSESP::logger().warning(error); + return false; + } +#if defined(EMSESP_DEBUG) + char msg[100]; + snprintf(msg, sizeof(msg), "Schedule %s: URL command successful with http code %d", name, httpResult); + emsesp::EMSESP::logger().debug(msg); +#endif + return true; } + // we can add other json tests here } - JsonDocument doc_input; - JsonObject input = doc_input.to(); - if (strlen(data)) { // empty data queries a value + doc.clear(); + JsonObject input = doc.to(); + if (!data.empty()) { // empty data queries a value input["data"] = data; } @@ -397,15 +393,15 @@ bool WebSchedulerService::command(const char * name, const char * cmd, const cha // prefix "api/" to command string char command_str[COMMAND_MAX_LENGTH]; - snprintf(command_str, sizeof(command_str), "/api/%s", cmd); + snprintf(command_str, sizeof(command_str), "/api/%s", cmd.c_str()); uint8_t return_code = Command::process(command_str, true, input, output); // admin set if (return_code == CommandRet::OK) { #if defined(EMSESP_DEBUG) - EMSESP::logger().debug("Schedule command '%s' with data '%s' was successful", cmd, data); + EMSESP::logger().debug("Schedule command '%s' with data '%s' was successful", cmd.c_str(), data.c_str()); #endif - if (strlen(data) == 0 && output.size()) { + if (data.empty() && output.size()) { Mqtt::queue_publish("response", output); } return true; @@ -416,7 +412,7 @@ bool WebSchedulerService::command(const char * name, const char * cmd, const cha // check for empty name snprintf(error, sizeof(error), "Schedule %s: %s", name ? name : "", (const char *)output["message"]); // use error message if we have it } else { - snprintf(error, sizeof(error), "Schedule %s: command %s failed with error %s", name, cmd, Command::return_code_string(return_code).c_str()); + snprintf(error, sizeof(error), "Schedule %s: command %s failed with error %s", name, cmd.c_str(), Command::return_code_string(return_code)); } emsesp::EMSESP::logger().warning(error); @@ -426,17 +422,8 @@ bool WebSchedulerService::command(const char * name, const char * cmd, const cha #include "shuntingYard.hpp" bool WebSchedulerService::onChange(const char * cmd) { - bool cmd_ok = false; - for (const ScheduleItem & scheduleItem : *scheduleItems_) { - if (scheduleItem.active && scheduleItem.flags == SCHEDULEFLAG_SCHEDULE_ONCHANGE - && Helpers::toLower(scheduleItem.time).find(Helpers::toLower(cmd)) != std::string::npos) { -#ifdef EMESESP_DEBUG - // emsesp::EMSESP::logger().debug(scheduleItem.cmd.c_str()); -#endif - cmd_ok |= command(scheduleItem.name.c_str(), scheduleItem.cmd.c_str(), compute(scheduleItem.value).c_str()); - } - } - return cmd_ok; + cmd_changed_.push_back(Helpers::toLower(cmd)); + return true; } void WebSchedulerService::condition() { @@ -447,7 +434,7 @@ void WebSchedulerService::condition() { // emsesp::EMSESP::logger().debug("condition match: %s", match.c_str()); #endif if (match.length() == 1 && match[0] == '1' && scheduleItem.retry_cnt == 0xFF) { - scheduleItem.retry_cnt = command(scheduleItem.name.c_str(), scheduleItem.cmd.c_str(), compute(scheduleItem.value).c_str()) ? 1 : 0xFF; + scheduleItem.retry_cnt = command(scheduleItem.name.c_str(), scheduleItem.cmd, compute(scheduleItem.value)) ? 1 : 0xFF; } else if (match.length() == 1 && match[0] == '0' && scheduleItem.retry_cnt == 1) { scheduleItem.retry_cnt = 0xFF; } else if (match.length() != 1) { // the match is not boolean @@ -458,7 +445,6 @@ void WebSchedulerService::condition() { } // process any scheduled jobs -// checks on the minute and at startup void WebSchedulerService::loop() { // initialize static value on startup static int8_t last_tm_min = -2; // invalid value also used for startup commands @@ -470,6 +456,17 @@ void WebSchedulerService::loop() { return; } + // check if we have onChange events + while (!cmd_changed_.empty()) { + for (const ScheduleItem & scheduleItem : *scheduleItems_) { + if (scheduleItem.active && scheduleItem.flags == SCHEDULEFLAG_SCHEDULE_ONCHANGE + && Helpers::toLower(scheduleItem.time).find(cmd_changed_.front()) != std::string::npos) { + command(scheduleItem.name.c_str(), scheduleItem.cmd, compute(scheduleItem.value)); + } + } + cmd_changed_.pop_front(); + } + // check conditions every 10 seconds uint32_t uptime_sec = uuid::get_uptime_sec() / 10; if (last_uptime_sec != uptime_sec) { @@ -481,7 +478,7 @@ void WebSchedulerService::loop() { if (last_tm_min == -2) { for (ScheduleItem & scheduleItem : *scheduleItems_) { if (scheduleItem.active && scheduleItem.flags == SCHEDULEFLAG_SCHEDULE_TIMER && scheduleItem.elapsed_min == 0) { - scheduleItem.retry_cnt = command(scheduleItem.name.c_str(), scheduleItem.cmd.c_str(), compute(scheduleItem.value).c_str()) ? 0xFF : 0; + scheduleItem.retry_cnt = command(scheduleItem.name.c_str(), scheduleItem.cmd, compute(scheduleItem.value)) ? 0xFF : 0; } } last_tm_min = -1; // startup done, now use for RTC @@ -494,13 +491,12 @@ void WebSchedulerService::loop() { // retry startup commands not yet executed if (scheduleItem.active && scheduleItem.flags == SCHEDULEFLAG_SCHEDULE_TIMER && scheduleItem.elapsed_min == 0 && scheduleItem.retry_cnt < MAX_STARTUP_RETRIES) { - scheduleItem.retry_cnt = - command(scheduleItem.name.c_str(), scheduleItem.cmd.c_str(), scheduleItem.value.c_str()) ? 0xFF : scheduleItem.retry_cnt + 1; + scheduleItem.retry_cnt = command(scheduleItem.name.c_str(), scheduleItem.cmd, scheduleItem.value) ? 0xFF : scheduleItem.retry_cnt + 1; } // scheduled timer commands if (scheduleItem.active && scheduleItem.flags == SCHEDULEFLAG_SCHEDULE_TIMER && scheduleItem.elapsed_min > 0 && (uptime_min % scheduleItem.elapsed_min == 0)) { - command(scheduleItem.name.c_str(), scheduleItem.cmd.c_str(), compute(scheduleItem.value).c_str()); + command(scheduleItem.name.c_str(), scheduleItem.cmd, compute(scheduleItem.value)); } } last_uptime_min = uptime_min; @@ -517,13 +513,23 @@ void WebSchedulerService::loop() { for (const ScheduleItem & scheduleItem : *scheduleItems_) { uint8_t dow = scheduleItem.flags & SCHEDULEFLAG_SCHEDULE_TIMER ? 0 : scheduleItem.flags; if (scheduleItem.active && (real_dow & dow) && real_min == scheduleItem.elapsed_min) { - command(scheduleItem.name.c_str(), scheduleItem.cmd.c_str(), compute(scheduleItem.value).c_str()); + command(scheduleItem.name.c_str(), scheduleItem.cmd, compute(scheduleItem.value)); } } last_tm_min = tm->tm_min; } } +void WebSchedulerService::scheduler_task(void * pvParameters) { + while (1) { + delay(100); // no need to hurry + if (!EMSESP::system_.upload_status()) { + EMSESP::webSchedulerService.loop(); + } + } + vTaskDelete(NULL); +} + // hard coded tests #if defined(EMSESP_TEST) void WebSchedulerService::test() { diff --git a/src/web/WebSchedulerService.h b/src/web/WebSchedulerService.h index b0a50a059..19b0ef32c 100644 --- a/src/web/WebSchedulerService.h +++ b/src/web/WebSchedulerService.h @@ -73,7 +73,9 @@ class WebSchedulerService : public StatefulService { #ifndef EMSESP_STANDALONE private: #endif - bool command(const char * name, const char * cmd, const char * data); + static void scheduler_task(void * pvParameters); + + bool command(const char * name, const std::string & cmd, const std::string & data); void condition(); HttpEndpoint _httpEndpoint; @@ -81,6 +83,7 @@ class WebSchedulerService : public StatefulService { std::list * scheduleItems_; // pointer to the list of schedule events bool ha_registered_ = false; + std::deque cmd_changed_; }; } // namespace emsesp diff --git a/src/web/shuntingYard.hpp b/src/web/shuntingYard.hpp index f0dd629b9..9abd608a4 100644 --- a/src/web/shuntingYard.hpp +++ b/src/web/shuntingYard.hpp @@ -622,18 +622,13 @@ std::string compute(const std::string & expr) { String method = doc["method"] | "GET"; // default GET // if there is data, force a POST - if (value.length()) { + if (value.length() || method == "post") { if (value.startsWith("{")) { http.addHeader("Content-Type", "application/json"); // auto-set to JSON } httpResult = http.POST(value); } else { - // no value, but check if it still a POST request - if (method == "POST") { - httpResult = http.POST(value); - } else { - httpResult = http.GET(); // normal GET - } + httpResult = http.GET(); // normal GET } http.end();