From fc7582492186bcef91e4140657e6df375119295d Mon Sep 17 00:00:00 2001 From: MichaelDvP Date: Sat, 15 Jun 2024 20:00:08 +0200 Subject: [PATCH 1/7] scheduler: onChange and conditions --- interface/src/project/Scheduler.tsx | 49 ++- interface/src/project/SchedulerDialog.tsx | 132 +++++-- interface/src/project/types.ts | 4 +- src/analogsensor.cpp | 3 + src/emsdevice.cpp | 43 ++- src/temperaturesensor.cpp | 3 + src/web/WebCustomEntityService.cpp | 11 +- src/web/WebSchedulerService.cpp | 53 ++- src/web/WebSchedulerService.h | 8 +- src/web/shuntingYard.hpp | 404 ++++++++++++++++++++++ 10 files changed, 648 insertions(+), 62 deletions(-) create mode 100644 src/web/shuntingYard.hpp diff --git a/interface/src/project/Scheduler.tsx b/interface/src/project/Scheduler.tsx index 7389c0e5c..1262f0030 100644 --- a/interface/src/project/Scheduler.tsx +++ b/interface/src/project/Scheduler.tsx @@ -87,7 +87,7 @@ const Scheduler: FC = () => { const schedule_theme = useTheme({ Table: ` - --data-table-library_grid-template-columns: 36px 324px 50px 192px repeat(1, minmax(100px, 1fr)) 160px; + --data-table-library_grid-template-columns: 36px 210px 100px 192px repeat(1, minmax(100px, 1fr)) 160px; `, BaseRow: ` font-size: 14px; @@ -198,7 +198,7 @@ const Scheduler: FC = () => { active: false, deleted: false, flags: 0, - time: '12:00', + time: '', cmd: '', value: '', name: '' @@ -216,11 +216,21 @@ const Scheduler: FC = () => { = ScheduleFlag.SCHEDULE_TIMER && si.flags !== flag + ? 'gray' + : (si.flags & flag) === flag + ? 'primary' + : 'grey' + } > {flag === ScheduleFlag.SCHEDULE_TIMER ? LL.TIMER(0) - : dow[Math.log(flag) / Math.log(2)]} + : flag === ScheduleFlag.SCHEDULE_ONCHANGE + ? 'OnChange' + : flag === ScheduleFlag.SCHEDULE_CONDITION + ? 'Condition' + : dow[Math.log(flag) / Math.log(2)]} @@ -245,7 +255,7 @@ const Scheduler: FC = () => { {LL.SCHEDULE(0)} - {LL.TIME(0)} + {LL.TIME(0)}/Cond. {LL.COMMAND(0)} {LL.VALUE(0)} {LL.NAME(0)} @@ -268,16 +278,25 @@ const Scheduler: FC = () => { )} - + - {dayBox(si, ScheduleFlag.SCHEDULE_MON)} - {dayBox(si, ScheduleFlag.SCHEDULE_TUE)} - {dayBox(si, ScheduleFlag.SCHEDULE_WED)} - {dayBox(si, ScheduleFlag.SCHEDULE_THU)} - {dayBox(si, ScheduleFlag.SCHEDULE_FRI)} - {dayBox(si, ScheduleFlag.SCHEDULE_SAT)} - {dayBox(si, ScheduleFlag.SCHEDULE_SUN)} - {dayBox(si, ScheduleFlag.SCHEDULE_TIMER)} + {si.flags < ScheduleFlag.SCHEDULE_TIMER ? ( + <> + {dayBox(si, ScheduleFlag.SCHEDULE_MON)} + {dayBox(si, ScheduleFlag.SCHEDULE_TUE)} + {dayBox(si, ScheduleFlag.SCHEDULE_WED)} + {dayBox(si, ScheduleFlag.SCHEDULE_THU)} + {dayBox(si, ScheduleFlag.SCHEDULE_FRI)} + {dayBox(si, ScheduleFlag.SCHEDULE_SAT)} + {dayBox(si, ScheduleFlag.SCHEDULE_SUN)} + + ) : ( + <> + {dayBox(si, ScheduleFlag.SCHEDULE_TIMER)} + {dayBox(si, ScheduleFlag.SCHEDULE_ONCHANGE)} + {dayBox(si, ScheduleFlag.SCHEDULE_CONDITION)} + + )} {si.time} @@ -341,7 +360,7 @@ const Scheduler: FC = () => { )} + + {isOnChange ? ( + + ) : ( + + )} + + + {isCondition ? ( + + ) : ( + + )} + - - {isTimer && ( - - {LL.SCHEDULER_HELP_2()} - + {isCondition || isOnChange ? ( + + ) : ( + <> + + {isTimer && ( + + {LL.SCHEDULER_HELP_2()} + + )} + )} = DeviceValueTAG::TAG_HC1) { + snprintf(cmd, sizeof(cmd), "%s/%s/%s", device_type_2_device_name(device_type_), tag_to_mqtt(dv.tag), dv.short_name); + } else { + snprintf(cmd, sizeof(cmd), "%s/%s", device_type_2_device_name(device_type_), (dv.short_name)); + } + EMSESP::webSchedulerService.onChange(cmd); } } } diff --git a/src/temperaturesensor.cpp b/src/temperaturesensor.cpp index 56c61631a..b9844f676 100644 --- a/src/temperaturesensor.cpp +++ b/src/temperaturesensor.cpp @@ -441,6 +441,9 @@ void TemperatureSensor::publish_sensor(const Sensor & sensor) { char payload[10]; Mqtt::queue_publish(topic, Helpers::render_value(payload, sensor.temperature_c, 10, EMSESP::system_.fahrenheit() ? 2 : 0)); } + char cmd[COMMAND_MAX_LENGTH]; + snprintf(cmd, sizeof(cmd), "%s/%s", F_(temperaturesensor), sensor.name().c_str()); + EMSESP::webSchedulerService.onChange(cmd); } // send empty config topic to remove the entry from HA diff --git a/src/web/WebCustomEntityService.cpp b/src/web/WebCustomEntityService.cpp index 93cf39231..be9fb8de0 100644 --- a/src/web/WebCustomEntityService.cpp +++ b/src/web/WebCustomEntityService.cpp @@ -175,6 +175,9 @@ bool WebCustomEntityService::command_setvalue(const char * value, const std::str if (EMSESP::mqtt_.get_publish_onchange(0)) { publish(); } + char cmd[COMMAND_MAX_LENGTH]; + snprintf(cmd, sizeof(cmd_function_p), "custom/%s", entityItem.name.c_str()); + EMSESP::webSchedulerService.onChange(cmd); return true; } } @@ -591,7 +594,7 @@ void WebCustomEntityService::fetch() { bool WebCustomEntityService::get_value(std::shared_ptr telegram) { bool has_change = false; EMSESP::webCustomEntityService.read([&](WebCustomEntity & webEntity) { customEntityItems = &webEntity.customEntityItems; }); - // read-length of BOOL, INT, UINT, SHORT, USHORT, ULONG, TIME + // read-length of BOOL, INT8, UINT8, INT16, UINT16, UINT24, TIME, UINT32 const uint8_t len[] = {1, 1, 1, 2, 2, 3, 3, 4}; for (auto & entity : *customEntityItems) { if (entity.value_type == DeviceValueType::STRING && telegram->type_id == entity.type_id && telegram->src == entity.device_id @@ -604,6 +607,9 @@ bool WebCustomEntityService::get_value(std::shared_ptr telegram) } else if (EMSESP::mqtt_.get_publish_onchange(0)) { has_change = true; } + char cmd[COMMAND_MAX_LENGTH]; + snprintf(cmd, sizeof(cmd_function_p), "custom/%s", entity.name.c_str()); + EMSESP::webSchedulerService.onChange(cmd); } } else if (entity.value_type != DeviceValueType::STRING && telegram->type_id == entity.type_id && telegram->src == entity.device_id && telegram->offset <= entity.offset && (telegram->offset + telegram->message_length) >= (entity.offset + len[entity.value_type])) { @@ -618,6 +624,9 @@ bool WebCustomEntityService::get_value(std::shared_ptr telegram) } else if (EMSESP::mqtt_.get_publish_onchange(0)) { has_change = true; } + char cmd[COMMAND_MAX_LENGTH]; + snprintf(cmd, sizeof(cmd_function_p), "%s/%s", "custom", entity.name.c_str()); + EMSESP::webSchedulerService.onChange(cmd); } // EMSESP::logger().debug("custom entity %s received with value %d", entity.name.c_str(), (int)entity.val); } diff --git a/src/web/WebSchedulerService.cpp b/src/web/WebSchedulerService.cpp index 12ec11146..e25868971 100644 --- a/src/web/WebSchedulerService.cpp +++ b/src/web/WebSchedulerService.cpp @@ -367,26 +367,64 @@ bool WebSchedulerService::command(const char * cmd, const char * data) { return false; } +#include "shuntingYard.hpp" + +bool WebSchedulerService::onChange(const char * cmd) { + for (const ScheduleItem & scheduleItem : *scheduleItems_) { + if (scheduleItem.active && scheduleItem.flags == SCHEDULEFLAG_SCHEDULE_ONCHANGE && Helpers::toLower(scheduleItem.time) == Helpers::toLower(cmd)) { + // emsesp::EMSESP::logger().debug(scheduleItem.cmd.c_str()); + return command(scheduleItem.cmd.c_str(), compute(scheduleItem.value.c_str()).c_str()); + } + } + return false; +} + +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::EMSESP::logger().debug("condition match: %s", match.c_str()); +#endif + if (!match.empty() && match.c_str()[0] == '1') { + if (scheduleItem.retry_cnt == 0xFF) { // default unswitched + scheduleItem.retry_cnt = command(scheduleItem.cmd.c_str(), compute(scheduleItem.value.c_str()).c_str()) ? 1 : 0xFF; + } + } else if (scheduleItem.retry_cnt == 1) { + scheduleItem.retry_cnt = 0xFF; + } + } + } +} + // 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 = -1; // invalid value also used for startup commands + 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; // get list of scheduler events and exit if it's empty if (scheduleItems_->size() == 0) { return; } + // check conditions every 10 seconds + uint32_t uptime_sec = uuid::get_uptime_sec() / 10; + if (last_uptime_sec != uptime_sec) { + condition(); + last_uptime_sec = uptime_sec; + } + // check startup commands - if (last_tm_min == -1) { + 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.cmd.c_str(), scheduleItem.value.c_str()) ? 0xFF : 0; + scheduleItem.retry_cnt = command(scheduleItem.cmd.c_str(), compute(scheduleItem.value.c_str()).c_str()) ? 0xFF : 0; } } - last_tm_min = 0; // startup done, now use for RTC + last_tm_min = -1; // startup done, now use for RTC } // check timer every minute, sync to EMS-ESP clock @@ -401,7 +439,7 @@ void WebSchedulerService::loop() { // scheduled timer commands if (scheduleItem.active && scheduleItem.flags == SCHEDULEFLAG_SCHEDULE_TIMER && scheduleItem.elapsed_min > 0 && (uptime_min % scheduleItem.elapsed_min == 0)) { - command(scheduleItem.cmd.c_str(), scheduleItem.value.c_str()); + command(scheduleItem.cmd.c_str(), compute(scheduleItem.value.c_str()).c_str()); } } last_uptime_min = uptime_min; @@ -416,8 +454,9 @@ void WebSchedulerService::loop() { uint16_t real_min = tm->tm_hour * 60 + tm->tm_min; for (const ScheduleItem & scheduleItem : *scheduleItems_) { - if (scheduleItem.active && (real_dow & scheduleItem.flags) && real_min == scheduleItem.elapsed_min) { - command(scheduleItem.cmd.c_str(), scheduleItem.value.c_str()); + uint8_t dow = scheduleItem.flags & SCHEDULEFLAG_SCHEDULE_TIMER ? 0 : scheduleItem.flags; + if (scheduleItem.active && (real_dow & dow) && real_min == scheduleItem.elapsed_min) { + command(scheduleItem.cmd.c_str(), compute(scheduleItem.value.c_str()).c_str()); } } last_tm_min = tm->tm_min; diff --git a/src/web/WebSchedulerService.h b/src/web/WebSchedulerService.h index 4b7e87897..65d12033d 100644 --- a/src/web/WebSchedulerService.h +++ b/src/web/WebSchedulerService.h @@ -22,8 +22,10 @@ #define EMSESP_SCHEDULER_FILE "/config/emsespScheduler.json" #define EMSESP_SCHEDULER_SERVICE_PATH "/rest/schedule" // GET and POST -#define SCHEDULEFLAG_SCHEDULE_TIMER 0x80 // 7th bit for Timer -#define MAX_STARTUP_RETRIES 3 // retry the start-up commands x times +#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 MAX_STARTUP_RETRIES 3 // retry the start-up commands x times namespace emsesp { @@ -61,6 +63,7 @@ class WebSchedulerService : public StatefulService { void ha_reset() { ha_registered_ = false; } + bool onChange(const char * cmd); #if defined(EMSESP_TEST) void test(); @@ -71,6 +74,7 @@ class WebSchedulerService : public StatefulService { private: #endif bool command(const char * cmd, const char * data); + void condition(); HttpEndpoint _httpEndpoint; FSPersistence _fsPersistence; diff --git a/src/web/shuntingYard.hpp b/src/web/shuntingYard.hpp new file mode 100644 index 000000000..fac408c6c --- /dev/null +++ b/src/web/shuntingYard.hpp @@ -0,0 +1,404 @@ +// Shunting-yard Algorithm +// https://en.wikipedia.org/wiki/Shunting-yard_algorithm +// +// Implementation notes for unary operators by Austin Taylor +// https://stackoverflow.com/a/5240912 +// +// Example: +// https://ideone.com/VocUTq +// +// License: +// If you use this code in binary / compiled / un-commented (removing all text comments) form, +// you can use it under CC0 license. +// +// But if you use this code as source code / readable text, since main content of this code is +// their notes, I recommend you to indicate notices which conform CC-BY-SA. For example, +// +// --- --- +// YOUR-CONTENT uses the following materials. +// (1) Wikipedia article [Shunting-yard algorithm](https://en.wikipedia.org/wiki/Shunting-yard_algorithm), +// which is released under the [Creative Commons Attribution-Share-Alike License 3.0](https://creativecommons.org/licenses/by-sa/3.0/). +// (2) [Implementation notes for unary operators in Shunting-Yard algorithm](https://stackoverflow.com/a/5240912) by Austin Taylor +// which is released under the [Creative Commons Attribution-Share-Alike License 2.5](https://creativecommons.org/licenses/by-sa/2.5/). +// --- --- +// copy from https://gist.github.com/t-mat/b9f681b7591cdae712f6 +// modified MDvP, 06.2024 +// +#include +#include +#include +#include + +class Token { + public: + enum class Type { + Unknown, + Number, + Operator, + LeftParen, + RightParen, + }; + + Token(Type type, const std::string & s, int8_t precedence = -1, bool rightAssociative = false, bool unary = false) + : type{type} + , str(s) + , precedence{precedence} + , rightAssociative{rightAssociative} + , unary{unary} { + } + + const Type type; + const std::string str; + const int8_t precedence; + const bool rightAssociative; + const bool unary; +}; + +std::deque exprToTokens(const std::string & expr) { + std::deque tokens; + + for (const auto * p = expr.c_str(); *p; ++p) { + if (isblank(*p)) { + // do nothing + } else if ((*p >= 'a' && *p <= 'z')) { + tokens.clear(); + return tokens; + } else if (isdigit(*p)) { + const auto * b = p; + while (isdigit(*p) || *p == '.') { + ++p; + } + const auto s = std::string(b, p); + tokens.push_back(Token{Token::Type::Number, s}); + --p; + } else { + Token::Type token = Token::Type::Operator; + int8_t precedence = -1; + bool rightAssociative = false; + bool unary = false; + char c = *p; + switch (c) { + default: + token = Token::Type::Unknown; + break; + case '(': + token = Token::Type::LeftParen; + break; + case ')': + token = Token::Type::RightParen; + break; + case '^': + precedence = 4; + rightAssociative = true; + break; + case '*': + precedence = 3; + break; + case '/': + precedence = 3; + break; + case '%': + precedence = 3; + break; + case '+': + precedence = 2; + break; + case '-': + // If current token is '-' + // and if it is the first token, or preceded by another operator, or left-paren, + if (tokens.empty() || tokens.back().type == Token::Type::Operator || tokens.back().type == Token::Type::LeftParen) { + // it's unary '-' + // note#1 : 'm' is a special operator name for unary '-' + // note#2 : It has highest precedence than any of the infix operators + unary = true; + c = 'm'; + precedence = 5; + } else { + // otherwise, it's binary '-' + precedence = 2; + } + break; + case '&': + if (p[1] == '&') + ++p; + precedence = 0; + break; + case '|': + if (p[1] == '|') + ++p; + precedence = 0; + break; + case '!': + unary = true; + precedence = 1; + break; + case '<': + if (p[1] == '=') { + ++p; + c = '{'; + } + precedence = 1; + break; + case '>': + if (p[1] == '=') { + ++p; + c = '}'; + } + precedence = 1; + break; + case '=': + if (p[1] == '=') + ++p; + precedence = 1; + break; + } + const auto s = std::string(1, c); + tokens.push_back(Token{token, s, precedence, rightAssociative, unary}); + } + } + + return tokens; +} + + +std::deque shuntingYard(const std::deque & tokens) { + std::deque queue; + std::vector stack; + + // While there are tokens to be read: + for (auto token : tokens) { + // Read a token + switch (token.type) { + case Token::Type::Number: + // If the token is a number, then add it to the output queue + queue.push_back(token); + break; + + case Token::Type::Operator: { + // If the token is operator, o1, then: + const auto o1 = token; + + // while there is an operator token, + while (!stack.empty()) { + // o2, at the top of stack, and + const auto o2 = stack.back(); + + // either o1 is left-associative and its precedence is + // *less than or equal* to that of o2, + // or o1 if right associative, and has precedence + // *less than* that of o2, + if ((!o1.rightAssociative && o1.precedence <= o2.precedence) || (o1.rightAssociative && o1.precedence < o2.precedence)) { + // then pop o2 off the stack, + stack.pop_back(); + // onto the output queue; + queue.push_back(o2); + + continue; + } + + // @@ otherwise, exit. + break; + } + + // push o1 onto the stack. + stack.push_back(o1); + } break; + + case Token::Type::LeftParen: + // If token is left parenthesis, then push it onto the stack + stack.push_back(token); + break; + + case Token::Type::RightParen: + // If token is right parenthesis: + { + bool match = false; + + // Until the token at the top of the stack + // is a left parenthesis, + while (!stack.empty() && stack.back().type != Token::Type::LeftParen) { + // pop operators off the stack + // onto the output queue. + queue.push_back(stack.back()); + stack.pop_back(); + match = true; + } + + if (!match && stack.empty()) { + // If the stack runs out without finding a left parenthesis, + // then there are mismatched parentheses. + return {}; + } + + // Pop the left parenthesis from the stack, + // but not onto the output queue. + stack.pop_back(); + } + break; + + default: + return {}; + } + } + + // When there are no more tokens to read: + // While there are still operator tokens in the stack: + while (!stack.empty()) { + // If the operator token on the top of the stack is a parenthesis, + // then there are mismatched parentheses. + if (stack.back().type == Token::Type::LeftParen) { + return {}; + } + + // Pop the operator onto the output queue. + queue.push_back(std::move(stack.back())); + stack.pop_back(); + } + return queue; +} + +// replace commands like "//" with its value" +std::string commands(std::string & expr) { + for (uint8_t device = 0; device < emsesp::EMSdevice::DeviceType::UNKNOWN; device++) { + const char * d = emsesp::EMSdevice::device_type_2_device_name(device); + auto f = expr.find(d); + while (f != std::string::npos) { + auto e = expr.find_first_of(" )=<>|&+-*\0", f); + if (e == std::string::npos) { + e = expr.length(); + } + char cmd[COMMAND_MAX_LENGTH]; + size_t l = e - f; + if (l >= sizeof(cmd) - 1) { + break; + } + expr.copy(cmd, l, f); + cmd[l] = '\0'; + if (strstr(cmd, "/value") == nullptr) { + strlcat(cmd, "/value", sizeof(cmd) - 6); + } + JsonDocument doc_out, doc_in; + JsonObject output = doc_out.to(); + JsonObject input = doc_in.to(); + std::string cmd_s = "api/" + std::string(cmd); + emsesp::Command::process(cmd_s.c_str(), true, input, output); + if (output.containsKey("api_data")) { + std::string data = output["api_data"].as(); + if (data == "true" || data == "ON" || data == "on") { + data = "1"; + } + if (data == "false" || data == "OFF" || data == "off") { + data = "0"; + } + expr.replace(f, l, data); + e = f + data.length(); + } + f = expr.find(d, e); + } + } + return expr; +} + +std::string compute(const std::string & expr) { + auto expr_new = emsesp::Helpers::toLower(expr); +#ifdef EMESESP_DEBUG + emsesp::EMSESP::logger().debug("calculate: %s", expr_new.c_str()); +#endif + commands(expr_new); +#ifdef EMESESP_DEBUG + emsesp::EMSESP::logger().debug("calculate: %s", expr_new.c_str()); +#endif + const auto tokens = exprToTokens(expr_new); + if (tokens.empty()) { + return "Error: no tokens"; + } + auto queue = shuntingYard(tokens); + std::vector stack; + + while (!queue.empty()) { + const auto token = queue.front(); + queue.pop_front(); + switch (token.type) { + case Token::Type::Number: + stack.push_back(std::stod(token.str)); + break; + + case Token::Type::Operator: { + if (token.unary) { + // unray operators + const auto rhs = stack.back(); + stack.pop_back(); + switch (token.str[0]) { + default: + return ""; + break; + case 'm': // Special operator name for unary '-' + stack.push_back(-rhs); + break; + case '!': + stack.push_back(!(int)rhs); + break; + } + } else { + // binary operators + const auto rhs = stack.back(); + stack.pop_back(); + const auto lhs = stack.back(); + stack.pop_back(); + + switch (token.str[0]) { + default: + return ""; + break; + case '^': + stack.push_back(static_cast(pow(lhs, rhs))); + break; + case '*': + stack.push_back(lhs * rhs); + break; + case '/': + stack.push_back(lhs / rhs); + break; + case '%': + stack.push_back((int)lhs % (int)rhs); + break; + case '+': + stack.push_back(lhs + rhs); + break; + case '-': + stack.push_back(lhs - rhs); + break; + case '&': + stack.push_back(((int)lhs && (int)rhs) ? 1 : 0); + break; + case '|': + stack.push_back(((int)lhs || (int)rhs) ? 1 : 0); + break; + case '<': + stack.push_back(((int)lhs < (int)rhs) ? 1 : 0); + break; + case '{': + stack.push_back(((int)lhs <= (int)rhs) ? 1 : 0); + break; + case '>': + stack.push_back(((int)lhs > (int)rhs) ? 1 : 0); + break; + case '}': + stack.push_back(((int)lhs >= (int)rhs) ? 1 : 0); + break; + case '=': + stack.push_back(((int)lhs == (int)rhs) ? 1 : 0); + break; + } + } + } break; + + default: + return ""; + } + } + if (stack.back() == (int)stack.back()) { + return (std::to_string((int)stack.back())); + } + return std::to_string(stack.back()); +} From bae5a112648c0ef364a1349d9946b8528ca79c9e Mon Sep 17 00:00:00 2001 From: MichaelDvP Date: Tue, 18 Jun 2024 18:23:08 +0200 Subject: [PATCH 2/7] schedule conditions chack numbers and strings, fix custom commands --- src/command.cpp | 13 + src/command.h | 1 + src/system.cpp | 4 + src/web/WebCustomEntityService.cpp | 52 ++-- src/web/WebCustomEntityService.h | 8 +- src/web/WebSchedulerService.cpp | 17 +- src/web/WebSchedulerService.h | 2 +- src/web/shuntingYard.hpp | 368 +++++++++++++++++++---------- 8 files changed, 297 insertions(+), 168 deletions(-) diff --git a/src/command.cpp b/src/command.cpp index 50b81e9b9..143f6644c 100644 --- a/src/command.cpp +++ b/src/command.cpp @@ -437,6 +437,19 @@ Command::CmdFunction * Command::find_command(const uint8_t device_type, const ui return nullptr; // command not found } +void Command::erase_device_commands(const uint8_t device_type) { + if (cmdfunctions_.empty()) { + return; + } + auto it = cmdfunctions_.end(); + do { + int i = it - cmdfunctions_.begin(); + if (cmdfunctions_[i].device_type_==device_type) { + cmdfunctions_.erase(it); + } + } while (it-- > cmdfunctions_.begin()); +} + void Command::erase_command(const uint8_t device_type, const char * cmd, uint8_t flag) { if ((cmd == nullptr) || (strlen(cmd) == 0) || (cmdfunctions_.empty())) { return; diff --git a/src/command.h b/src/command.h index d5a3d3854..c718f9224 100644 --- a/src/command.h +++ b/src/command.h @@ -126,6 +126,7 @@ class Command { static Command::CmdFunction * find_command(const uint8_t device_type, const uint8_t device_id, const char * cmd, const uint8_t flag); static std::string tagged_cmd(const std::string & cmd, const uint8_t flag); + static void erase_device_commands(const uint8_t device_type); static void erase_command(const uint8_t device_type, const char * cmd, uint8_t flag = CommandFlag::CMD_FLAG_DEFAULT); static void show(uuid::console::Shell & shell, uint8_t device_type, bool verbose); static void show_devices(uuid::console::Shell & shell); diff --git a/src/system.cpp b/src/system.cpp index db07845bd..a83b64ad1 100644 --- a/src/system.cpp +++ b/src/system.cpp @@ -124,6 +124,10 @@ bool System::command_allvalues(const char * value, const int8_t id, JsonObject o device_output = output["Custom Entities"].to(); EMSESP::webCustomEntityService.get_value_info(device_output, ""); + // Scheduler + device_output = output["Scheduler"].to(); + EMSESP::webSchedulerService.get_value_info(device_output, ""); + // Sensors device_output = output["Analog Sensors"].to(); EMSESP::analogsensor_.get_value_info(device_output, "values"); diff --git a/src/web/WebCustomEntityService.cpp b/src/web/WebCustomEntityService.cpp index be9fb8de0..53b802c10 100644 --- a/src/web/WebCustomEntityService.cpp +++ b/src/web/WebCustomEntityService.cpp @@ -34,6 +34,8 @@ WebCustomEntityService::WebCustomEntityService(AsyncWebServer * server, FS * fs, // load the settings when the service starts void WebCustomEntityService::begin() { _fsPersistence.readFromFS(); + // save a local pointer to the item list + EMSESP::webCustomEntityService.read([&](WebCustomEntity & webEntity) { customEntityItems_ = &webEntity.customEntityItems; }); EMSESP::logger().info("Starting Custom Entity service"); Mqtt::subscribe(EMSdevice::DeviceType::CUSTOM, "custom/#", nullptr); // use empty function callback } @@ -63,9 +65,7 @@ void WebCustomEntity::read(WebCustomEntity & webEntity, JsonObject root) { // this loads the data into the internal class StateUpdateResult WebCustomEntity::update(JsonObject root, WebCustomEntity & webCustomEntity) { // reset everything to start fresh - for (CustomEntityItem & entityItem : webCustomEntity.customEntityItems) { - Command::erase_command(EMSdevice::DeviceType::CUSTOM, entityItem.name.c_str()); - } + Command::erase_device_commands(EMSdevice::DeviceType::CUSTOM); webCustomEntity.customEntityItems.clear(); EMSESP::webCustomEntityService.ha_reset(); @@ -112,12 +112,12 @@ StateUpdateResult WebCustomEntity::update(JsonObject root, WebCustomEntity & web webCustomEntity.customEntityItems.push_back(entityItem); // add to list - if (entityItem.writeable) { + if (webCustomEntity.customEntityItems.back().writeable && !webCustomEntity.customEntityItems.back().name.empty()) { Command::add( EMSdevice::DeviceType::CUSTOM, webCustomEntity.customEntityItems.back().name.c_str(), [webCustomEntity](const char * value, const int8_t id) { - return EMSESP::webCustomEntityService.command_setvalue(value, webCustomEntity.customEntityItems.back().name); + return EMSESP::webCustomEntityService.command_setvalue(value, id, webCustomEntity.customEntityItems.back().name.c_str()); }, FL_(entity_cmd), CommandFlag::ADMIN_ONLY); @@ -128,9 +128,8 @@ StateUpdateResult WebCustomEntity::update(JsonObject root, WebCustomEntity & web } // set value by api command -bool WebCustomEntityService::command_setvalue(const char * value, const std::string name) { - EMSESP::webCustomEntityService.read([&](WebCustomEntity & webEntity) { customEntityItems = &webEntity.customEntityItems; }); - for (CustomEntityItem & entityItem : *customEntityItems) { +bool WebCustomEntityService::command_setvalue(const char * value, const int8_t id, const char * name) { + for (CustomEntityItem & entityItem : *customEntityItems_) { if (Helpers::toLower(entityItem.name) == Helpers::toLower(name)) { if (entityItem.ram == 1) { entityItem.data = value; @@ -249,20 +248,18 @@ void WebCustomEntityService::render_value(JsonObject output, CustomEntityItem en // display all custom entities // adding each one, with UOM to a json object string void WebCustomEntityService::show_values(JsonObject output) { - for (const CustomEntityItem & entity : *customEntityItems) { + for (const CustomEntityItem & entity : *customEntityItems_) { render_value(output, entity, false, false, true); // with add_uom } } // process json output for info/commands and value_info bool WebCustomEntityService::get_value_info(JsonObject output, const char * cmd) { - EMSESP::webCustomEntityService.read([&](WebCustomEntity & webEntity) { customEntityItems = &webEntity.customEntityItems; }); - // if it's commands... if (Helpers::toLower(cmd) == F_(commands)) { output[F_(info)] = Helpers::translated_word(FL_(info_cmd)); output[F_(commands)] = Helpers::translated_word(FL_(commands_cmd)); - for (const auto & entity : *customEntityItems) { + for (const auto & entity : *customEntityItems_) { output[entity.name] = "custom entity"; } return true; @@ -270,14 +267,14 @@ bool WebCustomEntityService::get_value_info(JsonObject output, const char * cmd) // if no entries, return empty json // https://github.com/emsesp/EMS-ESP32/issues/1297 - if (customEntityItems->size() == 0) { + if (customEntityItems_->size() == 0) { return true; } // if it's info or values... if (strlen(cmd) == 0 || Helpers::toLower(cmd) == F_(values) || Helpers::toLower(cmd) == F_(info)) { // list all names - for (const CustomEntityItem & entity : *customEntityItems) { + for (const CustomEntityItem & entity : *customEntityItems_) { render_value(output, entity); } return true; @@ -293,7 +290,7 @@ bool WebCustomEntityService::get_value_info(JsonObject output, const char * cmd) attribute_s = breakp + 1; } - for (const auto & entity : *customEntityItems) { + for (const auto & entity : *customEntityItems_) { if (Helpers::toLower(entity.name) == command_s) { output["name"] = entity.name; output["ram"] = entity.ram; @@ -370,12 +367,11 @@ void WebCustomEntityService::publish(const bool force) { return; } - EMSESP::webCustomEntityService.read([&](WebCustomEntity & webEntity) { customEntityItems = &webEntity.customEntityItems; }); - if (customEntityItems->size() == 0) { + if (customEntityItems_->size() == 0) { return; } if (Mqtt::publish_single() && force) { - for (const CustomEntityItem & entityItem : *customEntityItems) { + for (const CustomEntityItem & entityItem : *customEntityItems_) { publish_single(entityItem); } } @@ -384,7 +380,7 @@ void WebCustomEntityService::publish(const bool force) { JsonObject output = doc.to(); bool ha_created = ha_registered_; - for (const CustomEntityItem & entityItem : *customEntityItems) { + for (const CustomEntityItem & entityItem : *customEntityItems_) { render_value(output, entityItem); // create HA config if (Mqtt::ha_enabled() && !ha_registered_) { @@ -461,15 +457,14 @@ void WebCustomEntityService::publish(const bool force) { // count only entities with valid value or command to show in dashboard uint8_t WebCustomEntityService::count_entities() { - EMSESP::webCustomEntityService.read([&](WebCustomEntity & webEntity) { customEntityItems = &webEntity.customEntityItems; }); - if (customEntityItems->size() == 0) { + if (customEntityItems_->size() == 0) { return 0; } JsonDocument doc; JsonObject output = doc.to(); uint8_t count = 0; - for (const CustomEntityItem & entity : *customEntityItems) { + for (const CustomEntityItem & entity : *customEntityItems_) { render_value(output, entity); count += (output.containsKey(entity.name) || entity.writeable) ? 1 : 0; } @@ -478,9 +473,8 @@ uint8_t WebCustomEntityService::count_entities() { } uint8_t WebCustomEntityService::has_commands() { - EMSESP::webCustomEntityService.read([&](WebCustomEntity & webEntity) { customEntityItems = &webEntity.customEntityItems; }); uint8_t count = 0; - for (const CustomEntityItem & entity : *customEntityItems) { + for (const CustomEntityItem & entity : *customEntityItems_) { count += entity.writeable ? 1 : 0; } @@ -489,12 +483,10 @@ uint8_t WebCustomEntityService::has_commands() { // send to dashboard, msgpack don't like serialized, use number void WebCustomEntityService::generate_value_web(JsonObject output) { - EMSESP::webCustomEntityService.read([&](WebCustomEntity & webEntity) { customEntityItems = &webEntity.customEntityItems; }); - output["label"] = (std::string) "Custom Entities"; JsonArray data = output["data"].to(); uint8_t index = 0; - for (const CustomEntityItem & entity : *customEntityItems) { + for (const CustomEntityItem & entity : *customEntityItems_) { JsonObject obj = data.add(); // create the object, we know there is a value obj["id"] = "00" + entity.name; obj["u"] = entity.uom; @@ -562,10 +554,9 @@ void WebCustomEntityService::generate_value_web(JsonObject output) { // fetch telegram, called from emsesp::fetch void WebCustomEntityService::fetch() { - EMSESP::webCustomEntityService.read([&](WebCustomEntity & webEntity) { customEntityItems = &webEntity.customEntityItems; }); const uint8_t len[] = {1, 1, 1, 2, 2, 3, 3, 4}; - for (auto & entity : *customEntityItems) { + for (auto & entity : *customEntityItems_) { if (entity.device_id > 0 && entity.type_id > 0) { // ths excludes also RAM type bool needFetch = true; uint8_t fetchblock = entity.type_id > 0x0FF ? 25 : 27; @@ -593,10 +584,9 @@ void WebCustomEntityService::fetch() { // called on process telegram, read from telegram bool WebCustomEntityService::get_value(std::shared_ptr telegram) { bool has_change = false; - EMSESP::webCustomEntityService.read([&](WebCustomEntity & webEntity) { customEntityItems = &webEntity.customEntityItems; }); // read-length of BOOL, INT8, UINT8, INT16, UINT16, UINT24, TIME, UINT32 const uint8_t len[] = {1, 1, 1, 2, 2, 3, 3, 4}; - for (auto & entity : *customEntityItems) { + for (auto & entity : *customEntityItems_) { if (entity.value_type == DeviceValueType::STRING && telegram->type_id == entity.type_id && telegram->src == entity.device_id && telegram->offset <= entity.offset && (telegram->offset + telegram->message_length) >= (entity.offset + (uint8_t)entity.factor)) { auto data = Helpers::data_to_hex(telegram->message_data, (uint8_t)entity.factor); diff --git a/src/web/WebCustomEntityService.h b/src/web/WebCustomEntityService.h index f044dcb03..cb730f0a6 100644 --- a/src/web/WebCustomEntityService.h +++ b/src/web/WebCustomEntityService.h @@ -43,8 +43,7 @@ class CustomEntityItem { class WebCustomEntity { public: - std::vector customEntityItems; - // std::list customEntityItems; + std::list customEntityItems; static void read(WebCustomEntity & webEntity, JsonObject root); static StateUpdateResult update(JsonObject root, WebCustomEntity & webEntity); @@ -57,7 +56,7 @@ class WebCustomEntityService : public StatefulService { void begin(); void publish_single(const CustomEntityItem & entity); void publish(const bool force = false); - bool command_setvalue(const char * value, const std::string name); + bool command_setvalue(const char * value, const int8_t id, const char * name); bool get_value_info(JsonObject output, const char * cmd); bool get_value(std::shared_ptr telegram); void fetch(); @@ -79,8 +78,7 @@ class WebCustomEntityService : public StatefulService { HttpEndpoint _httpEndpoint; FSPersistence _fsPersistence; - std::vector * customEntityItems; // pointer to the list of entity items - // std::list * customEntityItems; // pointer to the list of entity items + std::list * customEntityItems_; // pointer to the list of entity items bool ha_registered_ = false; }; diff --git a/src/web/WebSchedulerService.cpp b/src/web/WebSchedulerService.cpp index e25868971..223c5cc17 100644 --- a/src/web/WebSchedulerService.cpp +++ b/src/web/WebSchedulerService.cpp @@ -57,11 +57,8 @@ void WebScheduler::read(WebScheduler & webScheduler, JsonObject root) { // 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) { - for (ScheduleItem & scheduleItem : webScheduler.scheduleItems) { - Command::erase_command(EMSdevice::DeviceType::SCHEDULER, scheduleItem.name.c_str()); - } - // reset the list + Command::erase_device_commands(EMSdevice::DeviceType::SCHEDULER); webScheduler.scheduleItems.clear(); EMSESP::webSchedulerService.ha_reset(); @@ -88,7 +85,7 @@ StateUpdateResult WebScheduler::update(JsonObject root, WebScheduler & webSchedu EMSdevice::DeviceType::SCHEDULER, webScheduler.scheduleItems.back().name.c_str(), [webScheduler](const char * value, const int8_t id) { - return EMSESP::webSchedulerService.command_setvalue(value, webScheduler.scheduleItems.back().name); + return EMSESP::webSchedulerService.command_setvalue(value, id, webScheduler.scheduleItems.back().name.c_str()); }, FL_(schedule_cmd), CommandFlag::ADMIN_ONLY); @@ -102,20 +99,20 @@ StateUpdateResult WebScheduler::update(JsonObject root, WebScheduler & webSchedu } // set active by api command -bool WebSchedulerService::command_setvalue(const char * value, const std::string name) { +bool WebSchedulerService::command_setvalue(const char * value, const int8_t id, const char * name) { bool v; if (!Helpers::value2bool(value, v)) { return false; } for (ScheduleItem & scheduleItem : *scheduleItems_) { - if (scheduleItem.name == name) { + if (Helpers::toLower(scheduleItem.name) == Helpers::toLower(name)) { if (scheduleItem.active == v) { return true; } scheduleItem.active = v; - publish_single(name.c_str(), v); + publish_single(name, v); if (EMSESP::mqtt_.get_publish_onchange(0)) { publish(); @@ -212,7 +209,7 @@ bool WebSchedulerService::get_value_info(JsonObject output, const char * cmd) { // publish single value void WebSchedulerService::publish_single(const char * name, const bool state) { - if (!Mqtt::publish_single() || name == nullptr || name[0] == '\0') { + if (!Mqtt::enabled() || !Mqtt::publish_single() || name == nullptr || name[0] == '\0') { return; } @@ -386,7 +383,7 @@ void WebSchedulerService::condition() { #ifdef EMESESP_DEBUG emsesp::EMSESP::logger().debug("condition match: %s", match.c_str()); #endif - if (!match.empty() && match.c_str()[0] == '1') { + if (!match.empty() && match[0] == '1') { if (scheduleItem.retry_cnt == 0xFF) { // default unswitched scheduleItem.retry_cnt = command(scheduleItem.cmd.c_str(), compute(scheduleItem.value.c_str()).c_str()) ? 1 : 0xFF; } diff --git a/src/web/WebSchedulerService.h b/src/web/WebSchedulerService.h index 65d12033d..042298d61 100644 --- a/src/web/WebSchedulerService.h +++ b/src/web/WebSchedulerService.h @@ -58,7 +58,7 @@ class WebSchedulerService : public StatefulService { void publish_single(const char * name, const bool state); void publish(const bool force = false); bool has_commands(); - bool command_setvalue(const char * value, const std::string name); + bool command_setvalue(const char * value, const int8_t id, const char * name); bool get_value_info(JsonObject output, const char * cmd); void ha_reset() { ha_registered_ = false; diff --git a/src/web/shuntingYard.hpp b/src/web/shuntingYard.hpp index fac408c6c..ef06f8bf2 100644 --- a/src/web/shuntingYard.hpp +++ b/src/web/shuntingYard.hpp @@ -34,24 +34,26 @@ class Token { enum class Type { Unknown, Number, + String, Operator, + Compare, + Logic, + Unary, LeftParen, RightParen, }; - Token(Type type, const std::string & s, int8_t precedence = -1, bool rightAssociative = false, bool unary = false) + Token(Type type, const std::string & s, int8_t precedence = -1, bool rightAssociative = false) : type{type} , str(s) , precedence{precedence} - , rightAssociative{rightAssociative} - , unary{unary} { + , rightAssociative{rightAssociative} { } const Type type; const std::string str; const int8_t precedence; const bool rightAssociative; - const bool unary; }; std::deque exprToTokens(const std::string & expr) { @@ -60,22 +62,48 @@ std::deque exprToTokens(const std::string & expr) { for (const auto * p = expr.c_str(); *p; ++p) { if (isblank(*p)) { // do nothing - } else if ((*p >= 'a' && *p <= 'z')) { - tokens.clear(); - return tokens; + } else if (*p >= 'a' && *p <= 'z') { + const auto * b = p; + while ((*p >= 'a' && *p <= 'z') || (*p == '_')) { + ++p; + } + const auto s = std::string(b, p); + tokens.push_back(Token{Token::Type::String, s, -2}); + --p; + } else if (*p == '"') { + ++p; + const auto * b = p; + while (*p && *p != '"') { + ++p; + } + const auto s = std::string(b, p); + tokens.push_back(Token{Token::Type::String, s, -3}); + if (*p == '\0') { + --p; + } + } else if (*p == '\'') { + ++p; + const auto * b = p; + while (*p && *p != '\'') { + ++p; + } + const auto s = std::string(b, p); + tokens.push_back(Token{Token::Type::String, s, -3}); + if (*p == '\0') { + --p; + } } else if (isdigit(*p)) { const auto * b = p; while (isdigit(*p) || *p == '.') { ++p; } const auto s = std::string(b, p); - tokens.push_back(Token{Token::Type::Number, s}); + tokens.push_back(Token{Token::Type::Number, s, -4}); --p; } else { Token::Type token = Token::Type::Operator; int8_t precedence = -1; bool rightAssociative = false; - bool unary = false; char c = *p; switch (c) { default: @@ -106,11 +134,16 @@ std::deque exprToTokens(const std::string & expr) { case '-': // If current token is '-' // and if it is the first token, or preceded by another operator, or left-paren, - if (tokens.empty() || tokens.back().type == Token::Type::Operator || tokens.back().type == Token::Type::LeftParen) { + if (tokens.empty() || tokens.back().type == Token::Type::Operator || tokens.back().type == Token::Type::Compare + || tokens.back().type == Token::Type::Logic || tokens.back().type == Token::Type::Unary || tokens.back().type == Token::Type::LeftParen) { // it's unary '-' // note#1 : 'm' is a special operator name for unary '-' // note#2 : It has highest precedence than any of the infix operators - unary = true; + if (!tokens.empty() && tokens.back().str[0] == 'm') { // double unary minus + tokens.pop_back(); + continue; + } + token = Token::Type::Unary; c = 'm'; precedence = 5; } else { @@ -122,15 +155,23 @@ std::deque exprToTokens(const std::string & expr) { if (p[1] == '&') ++p; precedence = 0; + token = Token::Type::Logic; break; case '|': if (p[1] == '|') ++p; precedence = 0; + token = Token::Type::Logic; break; case '!': - unary = true; - precedence = 1; + if (p[1] == '=') { + ++p; + precedence = 1; + token = Token::Type::Compare; + } else { + precedence = 1; + token = Token::Type::Unary; + } break; case '<': if (p[1] == '=') { @@ -138,6 +179,7 @@ std::deque exprToTokens(const std::string & expr) { c = '{'; } precedence = 1; + token = Token::Type::Compare; break; case '>': if (p[1] == '=') { @@ -145,15 +187,17 @@ std::deque exprToTokens(const std::string & expr) { c = '}'; } precedence = 1; + token = Token::Type::Compare; break; case '=': if (p[1] == '=') ++p; precedence = 1; + token = Token::Type::Compare; break; } const auto s = std::string(1, c); - tokens.push_back(Token{token, s, precedence, rightAssociative, unary}); + tokens.push_back(Token{token, s, precedence, rightAssociative}); } } @@ -170,10 +214,14 @@ std::deque shuntingYard(const std::deque & tokens) { // Read a token switch (token.type) { case Token::Type::Number: + case Token::Type::String: // If the token is a number, then add it to the output queue queue.push_back(token); break; + case Token::Type::Unary: + case Token::Type::Compare: + case Token::Type::Logic: case Token::Type::Operator: { // If the token is operator, o1, then: const auto o1 = token; @@ -209,32 +257,30 @@ std::deque shuntingYard(const std::deque & tokens) { stack.push_back(token); break; - case Token::Type::RightParen: + case Token::Type::RightParen: { // If token is right parenthesis: - { - bool match = false; + bool match = false; - // Until the token at the top of the stack - // is a left parenthesis, - while (!stack.empty() && stack.back().type != Token::Type::LeftParen) { - // pop operators off the stack - // onto the output queue. - queue.push_back(stack.back()); - stack.pop_back(); - match = true; - } - - if (!match && stack.empty()) { - // If the stack runs out without finding a left parenthesis, - // then there are mismatched parentheses. - return {}; - } - - // Pop the left parenthesis from the stack, - // but not onto the output queue. + // Until the token at the top of the stack + // is a left parenthesis, + while (!stack.empty() && stack.back().type != Token::Type::LeftParen) { + // pop operators off the stack + // onto the output queue. + queue.push_back(stack.back()); stack.pop_back(); + match = true; } - break; + + if (!match && stack.empty()) { + // If the stack runs out without finding a left parenthesis, + // then there are mismatched parentheses. + return {}; + } + + // Pop the left parenthesis from the stack, + // but not onto the output queue. + stack.pop_back(); + } break; default: return {}; @@ -263,7 +309,7 @@ std::string commands(std::string & expr) { const char * d = emsesp::EMSdevice::device_type_2_device_name(device); auto f = expr.find(d); while (f != std::string::npos) { - auto e = expr.find_first_of(" )=<>|&+-*\0", f); + auto e = expr.find_first_of(" )=<>|&+-*", f); if (e == std::string::npos) { e = expr.length(); } @@ -284,14 +330,15 @@ std::string commands(std::string & expr) { emsesp::Command::process(cmd_s.c_str(), true, input, output); if (output.containsKey("api_data")) { std::string data = output["api_data"].as(); - if (data == "true" || data == "ON" || data == "on") { - data = "1"; - } - if (data == "false" || data == "OFF" || data == "off") { - data = "0"; + // set strings in quotations for something like "3-way-valve" + if (isdigit(data[0] == 0 && data[0] != '-')) { + data.insert(data.begin(), '"'); + data.insert(data.end(), '"'); } expr.replace(f, l, data); e = f + data.length(); + } else { + return expr = ""; } f = expr.find(d, e); } @@ -299,97 +346,179 @@ std::string commands(std::string & expr) { return expr; } +int islogic(const std::string & s) { + if (s[0] == '1' || s == "on" || s == "ON" || s == "true") { + return 1; + } + if (s[0] == '0' || s == "off" || s == "OFF" || s == "false") { + return 0; + } + return 0; +} + +bool isnum(const std::string & s) { + if (isdigit(s[0]) || (s[0] == '-' && isdigit(s[1]))) { + return true; + } + return false; +} + std::string compute(const std::string & expr) { auto expr_new = emsesp::Helpers::toLower(expr); -#ifdef EMESESP_DEBUG - emsesp::EMSESP::logger().debug("calculate: %s", expr_new.c_str()); -#endif commands(expr_new); -#ifdef EMESESP_DEBUG - emsesp::EMSESP::logger().debug("calculate: %s", expr_new.c_str()); -#endif + // emsesp::EMSESP::logger().info("calculate: %s", expr_new.c_str()); const auto tokens = exprToTokens(expr_new); if (tokens.empty()) { - return "Error: no tokens"; + return ""; } - auto queue = shuntingYard(tokens); - std::vector stack; + auto queue = shuntingYard(tokens); + if (queue.empty()) { + return ""; + } + std::vector stack; while (!queue.empty()) { const auto token = queue.front(); queue.pop_front(); switch (token.type) { case Token::Type::Number: - stack.push_back(std::stod(token.str)); + case Token::Type::String: + stack.push_back(token.str); break; + case Token::Type::Unary: { + if (stack.empty()) { + return ""; + } + const auto rhs = stack.back(); + stack.pop_back(); + switch (token.str[0]) { + default: + return ""; + break; + case 'm': // Special operator name for unary '-' + if (!isnum(rhs)) { + return ""; + } + stack.push_back(std::to_string(-1 * std::stod(rhs))); + break; + case '!': + stack.push_back(islogic(rhs) == 0 ? "1" : "0"); + break; + } + } break; + case Token::Type::Compare: { + if (stack.size() < 2) { + return ""; + } + const auto rhs = stack.back(); + stack.pop_back(); + const auto lhs = stack.back(); + stack.pop_back(); + switch (token.str[0]) { + default: + return ""; + break; + case '<': + if (isnum(rhs) && isnum(lhs)) { + stack.push_back((std::stod(lhs) < std::stod(rhs)) ? "1" : "0"); + break; + } + stack.push_back((lhs < rhs) ? "1" : "0"); + break; + case '{': + if (isnum(rhs) && isnum(lhs)) { + stack.push_back((std::stod(lhs) <= std::stod(rhs)) ? "1" : "0"); + break; + } + stack.push_back((lhs <= rhs) ? "1" : "0"); + break; + case '>': + if (isnum(rhs) && isnum(lhs)) { + stack.push_back((std::stod(lhs) > std::stod(rhs)) ? "1" : "0"); + break; + } + stack.push_back((lhs > rhs) ? "1" : "0"); + break; + case '}': + if (isnum(rhs) && isnum(lhs)) { + stack.push_back((std::stod(lhs) >= std::stod(rhs)) ? "1" : "0"); + break; + } + stack.push_back((lhs >= rhs) ? "1" : "0"); + break; + case '=': + if (isnum(rhs) && isnum(lhs)) { + stack.push_back((std::stod(lhs) == std::stod(rhs)) ? "1" : "0"); + break; + } + stack.push_back((lhs == rhs) ? "1" : "0"); + break; + case '!': + if (isnum(rhs) && isnum(lhs)) { + stack.push_back((std::stod(lhs) != std::stod(rhs)) ? "1" : "0"); + break; + } + stack.push_back((lhs != rhs) ? "1" : "0"); + break; + } + } break; + case Token::Type::Logic: { + // binary operators + if (stack.size() < 2) { + return ""; + } + const auto rhs = islogic(stack.back()); + stack.pop_back(); + const auto lhs = islogic(stack.back()); + stack.pop_back(); + switch (token.str[0]) { + default: + return ""; + break; + case '&': + stack.push_back((lhs && rhs) ? "1" : "0"); + break; + case '|': + stack.push_back((lhs || rhs) ? "1" : "0"); + break; + } + } break; case Token::Type::Operator: { - if (token.unary) { - // unray operators - const auto rhs = stack.back(); - stack.pop_back(); - switch (token.str[0]) { - default: - return ""; - break; - case 'm': // Special operator name for unary '-' - stack.push_back(-rhs); - break; - case '!': - stack.push_back(!(int)rhs); - break; - } - } else { - // binary operators - const auto rhs = stack.back(); - stack.pop_back(); - const auto lhs = stack.back(); - stack.pop_back(); + // binary operators + if (stack.empty() || !isnum(stack.back())) { + return ""; + } + const auto rhs = std::stod(stack.back()); + stack.pop_back(); + if (stack.empty() || !isnum(stack.back())) { + return ""; + } + const auto lhs = std::stod(stack.back()); + stack.pop_back(); - switch (token.str[0]) { - default: - return ""; - break; - case '^': - stack.push_back(static_cast(pow(lhs, rhs))); - break; - case '*': - stack.push_back(lhs * rhs); - break; - case '/': - stack.push_back(lhs / rhs); - break; - case '%': - stack.push_back((int)lhs % (int)rhs); - break; - case '+': - stack.push_back(lhs + rhs); - break; - case '-': - stack.push_back(lhs - rhs); - break; - case '&': - stack.push_back(((int)lhs && (int)rhs) ? 1 : 0); - break; - case '|': - stack.push_back(((int)lhs || (int)rhs) ? 1 : 0); - break; - case '<': - stack.push_back(((int)lhs < (int)rhs) ? 1 : 0); - break; - case '{': - stack.push_back(((int)lhs <= (int)rhs) ? 1 : 0); - break; - case '>': - stack.push_back(((int)lhs > (int)rhs) ? 1 : 0); - break; - case '}': - stack.push_back(((int)lhs >= (int)rhs) ? 1 : 0); - break; - case '=': - stack.push_back(((int)lhs == (int)rhs) ? 1 : 0); - break; - } + switch (token.str[0]) { + default: + return ""; + break; + case '^': + stack.push_back(std::to_string(pow(lhs, rhs))); + break; + case '*': + stack.push_back(std::to_string(lhs * rhs)); + break; + case '/': + stack.push_back(std::to_string(lhs / rhs)); + break; + case '%': + stack.push_back(std::to_string(static_cast(lhs) % static_cast(rhs))); + break; + case '+': + stack.push_back(std::to_string(lhs + rhs)); + break; + case '-': + stack.push_back(std::to_string(lhs - rhs)); + break; } } break; @@ -397,8 +526,5 @@ std::string compute(const std::string & expr) { return ""; } } - if (stack.back() == (int)stack.back()) { - return (std::to_string((int)stack.back())); - } - return std::to_string(stack.back()); + return stack.back(); } From 03496acd222f5704525a54a33d5ca493b0e1b367 Mon Sep 17 00:00:00 2001 From: MichaelDvP Date: Wed, 19 Jun 2024 11:38:07 +0200 Subject: [PATCH 3/7] back to arduino 2.xx --- platformio.ini | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/platformio.ini b/platformio.ini index 12c81802d..b2e4be24a 100644 --- a/platformio.ini +++ b/platformio.ini @@ -38,7 +38,7 @@ unbuild_flags = ${common.core_unbuild_flags} [espressi32_base] -platform = espressif32 +platform = espressif32@6.7.0 framework = arduino board_build.filesystem = littlefs build_flags = @@ -51,9 +51,9 @@ extra_scripts = [espressi32_base_tasmota] ; use Tasmota's library which removes some unused libs (like mbedtsl, so no WiFi_secure.h) and increases available heap ; Tasmota Arduino Core 2.0.17 with IPv6 support, based on IDF 4.4.7 -; platform = https://github.com/tasmota/platform-espressif32/releases/download/2024.05.00/platform-espressif32.zip +platform = https://github.com/tasmota/platform-espressif32/releases/download/2024.05.00/platform-espressif32.zip ; Tasmota Arduino Core 3.0.1.240605 based on IDF v5.1.4.240602 -platform = https://github.com/tasmota/platform-espressif32/releases/download/2024.06.10/platform-espressif32.zip +; platform = https://github.com/tasmota/platform-espressif32/releases/download/2024.06.10/platform-espressif32.zip framework = arduino board_build.filesystem = littlefs build_flags = From 6c111c7816b02853424d665c53ab3120a2091c2b Mon Sep 17 00:00:00 2001 From: MichaelDvP Date: Thu, 20 Jun 2024 15:44:02 +0200 Subject: [PATCH 4/7] Scheduler conditions: allow system --- CHANGELOG_LATEST.md | 1 + platformio.ini | 2 +- src/command.cpp | 2 +- src/emsesp.cpp | 4 +++ src/system.cpp | 54 ++++++++++++++++++++++++++++----- src/system.h | 1 + src/web/WebSchedulerService.cpp | 4 ++- src/web/shuntingYard.hpp | 47 +++++++++++++++++----------- 8 files changed, 86 insertions(+), 29 deletions(-) diff --git a/CHANGELOG_LATEST.md b/CHANGELOG_LATEST.md index 11b8cd41d..a7e39dc21 100644 --- a/CHANGELOG_LATEST.md +++ b/CHANGELOG_LATEST.md @@ -22,6 +22,7 @@ - timeout for remote thermostat emulation [#1680](https://github.com/emsesp/EMS-ESP32/discussions/1680), [#1774](https://github.com/emsesp/EMS-ESP32/issues/1774) - CR120 thermostat as own model() [#1779](https://github.com/emsesp/EMS-ESP32/discussions/1779) - Modules - external linkable module library [#1778](https://github.com/emsesp/EMS-ESP32/issues/1778) +- Scheduler onChange and Conditions [#1806](https://github.com/emsesp/EMS-ESP32/issues/1806) ## Fixed diff --git a/platformio.ini b/platformio.ini index 6c7dca081..3262c6ed7 100644 --- a/platformio.ini +++ b/platformio.ini @@ -195,7 +195,7 @@ build_flags = [env:espressi32_v3] platform = espressif32 platform_packages= - platformio/framework-arduinoespressif32 @ https://github.com/espressif/arduino-esp32.git#3.0.0-rc2 + platformio/framework-arduinoespressif32 @ https://github.com/espressif/arduino-esp32.git#3.0.1 platformio/framework-arduinoespressif32-libs @ https://github.com/espressif/esp32-arduino-libs.git#idf-release/v5.1 framework = arduino board = esp32dev diff --git a/src/command.cpp b/src/command.cpp index 143f6644c..fbd132a8f 100644 --- a/src/command.cpp +++ b/src/command.cpp @@ -322,7 +322,7 @@ uint8_t Command::call(const uint8_t device_type, const char * cmd, const char * if (single_command) { // exception 1: anything that is from System // exception 2: boiler coldshot command - bool get_attributes = (!cf || !cf->cmdfunction_json_) && (device_type > EMSdevice::DeviceType::SYSTEM) && (strcmp(cmd, F_(coldshot)) != 0); + bool get_attributes = (!cf || !cf->cmdfunction_json_) && (strcmp(cmd, F_(coldshot)) != 0); if (get_attributes) { LOG_DEBUG("Calling %s command '%s' to retrieve attributes", dname, cmd); diff --git a/src/emsesp.cpp b/src/emsesp.cpp index 88a74195f..aabaa64bc 100644 --- a/src/emsesp.cpp +++ b/src/emsesp.cpp @@ -771,6 +771,10 @@ bool EMSESP::get_device_value_info(JsonObject root, const char * cmd, const int8 return webCustomEntityService.get_value_info(root, cmd); } + if (devicetype == DeviceType::SYSTEM) { + return system_.get_value_info(root, cmd); + } + char error[100]; snprintf(error, sizeof(error), "cannot find values for entity '%s'", cmd); root["message"] = error; diff --git a/src/system.cpp b/src/system.cpp index 9f9fea13c..d01ea90e0 100644 --- a/src/system.cpp +++ b/src/system.cpp @@ -1272,6 +1272,44 @@ bool System::saveSettings(const char * filename, const char * section, JsonObjec return false; // not found } +bool System::get_value_info(JsonObject root, const char * command) { + if (command == nullptr || strlen(command) == 0) { + LOG_ERROR("empty system command"); + return false; + } + char cmd[COMMAND_MAX_LENGTH]; + strlcpy(cmd, command, sizeof(cmd)); + char * val = strstr(cmd, "/value"); + if (val) { + val[0] = '\0'; + } + char * dash = strchr(cmd, '/'); + if (dash) { + *dash = '\0'; + dash++; + } + if (command_info("", 0, root)) { + std::string s; + if (dash && root[cmd].containsKey(dash)) { + s = root[cmd][dash].as(); + } else if (root.containsKey(cmd)) { + s = root[cmd].as(); + } + if (!s.empty()) { + root.clear(); + if (val) { + root["api_data"] = s; + } else { + root["value"] = s; + } + return true; + } + } + root.clear(); + LOG_ERROR("system command not found: %s from %s", cmd, command); + return false; +} + // export status information including the device information // http://ems-esp/api/system/info bool System::command_info(const char * value, const int8_t id, JsonObject output) { @@ -1452,13 +1490,13 @@ bool System::command_info(const char * value, const int8_t id, JsonObject output // Settings node = output["Settings"].to(); EMSESP::webSettingsService.read([&](WebSettings & settings) { - node["board profile"] = settings.board_profile; - node["locale"] = settings.locale; - node["tx mode"] = settings.tx_mode; - node["ems bus id"] = settings.ems_bus_id; - node["shower timer"] = settings.shower_timer; - node["shower alert"] = settings.shower_alert; - node["shpwe_min_duration"] = settings.shower_min_duration; // seconds + node["board profile"] = settings.board_profile; + node["locale"] = settings.locale; + node["tx mode"] = settings.tx_mode; + node["ems bus id"] = settings.ems_bus_id; + node["shower timer"] = settings.shower_timer; + node["shower min duration"] = settings.shower_min_duration; // seconds + node["shower alert"] = settings.shower_alert; if (settings.shower_alert) { node["shower alert coldshot"] = settings.shower_alert_coldshot; // seconds node["shower alert trigger"] = settings.shower_alert_trigger; // minutes @@ -1503,7 +1541,7 @@ bool System::command_info(const char * value, const int8_t id, JsonObject output obj["product id"] = emsdevice->product_id(); obj["version"] = emsdevice->version(); obj["entities"] = emsdevice->count_entities(); - char result[300]; + char result[500]; (void)emsdevice->show_telegram_handlers(result, sizeof(result), EMSdevice::Handlers::RECEIVED); if (result[0] != '\0') { obj["handlers received"] = result; // don't show handlers if there aren't any diff --git a/src/system.h b/src/system.h index 8ccbd4ee9..fff6e4afb 100644 --- a/src/system.h +++ b/src/system.h @@ -61,6 +61,7 @@ class System { static bool command_commands(const char * value, const int8_t id, JsonObject output); static bool command_response(const char * value, const int8_t id, JsonObject output); static bool command_allvalues(const char * value, const int8_t id, JsonObject output); + static bool get_value_info(JsonObject root, const char *cmd); #if defined(EMSESP_TEST) static bool command_test(const char * value, const int8_t id); diff --git a/src/web/WebSchedulerService.cpp b/src/web/WebSchedulerService.cpp index 223c5cc17..dd331fcb7 100644 --- a/src/web/WebSchedulerService.cpp +++ b/src/web/WebSchedulerService.cpp @@ -369,7 +369,9 @@ bool WebSchedulerService::command(const char * cmd, const char * data) { bool WebSchedulerService::onChange(const char * cmd) { for (const ScheduleItem & scheduleItem : *scheduleItems_) { if (scheduleItem.active && scheduleItem.flags == SCHEDULEFLAG_SCHEDULE_ONCHANGE && Helpers::toLower(scheduleItem.time) == Helpers::toLower(cmd)) { +#ifdef EMESESP_DEBUG // emsesp::EMSESP::logger().debug(scheduleItem.cmd.c_str()); +#endif return command(scheduleItem.cmd.c_str(), compute(scheduleItem.value.c_str()).c_str()); } } @@ -381,7 +383,7 @@ void WebSchedulerService::condition() { if (scheduleItem.active && scheduleItem.flags == SCHEDULEFLAG_SCHEDULE_CONDITION) { auto match = compute(scheduleItem.time.c_str()); #ifdef EMESESP_DEBUG - emsesp::EMSESP::logger().debug("condition match: %s", match.c_str()); + // emsesp::EMSESP::logger().debug("condition match: %s", match.c_str()); #endif if (!match.empty() && match[0] == '1') { if (scheduleItem.retry_cnt == 0xFF) { // default unswitched diff --git a/src/web/shuntingYard.hpp b/src/web/shuntingYard.hpp index ef06f8bf2..10e2fa08e 100644 --- a/src/web/shuntingYard.hpp +++ b/src/web/shuntingYard.hpp @@ -303,16 +303,27 @@ std::deque shuntingYard(const std::deque & tokens) { return queue; } +bool isnum(const std::string & s) { + if (s.find_first_not_of("0123456789.") == std::string::npos || (s[0] == '-' && s.find_first_not_of("0123456789.", 1) == std::string::npos)) { + return true; + } + return false; +} + + // replace commands like "//" with its value" std::string commands(std::string & expr) { for (uint8_t device = 0; device < emsesp::EMSdevice::DeviceType::UNKNOWN; device++) { const char * d = emsesp::EMSdevice::device_type_2_device_name(device); auto f = expr.find(d); while (f != std::string::npos) { - auto e = expr.find_first_of(" )=<>|&+-*", f); + auto e = expr.find_first_of(")=<>|&+-*!", f); if (e == std::string::npos) { e = expr.length(); } + while (e > 0 && expr[e - 1] == ' ') { // remove blanks from end + e--; + } char cmd[COMMAND_MAX_LENGTH]; size_t l = e - f; if (l >= sizeof(cmd) - 1) { @@ -330,8 +341,7 @@ std::string commands(std::string & expr) { emsesp::Command::process(cmd_s.c_str(), true, input, output); if (output.containsKey("api_data")) { std::string data = output["api_data"].as(); - // set strings in quotations for something like "3-way-valve" - if (isdigit(data[0] == 0 && data[0] != '-')) { + if (!isnum(data)) { data.insert(data.begin(), '"'); data.insert(data.end(), '"'); } @@ -346,7 +356,7 @@ std::string commands(std::string & expr) { return expr; } -int islogic(const std::string & s) { +int to_logic(const std::string & s) { if (s[0] == '1' || s == "on" || s == "ON" || s == "true") { return 1; } @@ -356,15 +366,16 @@ int islogic(const std::string & s) { return 0; } -bool isnum(const std::string & s) { - if (isdigit(s[0]) || (s[0] == '-' && isdigit(s[1]))) { - return true; +std::string to_string(double d) { + if (d == static_cast(d)) { + return std::to_string(static_cast(d)); } - return false; + return std::to_string(d); } std::string compute(const std::string & expr) { - auto expr_new = emsesp::Helpers::toLower(expr); + auto expr_new = expr; //emsesp::Helpers::toLower(expr); + // emsesp::EMSESP::logger().info("calculate: %s", expr_new.c_str()); commands(expr_new); // emsesp::EMSESP::logger().info("calculate: %s", expr_new.c_str()); const auto tokens = exprToTokens(expr_new); @@ -399,10 +410,10 @@ std::string compute(const std::string & expr) { if (!isnum(rhs)) { return ""; } - stack.push_back(std::to_string(-1 * std::stod(rhs))); + stack.push_back(to_string(-1 * std::stod(rhs))); break; case '!': - stack.push_back(islogic(rhs) == 0 ? "1" : "0"); + stack.push_back(to_logic(rhs) == 0 ? "1" : "0"); break; } @@ -468,9 +479,9 @@ std::string compute(const std::string & expr) { if (stack.size() < 2) { return ""; } - const auto rhs = islogic(stack.back()); + const auto rhs = to_logic(stack.back()); stack.pop_back(); - const auto lhs = islogic(stack.back()); + const auto lhs = to_logic(stack.back()); stack.pop_back(); switch (token.str[0]) { default: @@ -502,22 +513,22 @@ std::string compute(const std::string & expr) { return ""; break; case '^': - stack.push_back(std::to_string(pow(lhs, rhs))); + stack.push_back(to_string(pow(lhs, rhs))); break; case '*': - stack.push_back(std::to_string(lhs * rhs)); + stack.push_back(to_string(lhs * rhs)); break; case '/': - stack.push_back(std::to_string(lhs / rhs)); + stack.push_back(to_string(lhs / rhs)); break; case '%': stack.push_back(std::to_string(static_cast(lhs) % static_cast(rhs))); break; case '+': - stack.push_back(std::to_string(lhs + rhs)); + stack.push_back(to_string(lhs + rhs)); break; case '-': - stack.push_back(std::to_string(lhs - rhs)); + stack.push_back(to_string(lhs - rhs)); break; } } break; From c40625d65867d5e0edeeb59e8c4c00f1e720948a Mon Sep 17 00:00:00 2001 From: MichaelDvP Date: Thu, 20 Jun 2024 15:46:08 +0200 Subject: [PATCH 5/7] Conditions, v3.7.0-test.15 --- src/version.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/version.h b/src/version.h index 00c9d1e6c..e40812cf4 100644 --- a/src/version.h +++ b/src/version.h @@ -1 +1 @@ -#define EMSESP_APP_VERSION "3.7.0-dev.15" +#define EMSESP_APP_VERSION "3.7.0-test.15" From 4a48e03552b43cc96425fcd478f9257596b4917c Mon Sep 17 00:00:00 2001 From: MichaelDvP Date: Thu, 20 Jun 2024 17:47:48 +0200 Subject: [PATCH 6/7] update testrelease --- .github/workflows/test_release.yml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test_release.yml b/.github/workflows/test_release.yml index db5d88b8b..8f1be2863 100644 --- a/.github/workflows/test_release.yml +++ b/.github/workflows/test_release.yml @@ -13,12 +13,15 @@ jobs: steps: - uses: actions/checkout@v4 + - name: Enable Corepack + run: corepack enable - uses: actions/setup-python@v5 with: python-version: '3.11' - - uses: actions/setup-node@v4 + - name: Use Node.js 20.x + uses: actions/setup-node@v4 with: - node-version: '20' + node-version: '20.x' - name: Get EMS-ESP source code and version id: build_info From 0fc62b216a13aae4baaf0de07b70053b25e1c8b1 Mon Sep 17 00:00:00 2001 From: MichaelDvP Date: Thu, 20 Jun 2024 22:08:47 +0200 Subject: [PATCH 7/7] merge conditions, 3.7.0-dev.17 --- src/version.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/version.h b/src/version.h index f084086e8..1d59dfab5 100644 --- a/src/version.h +++ b/src/version.h @@ -1 +1 @@ -#define EMSESP_APP_VERSION "3.7.0-test.16" +#define EMSESP_APP_VERSION "3.7.0-dev.17"