mirror of
https://github.com/emsesp/EMS-ESP32.git
synced 2026-06-18 22:06:25 +03:00
first try
This commit is contained in:
@@ -716,6 +716,10 @@ bool Command::device_has_commands(const uint8_t device_type) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (device_type == EMSdevice::DeviceType::COMMAND) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (device_type == EMSdevice::DeviceType::CUSTOM) {
|
||||
return true;
|
||||
}
|
||||
@@ -741,6 +745,7 @@ bool Command::device_has_commands(const uint8_t device_type) {
|
||||
void Command::show_devices(uuid::console::Shell & shell) {
|
||||
shell.printf("%s ", EMSdevice::device_type_2_device_name(EMSdevice::DeviceType::SYSTEM));
|
||||
shell.printf("%s ", EMSdevice::device_type_2_device_name(EMSdevice::DeviceType::CUSTOM));
|
||||
shell.printf("%s ", EMSdevice::device_type_2_device_name(EMSdevice::DeviceType::COMMAND));
|
||||
shell.printf("%s ", EMSdevice::device_type_2_device_name(EMSdevice::DeviceType::SCHEDULER));
|
||||
if (EMSESP::sensor_enabled()) {
|
||||
shell.printf("%s ", EMSdevice::device_type_2_device_name(EMSdevice::DeviceType::TEMPERATURESENSOR));
|
||||
@@ -779,6 +784,7 @@ void Command::show_all(uuid::console::Shell & shell) {
|
||||
// show system ones first
|
||||
show(shell, EMSdevice::DeviceType::SYSTEM, true);
|
||||
show(shell, EMSdevice::DeviceType::CUSTOM, true);
|
||||
show(shell, EMSdevice::DeviceType::COMMAND, true);
|
||||
show(shell, EMSdevice::DeviceType::SCHEDULER, true);
|
||||
|
||||
// then sensors
|
||||
|
||||
@@ -145,6 +145,8 @@ const char * EMSdevice::device_type_2_device_name(const uint8_t device_type) {
|
||||
return F_(scheduler);
|
||||
case DeviceType::CUSTOM:
|
||||
return F_(custom);
|
||||
case DeviceType::COMMAND:
|
||||
return F_(commands);
|
||||
case DeviceType::BOILER:
|
||||
return F_(boiler);
|
||||
case DeviceType::THERMOSTAT:
|
||||
@@ -297,6 +299,9 @@ uint8_t EMSdevice::device_name_2_device_type(const char * topic) {
|
||||
if (!strcmp(lowtopic, F_(scheduler))) {
|
||||
return DeviceType::SCHEDULER;
|
||||
}
|
||||
if (!strcmp(lowtopic, F_(commands))) {
|
||||
return DeviceType::COMMAND;
|
||||
}
|
||||
if (!strcmp(lowtopic, F_(system))) {
|
||||
return DeviceType::SYSTEM;
|
||||
}
|
||||
|
||||
@@ -405,6 +405,7 @@ class EMSdevice {
|
||||
// Unique Identifiers for each Device type, used in Dashboard table
|
||||
// 100 and above is reserved for DeviceType
|
||||
enum DeviceTypeUniqueID : uint8_t {
|
||||
COMMAND_UID = 95,
|
||||
SCHEDULER_UID = 96,
|
||||
ANALOGSENSOR_UID = 97,
|
||||
TEMPERATURESENSOR_UID = 98,
|
||||
@@ -417,6 +418,7 @@ class EMSdevice {
|
||||
ANALOGSENSOR, // for internal analog sensors
|
||||
SCHEDULER, // for internal schedule
|
||||
CUSTOM, // for user defined entities
|
||||
COMMAND, // for user defined commands
|
||||
BOILER, // from here on enum the ems-devices
|
||||
THERMOSTAT,
|
||||
MIXER,
|
||||
|
||||
@@ -56,6 +56,7 @@ ESP32React EMSESP::esp32React(&webServer, &dummyFS);
|
||||
WebSettingsService EMSESP::webSettingsService = WebSettingsService(&webServer, &dummyFS, EMSESP::esp32React.getSecurityManager());
|
||||
WebCustomizationService EMSESP::webCustomizationService = WebCustomizationService(&webServer, &dummyFS, EMSESP::esp32React.getSecurityManager());
|
||||
WebSchedulerService EMSESP::webSchedulerService = WebSchedulerService(&webServer, &dummyFS, EMSESP::esp32React.getSecurityManager());
|
||||
WebCommandService EMSESP::webCommandService = WebCommandService(&webServer, &dummyFS, EMSESP::esp32React.getSecurityManager());
|
||||
WebCustomEntityService EMSESP::webCustomEntityService = WebCustomEntityService(&webServer, &dummyFS, EMSESP::esp32React.getSecurityManager());
|
||||
WebModulesService EMSESP::webModulesService = WebModulesService(&webServer, &dummyFS, EMSESP::esp32React.getSecurityManager());
|
||||
#else
|
||||
@@ -63,6 +64,7 @@ ESP32React EMSESP::esp32React(&webServer, &LittleFS);
|
||||
WebSettingsService EMSESP::webSettingsService = WebSettingsService(&webServer, &LittleFS, EMSESP::esp32React.getSecurityManager());
|
||||
WebCustomizationService EMSESP::webCustomizationService = WebCustomizationService(&webServer, &LittleFS, EMSESP::esp32React.getSecurityManager());
|
||||
WebSchedulerService EMSESP::webSchedulerService = WebSchedulerService(&webServer, &LittleFS, EMSESP::esp32React.getSecurityManager());
|
||||
WebCommandService EMSESP::webCommandService = WebCommandService(&webServer, &LittleFS, EMSESP::esp32React.getSecurityManager());
|
||||
WebCustomEntityService EMSESP::webCustomEntityService = WebCustomEntityService(&webServer, &LittleFS, EMSESP::esp32React.getSecurityManager());
|
||||
WebModulesService EMSESP::webModulesService = WebModulesService(&webServer, &LittleFS, EMSESP::esp32React.getSecurityManager());
|
||||
#endif
|
||||
@@ -682,6 +684,7 @@ void EMSESP::publish_other_values() {
|
||||
// publish_device_values(EMSdevice::DeviceType::GENERIC);
|
||||
|
||||
webSchedulerService.publish();
|
||||
webCommandService.publish();
|
||||
webCustomEntityService.publish();
|
||||
}
|
||||
|
||||
@@ -788,6 +791,11 @@ bool EMSESP::get_device_value_info(JsonObject root, const char * cmd, const int8
|
||||
return webSchedulerService.get_value_info(root, cmd);
|
||||
}
|
||||
|
||||
// commands
|
||||
if (devicetype == DeviceType::COMMAND) {
|
||||
return webCommandService.get_value_info(root, cmd);
|
||||
}
|
||||
|
||||
// custom entities
|
||||
if (devicetype == DeviceType::CUSTOM) {
|
||||
return webCustomEntityService.get_value_info(root, cmd);
|
||||
@@ -1761,6 +1769,7 @@ void EMSESP::start() {
|
||||
// this will also handle any MQTT subscriptions
|
||||
webCustomizationService.begin(); // load the customizations
|
||||
webSchedulerService.begin(); // load the scheduler events
|
||||
webCommandService.begin(); // load the user commands
|
||||
webCustomEntityService.begin(); // load the custom telegram reads
|
||||
|
||||
// perform any system upgrades
|
||||
|
||||
@@ -51,6 +51,7 @@
|
||||
#include "../web/WebSettingsService.h"
|
||||
#include "../web/WebCustomizationService.h"
|
||||
#include "../web/WebSchedulerService.h"
|
||||
#include "../web/WebCommandService.h"
|
||||
#include "../web/WebAPIService.h"
|
||||
#include "../web/WebLogService.h"
|
||||
#include "../web/WebCustomEntityService.h"
|
||||
@@ -260,6 +261,7 @@ class EMSESP {
|
||||
static WebLogService webLogService;
|
||||
static WebCustomizationService webCustomizationService;
|
||||
static WebSchedulerService webSchedulerService;
|
||||
static WebCommandService webCommandService;
|
||||
static WebCustomEntityService webCustomEntityService;
|
||||
static WebModulesService webModulesService;
|
||||
|
||||
|
||||
@@ -76,6 +76,7 @@ MAKE_WORD_TRANSLATION(watch_cmd, "watch incoming telegrams", "Beobachte eingehen
|
||||
MAKE_WORD_TRANSLATION(publish_cmd, "publish all to MQTT", "Publiziere MQTT", "publiceer alles naar MQTT", "publicera allt till MQTT", "opublikuj wszystko na MQTT", "Publiser alt til MQTT", "publier tout vers MQTT", "Hepsini MQTTye gönder", "pubblica tutto su MQTT", "zverejniť všetko na MQTT", "publikovat vše do MQTT")
|
||||
MAKE_WORD_TRANSLATION(system_info_cmd, "show system info", "Zeige Systeminformationen", "toon systeemstatus", "visa systeminformation", "pokaż status systemu", "vis system status", "afficher les informations système", "Sistem Durumunu Göster", "visualizza stati di sistema", "zobraziť stav systému", "zobrazit informace o systému")
|
||||
MAKE_WORD_TRANSLATION(schedule_cmd, "enable schedule item", "Aktiviere Zeitplanelemente", "activeer tijdschema item", "aktivera schemalagt objekt", "aktywuj wybrany harmonogram", "aktiver planlagt element", "activer élément programmé", "program öğesini etkinleştir", "abilitare l'elemento programmato", "povoliť položku plánovania", "povolit položku plánování")
|
||||
MAKE_WORD_TRANSLATION(command_cmd, "execute command", "Befehl ausführen", "opdracht uitvoeren", "kör kommando", "wykonaj polecenie", "kjør kommando", "exécuter commande", "komut çalıştır", "esegui comando", "vykonať príkaz", "provést příkaz")
|
||||
MAKE_WORD_TRANSLATION(entity_cmd, "set custom value", "Sende eigene Entitäten", "verstuur custom waarde", "sätt ett eget värde", "wyślij własną wartość", "sett egendefinert verdi", "définir valeur personnalisée", "özel değer ayarla", "imposta valori personalizzati", "nastaviť vlastnú hodnotu", "nastavit vlastní hodnotu")
|
||||
MAKE_WORD_TRANSLATION(commands_response, "get response", "Hole Antwort", "Verzoek om antwoord", "hämta svar", "uzyskaj odpowiedź", "få svar", "obtenir réponse", "yanıt al", "ottieni risposta", "získať odpoveď", "získat odpověď")
|
||||
MAKE_WORD_TRANSLATION(coldshot_cmd, "send a cold shot of water", "Zugabe einer Menge kalten Wassers", "stuur koud water", "sckicka en liten mängd kallvatten", "uruchom tryśnięcie zimnej wody", "send kaldtvannspuls", "envoyer de l'eau froide", "soğuk su gönder", "invia acqua fredda", "pošlite studenú dávku vody", "poslat studenou vodu")
|
||||
|
||||
@@ -510,6 +510,7 @@ void Mqtt::on_connect() {
|
||||
// send initial MQTT messages for some of our services
|
||||
EMSESP::system_.send_heartbeat(); // send heartbeat
|
||||
EMSESP::webCustomEntityService.publish(true);
|
||||
EMSESP::webCommandService.publish(true);
|
||||
EMSESP::webSchedulerService.publish(true);
|
||||
EMSESP::analogsensor_.publish_values(true);
|
||||
EMSESP::temperaturesensor_.publish_values(true);
|
||||
|
||||
@@ -2610,6 +2610,12 @@ bool System::command_info(const char * value, const int8_t id, JsonObject output
|
||||
obj["name"] = F_(scheduler);
|
||||
obj["entities"] = EMSESP::webSchedulerService.count_entities();
|
||||
}
|
||||
if (EMSESP::webCommandService.count_entities()) {
|
||||
JsonObject obj = devices.add<JsonObject>();
|
||||
obj["type"] = F_(commands);
|
||||
obj["name"] = F_(commands);
|
||||
obj["entities"] = EMSESP::webCommandService.count_entities();
|
||||
}
|
||||
if (EMSESP::webCustomEntityService.count_entities()) {
|
||||
JsonObject obj = devices.add<JsonObject>();
|
||||
obj["type"] = F_(custom);
|
||||
|
||||
292
src/web/WebCommandService.cpp
Normal file
292
src/web/WebCommandService.cpp
Normal file
@@ -0,0 +1,292 @@
|
||||
/*
|
||||
* EMS-ESP - https://github.com/emsesp/EMS-ESP
|
||||
* Copyright 2020-2025 emsesp.org
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "emsesp.h"
|
||||
#include "WebCommandService.h"
|
||||
|
||||
#include "shuntingYard.h"
|
||||
|
||||
namespace emsesp {
|
||||
|
||||
WebCommandService::WebCommandService(AsyncWebServer * server, FS * fs, SecurityManager * securityManager)
|
||||
: _httpEndpoint(WebCommands::read, WebCommands::update, this, server, EMSESP_COMMAND_SERVICE_PATH, securityManager, AuthenticationPredicates::IS_AUTHENTICATED)
|
||||
, _fsPersistence(WebCommands::read, WebCommands::update, this, fs, EMSESP_COMMAND_FILE) {
|
||||
}
|
||||
|
||||
void WebCommandService::begin() {
|
||||
_fsPersistence.readFromFS();
|
||||
|
||||
EMSESP::webCommandService.read([&](WebCommands & webCommands) { commandItems_ = &webCommands.commandItems; });
|
||||
|
||||
EMSESP::logger().info("Starting Commands service");
|
||||
char topic[Mqtt::MQTT_TOPIC_MAX_SIZE];
|
||||
snprintf(topic, sizeof(topic), "%s/#", F_(commands));
|
||||
Mqtt::subscribe(EMSdevice::DeviceType::COMMAND, topic, nullptr);
|
||||
}
|
||||
|
||||
void WebCommands::read(WebCommands & webCommands, JsonObject root) {
|
||||
JsonArray items = root["commands"].to<JsonArray>();
|
||||
uint8_t counter = 1;
|
||||
for (const CommandItem & ci : webCommands.commandItems) {
|
||||
JsonObject obj = items.add<JsonObject>();
|
||||
obj["id"] = counter++;
|
||||
obj["cmd"] = ci.cmd;
|
||||
obj["value"] = ci.value;
|
||||
obj["name"] = (const char *)ci.name;
|
||||
}
|
||||
}
|
||||
|
||||
StateUpdateResult WebCommands::update(JsonObject root, WebCommands & webCommands) {
|
||||
Command::erase_device_commands(EMSdevice::DeviceType::COMMAND);
|
||||
webCommands.commandItems.clear();
|
||||
|
||||
auto items = root["commands"].as<JsonArray>();
|
||||
for (const JsonObject item : items) {
|
||||
auto ci = CommandItem();
|
||||
ci.cmd = item["cmd"].as<std::string>();
|
||||
ci.value = item["value"].as<std::string>();
|
||||
strlcpy(ci.name, item["name"].as<const char *>(), sizeof(ci.name));
|
||||
|
||||
webCommands.commandItems.push_back(ci);
|
||||
if (webCommands.commandItems.back().name[0] != '\0') {
|
||||
Command::add(
|
||||
EMSdevice::DeviceType::COMMAND,
|
||||
webCommands.commandItems.back().name,
|
||||
[](const char * value, const int8_t id) {
|
||||
return EMSESP::webCommandService.executeCommand(value);
|
||||
},
|
||||
FL_(command_cmd),
|
||||
CommandFlag::ADMIN_ONLY);
|
||||
}
|
||||
}
|
||||
return StateUpdateResult::CHANGED;
|
||||
}
|
||||
|
||||
// find a command item by name (case-insensitive)
|
||||
const CommandItem * WebCommandService::find(const char * name) {
|
||||
if (name == nullptr || name[0] == '\0') {
|
||||
return nullptr;
|
||||
}
|
||||
auto lower_name = Helpers::toLower(name);
|
||||
for (const CommandItem & ci : *commandItems_) {
|
||||
if (ci.name[0] != '\0' && Helpers::toLower(ci.name) == lower_name) {
|
||||
return &ci;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// execute a named command — looks up by name and runs it
|
||||
// called from console 'call commands <name>', API/MQTT, web UI
|
||||
bool WebCommandService::executeCommand(const char * name) {
|
||||
const CommandItem * ci = find(name);
|
||||
if (!ci) {
|
||||
EMSESP::logger().warning("Command '%s' not found", name ? name : "");
|
||||
return false;
|
||||
}
|
||||
return executeCommand(ci->name, ci->cmd, ci->value);
|
||||
}
|
||||
|
||||
// execute a command with explicit cmd and value strings
|
||||
// handles both HTTP URLs (JSON format) and internal API commands
|
||||
bool WebCommandService::executeCommand(const char * name, const std::string & command, const std::string & data) {
|
||||
std::string cmd = Helpers::toLower(command);
|
||||
|
||||
// handle HTTP commands (JSON with url/method/value)
|
||||
JsonDocument doc;
|
||||
if (deserializeJson(doc, cmd) == DeserializationError::Ok) {
|
||||
std::string url = doc["url"] | "";
|
||||
auto q = url.find_first_of('?');
|
||||
if (q != std::string::npos) {
|
||||
auto s = url.substr(q + 1);
|
||||
auto l = s.length();
|
||||
commands(s, false);
|
||||
url.replace(q + 1, l, s);
|
||||
}
|
||||
std::string value = doc["value"] | data;
|
||||
std::string method = doc["method"] | "GET";
|
||||
commands(value, false);
|
||||
auto lower_url = Helpers::toLower(url.c_str());
|
||||
if (lower_url.starts_with("http://") || lower_url.starts_with("https://")) {
|
||||
std::string result;
|
||||
int httpResult = http_request(url, method, value, doc["header"].as<JsonObjectConst>(), result);
|
||||
if (httpResult != 200) {
|
||||
EMSESP::logger().warning("Command '%s': URL command failed with http code %d", name, httpResult);
|
||||
return false;
|
||||
}
|
||||
#if defined(EMSESP_DEBUG)
|
||||
EMSESP::logger().debug("Command '%s': URL '%s' successful with http code %d", name, url.c_str(), httpResult);
|
||||
#endif
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// handle internal API commands
|
||||
doc.clear();
|
||||
JsonObject input = doc.to<JsonObject>();
|
||||
if (!data.empty()) {
|
||||
input["data"] = data;
|
||||
}
|
||||
|
||||
JsonDocument doc_output;
|
||||
JsonObject output = doc_output.to<JsonObject>();
|
||||
|
||||
char command_str[COMMAND_MAX_LENGTH];
|
||||
snprintf(command_str, sizeof(command_str), "/api/%s", cmd.c_str());
|
||||
|
||||
uint8_t return_code = Command::process(command_str, true, input, output);
|
||||
if (return_code == CommandRet::OK) {
|
||||
#if defined(EMSESP_DEBUG)
|
||||
EMSESP::logger().debug("Command '%s' (%s with data '%s') was successful", name, cmd.c_str(), data.c_str());
|
||||
#endif
|
||||
if (data.empty() && output.size()) {
|
||||
Mqtt::queue_publish("response", output);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
char error[100];
|
||||
if (output.size()) {
|
||||
snprintf(error, sizeof(error), "Command '%s': %s", name ? name : "", (const char *)output["message"]);
|
||||
} else {
|
||||
snprintf(error, sizeof(error), "Command '%s': %s failed with error %s", name, cmd.c_str(), Command::return_code_string(return_code));
|
||||
}
|
||||
EMSESP::logger().warning(error);
|
||||
return false;
|
||||
}
|
||||
|
||||
bool WebCommandService::get_value_info(JsonObject output, const char * cmd) {
|
||||
if (commandItems_->empty()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!strlen(cmd) || !strcmp(cmd, F_(values)) || !strcmp(cmd, F_(info))) {
|
||||
for (const CommandItem & ci : *commandItems_) {
|
||||
if (ci.name[0] != '\0') {
|
||||
output[(const char *)ci.name] = ci.cmd;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!strcmp(cmd, F_(entities))) {
|
||||
for (const CommandItem & ci : *commandItems_) {
|
||||
if (ci.name[0] != '\0') {
|
||||
get_value_json(output[ci.name].to<JsonObject>(), ci);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!strcmp(cmd, F_(metrics))) {
|
||||
std::string metrics = get_metrics_prometheus();
|
||||
if (!metrics.empty()) {
|
||||
output["api_data"] = metrics;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// look up specific command by name
|
||||
const char * attribute_s = Command::get_attribute(cmd);
|
||||
for (const CommandItem & ci : *commandItems_) {
|
||||
if (Helpers::toLower(ci.name) == cmd) {
|
||||
get_value_json(output, ci);
|
||||
return Command::get_attribute(output, cmd, attribute_s);
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
std::string WebCommandService::get_metrics_prometheus() {
|
||||
std::string result;
|
||||
result.reserve(commandItems_->size() * 100);
|
||||
for (const CommandItem & ci : *commandItems_) {
|
||||
if (ci.name[0] == '\0') {
|
||||
continue;
|
||||
}
|
||||
result += (std::string) "# HELP emsesp_cmd_" + ci.name + " " + ci.name + "\n";
|
||||
result += (std::string) "# TYPE emsesp_cmd_" + ci.name + " gauge\n";
|
||||
result += (std::string) "emsesp_cmd_" + ci.name + " 1\n";
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
void WebCommandService::get_value_json(JsonObject output, const CommandItem & ci) {
|
||||
output["name"] = (const char *)ci.name;
|
||||
output["fullname"] = (const char *)ci.name;
|
||||
output["type"] = "command";
|
||||
output["command"] = ci.cmd;
|
||||
output["cmd_data"] = ci.value;
|
||||
bool hasName = ci.name[0] != '\0';
|
||||
output["readable"] = hasName;
|
||||
output["writeable"] = hasName;
|
||||
output["visible"] = hasName;
|
||||
}
|
||||
|
||||
void WebCommandService::publish(const bool force) {
|
||||
if (!Mqtt::enabled() || commandItems_->empty()) {
|
||||
return;
|
||||
}
|
||||
if (force && !EMSESP::mqtt_.get_publish_onchange(0)) {
|
||||
return;
|
||||
}
|
||||
|
||||
JsonDocument doc(PSRAM_DOC);
|
||||
JsonObject output = doc.to<JsonObject>();
|
||||
for (const CommandItem & ci : *commandItems_) {
|
||||
if (ci.name[0] != '\0' && !output[ci.name].is<JsonVariantConst>()) {
|
||||
output[(const char *)ci.name] = ci.cmd;
|
||||
}
|
||||
}
|
||||
|
||||
if (!doc.isNull()) {
|
||||
char topic[Mqtt::MQTT_TOPIC_MAX_SIZE];
|
||||
snprintf(topic, sizeof(topic), "%s_data", F_(commands));
|
||||
Mqtt::queue_publish(topic, output);
|
||||
}
|
||||
}
|
||||
|
||||
uint8_t WebCommandService::count_entities() {
|
||||
return static_cast<uint8_t>(commandItems_ ? commandItems_->size() : 0);
|
||||
}
|
||||
|
||||
#if defined(EMSESP_TEST)
|
||||
void WebCommandService::load_test_data() {
|
||||
update([&](WebCommands & webCommands) {
|
||||
webCommands.commandItems.clear();
|
||||
|
||||
auto ci = CommandItem();
|
||||
ci.cmd = "system/fetch";
|
||||
ci.value = "10";
|
||||
strcpy(ci.name, "test_cmd1");
|
||||
webCommands.commandItems.push_back(ci);
|
||||
|
||||
ci = CommandItem();
|
||||
ci.cmd = "system/message";
|
||||
ci.value = "hello";
|
||||
strcpy(ci.name, "test_cmd2");
|
||||
webCommands.commandItems.push_back(ci);
|
||||
|
||||
return StateUpdateResult::CHANGED;
|
||||
});
|
||||
}
|
||||
#endif
|
||||
|
||||
} // namespace emsesp
|
||||
75
src/web/WebCommandService.h
Normal file
75
src/web/WebCommandService.h
Normal file
@@ -0,0 +1,75 @@
|
||||
/*
|
||||
* EMS-ESP - https://github.com/emsesp/EMS-ESP
|
||||
* Copyright 2020-2025 emsesp.org
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include <esp32-psram.h>
|
||||
|
||||
#ifndef WebCommandService_h
|
||||
#define WebCommandService_h
|
||||
|
||||
#define EMSESP_COMMAND_FILE "/config/emsespCommands.json"
|
||||
#define EMSESP_COMMAND_SERVICE_PATH "/rest/commands" // GET and POST
|
||||
|
||||
namespace emsesp {
|
||||
|
||||
class CommandItem {
|
||||
public:
|
||||
stringPSRAM cmd;
|
||||
stringPSRAM value;
|
||||
char name[20];
|
||||
};
|
||||
|
||||
class WebCommands {
|
||||
public:
|
||||
std::list<CommandItem, AllocatorPSRAM<CommandItem>> commandItems;
|
||||
|
||||
static void read(WebCommands & webCommands, JsonObject root);
|
||||
static StateUpdateResult update(JsonObject root, WebCommands & webCommands);
|
||||
};
|
||||
|
||||
class WebCommandService : public StatefulService<WebCommands> {
|
||||
public:
|
||||
WebCommandService(AsyncWebServer * server, FS * fs, SecurityManager * securityManager);
|
||||
|
||||
void begin();
|
||||
void publish(const bool force = false);
|
||||
bool get_value_info(JsonObject output, const char * cmd);
|
||||
void get_value_json(JsonObject output, const CommandItem & commandItem);
|
||||
|
||||
bool executeCommand(const char * name);
|
||||
bool executeCommand(const char * name, const std::string & cmd, const std::string & value);
|
||||
|
||||
const CommandItem * find(const char * name);
|
||||
|
||||
uint8_t count_entities();
|
||||
|
||||
std::string get_metrics_prometheus();
|
||||
|
||||
#if defined(EMSESP_TEST)
|
||||
void load_test_data();
|
||||
#endif
|
||||
|
||||
private:
|
||||
HttpEndpoint<WebCommands> _httpEndpoint;
|
||||
FSPersistence<WebCommands> _fsPersistence;
|
||||
|
||||
std::list<CommandItem, AllocatorPSRAM<CommandItem>> * commandItems_;
|
||||
};
|
||||
|
||||
} // namespace emsesp
|
||||
|
||||
#endif
|
||||
@@ -250,6 +250,9 @@ void WebDataService::write_device_value(AsyncWebServerRequest * request, JsonVar
|
||||
case EMSdevice::DeviceTypeUniqueID::SCHEDULER_UID:
|
||||
device_type = EMSdevice::DeviceType::SCHEDULER;
|
||||
break;
|
||||
case EMSdevice::DeviceTypeUniqueID::COMMAND_UID:
|
||||
device_type = EMSdevice::DeviceType::COMMAND;
|
||||
break;
|
||||
case EMSdevice::DeviceTypeUniqueID::TEMPERATURESENSOR_UID:
|
||||
device_type = EMSdevice::DeviceType::TEMPERATURESENSOR;
|
||||
break;
|
||||
@@ -478,11 +481,11 @@ void WebDataService::dashboard_data(AsyncWebServerRequest * request) {
|
||||
}
|
||||
}
|
||||
|
||||
// show scheduler items
|
||||
// show scheduler items (active state toggles)
|
||||
if (EMSESP::webSchedulerService.count_entities()) {
|
||||
JsonObject obj = nodes.add<JsonObject>();
|
||||
obj["id"] = EMSdevice::DeviceTypeUniqueID::SCHEDULER_UID; // it's unique id
|
||||
obj["t"] = EMSdevice::DeviceType::SCHEDULER; // device type number
|
||||
obj["id"] = EMSdevice::DeviceTypeUniqueID::SCHEDULER_UID;
|
||||
obj["t"] = EMSdevice::DeviceType::SCHEDULER;
|
||||
JsonArray nodes = obj["nodes"].to<JsonArray>();
|
||||
uint8_t count = 0;
|
||||
|
||||
@@ -495,14 +498,31 @@ void WebDataService::dashboard_data(AsyncWebServerRequest * request) {
|
||||
dv["id"] = std::string("00") + scheduleItem.name;
|
||||
dv["c"] = scheduleItem.name;
|
||||
|
||||
// for immediate schedules, we don't show the active/inactive state or on/off options
|
||||
if (scheduleItem.flags != SCHEDULEFLAG_SCHEDULE_IMMEDIATE) {
|
||||
char s[12];
|
||||
dv["v"] = Helpers::render_boolean(s, scheduleItem.active, true);
|
||||
JsonArray l = dv["l"].to<JsonArray>();
|
||||
l.add(Helpers::render_boolean(s, false, true)); // False option
|
||||
l.add(Helpers::render_boolean(s, true, true)); // True option
|
||||
}
|
||||
char s[12];
|
||||
dv["v"] = Helpers::render_boolean(s, scheduleItem.active, true);
|
||||
JsonArray l = dv["l"].to<JsonArray>();
|
||||
l.add(Helpers::render_boolean(s, false, true));
|
||||
l.add(Helpers::render_boolean(s, true, true));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// show command items (executable from dashboard)
|
||||
if (EMSESP::webCommandService.count_entities()) {
|
||||
JsonObject obj = nodes.add<JsonObject>();
|
||||
obj["id"] = EMSdevice::DeviceTypeUniqueID::COMMAND_UID;
|
||||
obj["t"] = EMSdevice::DeviceType::COMMAND;
|
||||
JsonArray nodes = obj["nodes"].to<JsonArray>();
|
||||
uint8_t count = 0;
|
||||
|
||||
EMSESP::webCommandService.read([&](const WebCommands & webCommands) {
|
||||
for (const CommandItem & ci : webCommands.commandItems) {
|
||||
JsonObject node = nodes.add<JsonObject>();
|
||||
node["id"] = (EMSdevice::DeviceTypeUniqueID::COMMAND_UID * 100) + count++;
|
||||
|
||||
JsonObject dv = node["dv"].to<JsonObject>();
|
||||
dv["id"] = std::string("00") + ci.name;
|
||||
dv["c"] = ci.name;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -57,21 +57,18 @@ void WebScheduler::read(WebScheduler & webScheduler, JsonObject root) {
|
||||
JsonArray schedule = root["schedule"].to<JsonArray>();
|
||||
uint8_t counter = 1;
|
||||
for (const ScheduleItem & scheduleItem : webScheduler.scheduleItems) {
|
||||
JsonObject si = schedule.add<JsonObject>();
|
||||
si["id"] = counter++; // id is only used to render the table and must be unique. 0 is for Dashboard
|
||||
si["flags"] = scheduleItem.flags;
|
||||
si["active"] = scheduleItem.flags != SCHEDULEFLAG_SCHEDULE_IMMEDIATE ? scheduleItem.active : false;
|
||||
si["time"] = scheduleItem.flags != SCHEDULEFLAG_SCHEDULE_IMMEDIATE ? scheduleItem.time : "";
|
||||
si["cmd"] = scheduleItem.cmd;
|
||||
si["value"] = scheduleItem.value;
|
||||
si["name"] = (const char *)scheduleItem.name;
|
||||
JsonObject si = schedule.add<JsonObject>();
|
||||
si["id"] = counter++;
|
||||
si["flags"] = scheduleItem.flags;
|
||||
si["active"] = scheduleItem.active;
|
||||
si["time"] = scheduleItem.time;
|
||||
si["cmd_name"] = scheduleItem.cmd_name;
|
||||
si["name"] = (const char *)scheduleItem.name;
|
||||
}
|
||||
}
|
||||
|
||||
// call on initialization and also when the Schedule web page is saved
|
||||
// this loads the data into the internal class
|
||||
StateUpdateResult WebScheduler::update(JsonObject root, WebScheduler & webScheduler) {
|
||||
// reset the list
|
||||
Command::erase_device_commands(EMSdevice::DeviceType::SCHEDULER);
|
||||
for (ScheduleItem & scheduleItem : webScheduler.scheduleItems) {
|
||||
char key[sizeof(scheduleItem.name) + 2];
|
||||
@@ -83,28 +80,23 @@ StateUpdateResult WebScheduler::update(JsonObject root, WebScheduler & webSchedu
|
||||
webScheduler.scheduleItems.clear();
|
||||
EMSESP::webSchedulerService.ha_reset();
|
||||
|
||||
// build up the list of schedule items
|
||||
auto scheduleItems = root["schedule"].as<JsonArray>();
|
||||
for (const JsonObject schedule : scheduleItems) {
|
||||
// create each schedule item, overwriting any previous settings
|
||||
// ignore the id (as this is only used in the web for table rendering)
|
||||
auto si = ScheduleItem();
|
||||
si.active = schedule["active"];
|
||||
si.flags = schedule["flags"];
|
||||
si.time = si.flags == SCHEDULEFLAG_SCHEDULE_IMMEDIATE ? "" : schedule["time"].as<std::string>();
|
||||
si.cmd = schedule["cmd"].as<std::string>();
|
||||
si.value = schedule["value"].as<std::string>();
|
||||
auto si = ScheduleItem();
|
||||
si.active = schedule["active"];
|
||||
si.flags = schedule["flags"];
|
||||
si.time = schedule["time"].as<std::string>();
|
||||
si.cmd_name = schedule["cmd_name"].as<std::string>();
|
||||
strlcpy(si.name, schedule["name"].as<const char *>(), sizeof(si.name));
|
||||
|
||||
// calculated elapsed minutes
|
||||
si.elapsed_min = Helpers::string2minutes(si.time.c_str());
|
||||
si.retry_cnt = 0xFF; // no startup retries
|
||||
si.retry_cnt = 0xFF;
|
||||
|
||||
webScheduler.scheduleItems.push_back(si); // add to list
|
||||
webScheduler.scheduleItems.push_back(si);
|
||||
if (webScheduler.scheduleItems.back().name[0] != '\0') {
|
||||
char key[sizeof(webScheduler.scheduleItems.back().name) + 2];
|
||||
snprintf(key, sizeof(key), "s:%s", webScheduler.scheduleItems.back().name);
|
||||
if (EMSESP::nvs_.isKey(key) && webScheduler.scheduleItems.back().flags != SCHEDULEFLAG_SCHEDULE_IMMEDIATE) {
|
||||
if (EMSESP::nvs_.isKey(key)) {
|
||||
webScheduler.scheduleItems.back().active = EMSESP::nvs_.getBool(key);
|
||||
}
|
||||
Command::add(
|
||||
@@ -140,12 +132,9 @@ bool WebSchedulerService::command_setvalue(const char * value, const int8_t id,
|
||||
publish();
|
||||
}
|
||||
|
||||
// save new state to nvs #2946
|
||||
if (scheduleItem.flags != SCHEDULEFLAG_SCHEDULE_IMMEDIATE) {
|
||||
char key[sizeof(scheduleItem.name) + 2];
|
||||
snprintf(key, sizeof(key), "s:%s", scheduleItem.name);
|
||||
EMSESP::nvs_.putBool(key, scheduleItem.active);
|
||||
}
|
||||
char key[sizeof(scheduleItem.name) + 2];
|
||||
snprintf(key, sizeof(key), "s:%s", scheduleItem.name);
|
||||
EMSESP::nvs_.putBool(key, scheduleItem.active);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -225,11 +214,10 @@ void WebSchedulerService::get_value_json(JsonObject output, const ScheduleItem &
|
||||
output["onchange"] = scheduleItem.time;
|
||||
} else if (scheduleItem.flags == SCHEDULEFLAG_SCHEDULE_TIMER) {
|
||||
output["timer"] = scheduleItem.time;
|
||||
} else if (scheduleItem.flags != SCHEDULEFLAG_SCHEDULE_IMMEDIATE) {
|
||||
} else {
|
||||
output["time"] = scheduleItem.time;
|
||||
}
|
||||
output["command"] = scheduleItem.cmd;
|
||||
output["cmd_data"] = scheduleItem.value;
|
||||
output["cmd_name"] = scheduleItem.cmd_name;
|
||||
bool hasName = scheduleItem.name[0] != '\0';
|
||||
output["readable"] = hasName;
|
||||
output["writeable"] = hasName;
|
||||
@@ -339,80 +327,14 @@ uint8_t WebSchedulerService::count_entities() {
|
||||
return static_cast<uint8_t>(scheduleItems_ ? scheduleItems_->size() : 0);
|
||||
}
|
||||
|
||||
// execute scheduled command
|
||||
// return true if successful, false if not
|
||||
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://<tasmotaIP>/cm?cmnd=power%20ON
|
||||
// shelly(get): http://<shellyIP>/relais/0?turn=on
|
||||
// parse json
|
||||
JsonDocument doc;
|
||||
if (deserializeJson(doc, cmd) == DeserializationError::Ok) {
|
||||
std::string url = doc["url"] | "";
|
||||
// for a GET with parameters replace commands with values
|
||||
// don't search the complete url, it may contain a devicename in path
|
||||
auto q = url.find_first_of('?');
|
||||
if (q != std::string::npos) {
|
||||
auto s = url.substr(q + 1); // copy only parameters
|
||||
auto l = s.length();
|
||||
commands(s, false);
|
||||
url.replace(q + 1, l, s);
|
||||
}
|
||||
std::string value = doc["value"] | data; // extract value if its in the command, or take the data
|
||||
std::string method = doc["method"] | "GET"; // default GET
|
||||
commands(value, false);
|
||||
auto lower_url = Helpers::toLower(url.c_str());
|
||||
if (lower_url.starts_with("http://") || lower_url.starts_with("https://")) {
|
||||
std::string result;
|
||||
int httpResult = http_request(url, method, value, doc["header"].as<JsonObjectConst>(), result);
|
||||
if (httpResult != 200) {
|
||||
EMSESP::logger().warning("Schedule '%s': URL command failed with http code %d", name, httpResult);
|
||||
return false;
|
||||
}
|
||||
#if defined(EMSESP_DEBUG)
|
||||
EMSESP::logger().debug("Schedule %s: URL '%s' command successful with http code %d", name, url.c_str(), httpResult);
|
||||
#endif
|
||||
return true;
|
||||
}
|
||||
// we can add other json tests here
|
||||
// execute the command associated with a schedule item
|
||||
// looks up the named command in WebCommandService and runs it
|
||||
bool WebSchedulerService::runScheduleCommand(const ScheduleItem & si) {
|
||||
if (si.cmd_name.empty()) {
|
||||
EMSESP::logger().warning("Schedule '%s': no command assigned", si.name);
|
||||
return false;
|
||||
}
|
||||
|
||||
doc.clear();
|
||||
JsonObject input = doc.to<JsonObject>();
|
||||
if (!data.empty()) { // empty data queries a value
|
||||
input["data"] = data;
|
||||
}
|
||||
|
||||
JsonDocument doc_output; // only for commands without output
|
||||
JsonObject output = doc_output.to<JsonObject>();
|
||||
|
||||
// prefix "api/" to command string
|
||||
char command_str[COMMAND_MAX_LENGTH];
|
||||
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.c_str(), data.c_str());
|
||||
#endif
|
||||
if (data.empty() && output.size()) {
|
||||
Mqtt::queue_publish("response", output);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
char error[100];
|
||||
if (output.size()) {
|
||||
// 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.c_str(), Command::return_code_string(return_code));
|
||||
}
|
||||
|
||||
EMSESP::logger().warning(error);
|
||||
return false;
|
||||
return EMSESP::webCommandService.executeCommand(si.cmd_name.c_str());
|
||||
}
|
||||
|
||||
// called from emsesp.cpp on every entity-change
|
||||
@@ -427,31 +349,16 @@ bool WebSchedulerService::onChange(const char * cmd) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// system/message evaluates its own argument later (deferred via raw_value, computed in loop()),
|
||||
// so pre-computing it here would make any {url} or expression inside it run twice. Pass
|
||||
// system/message its value raw; compute() everything else as before.
|
||||
// templated because ScheduleItem's strings use a PSRAM allocator, not std::string.
|
||||
template <typename C, typename V>
|
||||
static std::string compute_cmd_value(const C & cmd, const V & value) {
|
||||
if (Helpers::toLower(cmd.c_str()) == "system/message") {
|
||||
return std::string(value.c_str());
|
||||
}
|
||||
return compute(value.c_str());
|
||||
}
|
||||
|
||||
// handle condition schedules, parse string stored in schedule.time field
|
||||
void WebSchedulerService::condition() {
|
||||
for (ScheduleItem & scheduleItem : *scheduleItems_) {
|
||||
if (scheduleItem.active && scheduleItem.flags == SCHEDULEFLAG_SCHEDULE_CONDITION) {
|
||||
auto match = compute(scheduleItem.time.c_str());
|
||||
#ifdef EMESESP_DEBUG
|
||||
// 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, scheduleItem.cmd.c_str(), compute_cmd_value(scheduleItem.cmd, scheduleItem.value)) ? 1 : 0xFF;
|
||||
scheduleItem.retry_cnt = runScheduleCommand(scheduleItem) ? 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
|
||||
} else if (match.length() != 1) {
|
||||
#if defined(EMSESP_DEBUG)
|
||||
EMSESP::logger().debug("condition result: %s", match.c_str());
|
||||
#endif
|
||||
@@ -462,17 +369,15 @@ void WebSchedulerService::condition() {
|
||||
|
||||
// process any scheduled jobs
|
||||
void WebSchedulerService::loop() {
|
||||
// initialize static value on startup
|
||||
static int8_t last_tm_min = -2; // invalid value also used for startup commands
|
||||
static uint32_t last_uptime_min = 0;
|
||||
static uint32_t last_uptime_sec = 0;
|
||||
|
||||
if (!raw_value.empty()) { // process a value from system/message command
|
||||
if (!raw_value.empty()) {
|
||||
computed_value = compute(raw_value);
|
||||
raw_value.clear();
|
||||
}
|
||||
|
||||
// get list of scheduler events and exit if it's empty
|
||||
if (scheduleItems_->empty()) {
|
||||
return;
|
||||
}
|
||||
@@ -480,21 +385,10 @@ void WebSchedulerService::loop() {
|
||||
// check if we have onChange events
|
||||
while (!cmd_changed_.empty()) {
|
||||
ScheduleItem si = *cmd_changed_.front();
|
||||
command(si.name, si.cmd.c_str(), compute_cmd_value(si.cmd, si.value));
|
||||
runScheduleCommand(si);
|
||||
cmd_changed_.pop_front();
|
||||
}
|
||||
|
||||
for (ScheduleItem & scheduleItem : *scheduleItems_) {
|
||||
if (scheduleItem.active && scheduleItem.flags == SCHEDULEFLAG_SCHEDULE_IMMEDIATE) {
|
||||
command(scheduleItem.name, scheduleItem.cmd.c_str(), compute_cmd_value(scheduleItem.cmd, scheduleItem.value));
|
||||
scheduleItem.active = false;
|
||||
publish_single(scheduleItem.name, false);
|
||||
if (EMSESP::mqtt_.get_publish_onchange(0)) {
|
||||
publish();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// check conditions every 10 seconds, start after one minute
|
||||
uint32_t uptime_sec = uuid::get_uptime_sec() / 10;
|
||||
if (last_uptime_sec != uptime_sec && uptime_sec > 5) {
|
||||
@@ -506,64 +400,49 @@ 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, scheduleItem.cmd.c_str(), compute_cmd_value(scheduleItem.cmd, scheduleItem.value)) ? 0xFF : 0;
|
||||
scheduleItem.retry_cnt = runScheduleCommand(scheduleItem) ? 0xFF : 0;
|
||||
}
|
||||
}
|
||||
last_tm_min = -1; // startup done, now use for RTC
|
||||
last_tm_min = -1;
|
||||
}
|
||||
|
||||
// check timer every minute, sync to EMS-ESP clock
|
||||
uint32_t uptime_min = uuid::get_uptime_sec() / 60;
|
||||
if (last_uptime_min != uptime_min) {
|
||||
for (ScheduleItem & scheduleItem : *scheduleItems_) {
|
||||
// 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, scheduleItem.cmd.c_str(), scheduleItem.value.c_str()) ? 0xFF : scheduleItem.retry_cnt + 1;
|
||||
scheduleItem.retry_cnt = runScheduleCommand(scheduleItem) ? 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, scheduleItem.cmd.c_str(), compute_cmd_value(scheduleItem.cmd, scheduleItem.value));
|
||||
runScheduleCommand(scheduleItem);
|
||||
}
|
||||
}
|
||||
last_uptime_min = uptime_min;
|
||||
}
|
||||
|
||||
// check calender, sync to RTC, only execute if year is valid
|
||||
// check calendar, sync to RTC, only execute if year is valid
|
||||
time_t now = time(nullptr);
|
||||
tm * tm = localtime(&now);
|
||||
if (tm->tm_min != last_tm_min && tm->tm_year > 120) {
|
||||
// find the real dow and minute from RTC
|
||||
uint8_t real_dow = 1 << tm->tm_wday; // 1 is Sunday
|
||||
uint8_t real_dow = 1 << tm->tm_wday;
|
||||
uint16_t real_min = tm->tm_hour * 60 + tm->tm_min;
|
||||
|
||||
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, scheduleItem.cmd.c_str(), compute_cmd_value(scheduleItem.cmd, scheduleItem.value));
|
||||
runScheduleCommand(scheduleItem);
|
||||
}
|
||||
}
|
||||
last_tm_min = tm->tm_min;
|
||||
}
|
||||
}
|
||||
|
||||
// execute a schedule item immediately
|
||||
bool WebSchedulerService::executeSchedule(const char * name) {
|
||||
for (ScheduleItem & scheduleItem : *scheduleItems_) {
|
||||
if (scheduleItem.flags == SCHEDULEFLAG_SCHEDULE_IMMEDIATE && strcmp(scheduleItem.name, name) == 0) {
|
||||
EMSESP::logger().info("Executing schedule '%s'", name);
|
||||
return command(scheduleItem.name, scheduleItem.cmd.c_str(), compute_cmd_value(scheduleItem.cmd, scheduleItem.value));
|
||||
}
|
||||
}
|
||||
EMSESP::logger().warning("Schedule '%s' not found", name);
|
||||
return false; // not found
|
||||
}
|
||||
|
||||
// process schedules async
|
||||
void WebSchedulerService::scheduler_task(void * pvParameters) {
|
||||
while (1) {
|
||||
delay(10); // no need to hurry
|
||||
delay(10);
|
||||
if (EMSESP::system_.systemStatus() == SYSTEM_STATUS::SYSTEM_STATUS_NORMAL) {
|
||||
EMSESP::webSchedulerService.loop();
|
||||
}
|
||||
@@ -573,39 +452,34 @@ void WebSchedulerService::scheduler_task(void * pvParameters) {
|
||||
#endif
|
||||
}
|
||||
|
||||
// hard coded tests
|
||||
#if defined(EMSESP_TEST)
|
||||
void WebSchedulerService::load_test_data() {
|
||||
update([&](WebScheduler & webScheduler) {
|
||||
webScheduler.scheduleItems.clear(); // delete all existing schedules
|
||||
webScheduler.scheduleItems.clear();
|
||||
|
||||
// test 1
|
||||
auto si = ScheduleItem();
|
||||
si.active = true;
|
||||
si.flags = 1; // day schedule
|
||||
si.time = "12:00";
|
||||
si.cmd = "system/fetch";
|
||||
si.value = "10";
|
||||
auto si = ScheduleItem();
|
||||
si.active = true;
|
||||
si.flags = 1; // day schedule
|
||||
si.time = "12:00";
|
||||
si.cmd_name = "test_cmd1";
|
||||
strcpy(si.name, "test_scheduler1");
|
||||
si.elapsed_min = 0;
|
||||
si.retry_cnt = 0xFF; // no startup retries
|
||||
si.retry_cnt = 0xFF;
|
||||
|
||||
webScheduler.scheduleItems.push_back(si);
|
||||
|
||||
// test 2
|
||||
si = ScheduleItem();
|
||||
si.active = false;
|
||||
si.flags = SCHEDULEFLAG_SCHEDULE_IMMEDIATE; // immediate
|
||||
si.time = "13:00";
|
||||
si.cmd = "system/message";
|
||||
si.value = "20";
|
||||
strcpy(si.name, "test_scheduler2"); // to make sure its excluded from Dashboard
|
||||
si.elapsed_min = 0;
|
||||
si.retry_cnt = 0xFF; // no startup retries
|
||||
si = ScheduleItem();
|
||||
si.active = true;
|
||||
si.flags = SCHEDULEFLAG_SCHEDULE_TIMER;
|
||||
si.time = "01:00";
|
||||
si.cmd_name = "test_cmd2";
|
||||
strcpy(si.name, "test_scheduler2");
|
||||
si.elapsed_min = 60;
|
||||
si.retry_cnt = 0xFF;
|
||||
|
||||
webScheduler.scheduleItems.push_back(si);
|
||||
|
||||
return StateUpdateResult::CHANGED; // persist the changes
|
||||
return StateUpdateResult::CHANGED;
|
||||
});
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -41,11 +41,9 @@
|
||||
// 128 (0x80) is timer
|
||||
// 129 (0x81) is on change
|
||||
// 130 (0x82) is on condition
|
||||
// 132 (0x84) is immediate
|
||||
#define SCHEDULEFLAG_SCHEDULE_TIMER 0x80 // 7th bit for Timer
|
||||
#define SCHEDULEFLAG_SCHEDULE_ONCHANGE 0x81 // 7th+1st bit for OnChange
|
||||
#define SCHEDULEFLAG_SCHEDULE_CONDITION 0x82 // 7th+2nd bit for Condition
|
||||
#define SCHEDULEFLAG_SCHEDULE_IMMEDIATE 0x84 // 7th+3rd bit for Immediate
|
||||
|
||||
#define MAX_STARTUP_RETRIES 3 // retry the start-up commands x times
|
||||
|
||||
@@ -57,8 +55,7 @@ class ScheduleItem {
|
||||
uint8_t flags; // bit flags, see SCHEDULEFLAG_* defines
|
||||
uint16_t elapsed_min; // total mins from 00:00
|
||||
stringPSRAM time; // HH:MM
|
||||
stringPSRAM cmd;
|
||||
stringPSRAM value;
|
||||
stringPSRAM cmd_name; // references a named command from WebCommandService
|
||||
char name[20];
|
||||
uint8_t retry_cnt;
|
||||
};
|
||||
@@ -88,8 +85,6 @@ class WebSchedulerService : public StatefulService<WebScheduler> {
|
||||
uint8_t count_entities();
|
||||
bool onChange(const char * cmd);
|
||||
|
||||
bool executeSchedule(const char * name);
|
||||
|
||||
std::string get_metrics_prometheus();
|
||||
|
||||
std::string raw_value;
|
||||
@@ -105,7 +100,7 @@ class WebSchedulerService : public StatefulService<WebScheduler> {
|
||||
#endif
|
||||
static void scheduler_task(void * pvParameters);
|
||||
|
||||
bool command(const char * name, const std::string & cmd, const std::string & data);
|
||||
bool runScheduleCommand(const ScheduleItem & si);
|
||||
void condition();
|
||||
|
||||
HttpEndpoint<WebScheduler> _httpEndpoint;
|
||||
|
||||
@@ -232,8 +232,8 @@ void WebStatusService::action(AsyncWebServerRequest * request, JsonVariant json)
|
||||
EMSESP::mqtt_.reset_mqtt();
|
||||
} else if (action == "upgradeImportantMessages") {
|
||||
root["upgradeImportantMessageType"] = upgradeImportantMessages(param);
|
||||
} else if (action == "executeSchedule") {
|
||||
ok = EMSESP::webSchedulerService.executeSchedule(param.c_str());
|
||||
} else if (action == "executeCommand") {
|
||||
ok = EMSESP::webCommandService.executeCommand(param.c_str());
|
||||
}
|
||||
|
||||
#if defined(EMSESP_STANDALONE) && !defined(EMSESP_UNITY)
|
||||
|
||||
Reference in New Issue
Block a user