This commit is contained in:
MichaelDvP
2025-10-07 07:26:24 +02:00
43 changed files with 7927 additions and 7832 deletions

View File

@@ -809,7 +809,7 @@ bool AnalogSensor::get_value_info(JsonObject output, const char * cmd, const int
// match custom name or sensor GPIO
if (cmd == Helpers::toLower(sensor.name()) || Helpers::atoint(cmd) == sensor.gpio()) {
get_value_json(output, sensor);
return Command::set_attribute(output, cmd, attribute_s);
return Command::get_attribute(output, cmd, attribute_s);
}
}

View File

@@ -143,7 +143,8 @@ uint8_t Command::process(const char * path, const bool is_admin, const JsonObjec
}
// check if data is entity like device/hc/name/value
if (data.is<const char *>()) {
// unless the command is system/message
if ((strcmp(command_p, "message") != 0) && data.is<const char *>()) {
const char * d = data.as<const char *>();
if (strlen(d)) {
char * device_end = (char *)strchr(d, '/');
@@ -183,9 +184,9 @@ uint8_t Command::process(const char * path, const bool is_admin, const JsonObjec
}
}
// call the command based on the type
uint8_t return_code = CommandRet::OK;
// call the command based on the type
if (data.is<const char *>()) {
return_code = Command::call(device_type, command_p, data.as<const char *>(), is_admin, id_n, output);
} else if (data.is<int>()) {
@@ -289,8 +290,12 @@ const char * Command::parse_command_string(const char * command, int8_t & id) {
return command;
}
// check if command contains an attribute
// check if command string contains an attribute and returns it
const char * Command::get_attribute(const char * cmd) {
if (cmd == nullptr) {
return nullptr;
}
char * breakp = (char *)strchr(cmd, '/');
if (breakp) {
*breakp = '\0';
@@ -299,9 +304,9 @@ const char * Command::get_attribute(const char * cmd) {
return nullptr;
}
// returns the attribute in the given JSON object as a key/value pair called api_data
// get the attribute in the given JSON object as a key/value pair called api_data
// or errors if the attribute is not found
bool Command::set_attribute(JsonObject output, const char * cmd, const char * attribute) {
bool Command::get_attribute(JsonObject output, const char * cmd, const char * attribute) {
if (attribute == nullptr) {
return true;
}
@@ -325,7 +330,8 @@ bool Command::set_attribute(JsonObject output, const char * cmd, const char * at
char error[100];
snprintf(error, sizeof(error), "no attribute '%s' in %s", attribute, cmd);
output["message"] = error;
return false;
return false; // fail
}
// calls a command directly

View File

@@ -137,7 +137,7 @@ class Command {
static const char * parse_command_string(const char * command, int8_t & id);
static const char * get_attribute(const char * cmd);
static bool set_attribute(JsonObject output, const char * cmd, const char * attribute);
static bool get_attribute(JsonObject output, const char * cmd, const char * attribute);
static const char * return_code_string(const uint8_t return_code);

View File

@@ -1537,7 +1537,7 @@ bool EMSdevice::get_value_info(JsonObject output, const char * cmd, const int8_t
get_value_json(output, dv);
// if we're filtering on an attribute, go find it
// if we can't find it, maybe it exists but doesn't not have a value assigned yet
return Command::set_attribute(output, cmd_s, attribute_s);
return Command::get_attribute(output, cmd_s, attribute_s);
}
}
return false; // not found, but don't return a message error yet

View File

@@ -549,7 +549,7 @@ class EMSdevice {
std::vector<uint16_t> handlers_ignored_;
#if defined(EMSESP_STANDALONE) || defined(EMSESP_TEST)
public: // so we can call it from WebCustomizationService::test() and EMSESP::dump_all_entities()
public: // so we can call it from WebCustomizationService::load_test_data() and EMSESP::dump_all_entities()
#endif
std::vector<TelegramFunction> telegram_functions_; // each EMS device has its own set of registered telegram types
std::vector<DeviceValue> devicevalues_; // all the device values

View File

@@ -29,7 +29,7 @@ static_assert(uuid::console::thread_safe, "uuid-console must be thread-safe");
namespace emsesp {
// Static member definitions
std::deque<std::unique_ptr<EMSdevice>> EMSESP::emsdevices;
std::deque<std::unique_ptr<EMSdevice>> EMSESP::emsdevices{};
std::vector<EMSESP::Device_record> EMSESP::device_library_;
uuid::log::Logger EMSESP::logger_{F_(emsesp), uuid::log::Facility::KERN};
uint16_t EMSESP::watch_id_ = WATCH_ID_NONE;
@@ -1619,7 +1619,7 @@ void EMSESP::start() {
serial_console_.begin(SERIAL_CONSOLE_BAUD_RATE);
// always start a serial console if we're running standalone, except if we're running unit tests
#if defined(EMSESP_STANDALONE) || defined(EMSESP_DEBUG)
#if defined(EMSESP_STANDALONE) || defined(EMSESP_TEST) || defined(EMSESP_DEBUG)
#ifndef EMSESP_UNITY
start_serial_console();
#endif
@@ -1727,7 +1727,11 @@ void EMSESP::start() {
void EMSESP::start_serial_console() {
shell_ = std::make_shared<EMSESPConsole>(*this, serial_console_, true);
#if defined(EMSESP_STANDALONE)
shell_->maximum_log_messages(500);
#else
shell_->maximum_log_messages(100);
#endif
shell_->start();
#if defined(EMSESP_DEBUG)
shell_->log_level(uuid::log::Level::DEBUG);

View File

@@ -74,7 +74,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", "", "Hepsini MQTTye gönder", "pubblica tutto su MQTT", "zverejniť všetko na MQTT", "publikovat vše do MQTT") // TODO translate
MAKE_WORD_TRANSLATION(system_info_cmd, "show system info", "Zeige Systeminformationen", "toon systeemstatus", "visa systeminformation", "pokaż status systemu", "vis system status", "", "Sistem Durumunu Göster", "visualizza stati di sistema", "zobraziť stav systému", "zobrazit informace o systému") // TODO translate
MAKE_WORD_TRANSLATION(schedule_cmd, "enable schedule item", "Aktiviere Zeitplanelemente", "activeer tijdschema item", "aktivera schemalagt objekt", "aktywuj wybrany harmonogram", "", "", "program öğesini etkinleştir", "abilitare l'elemento programmato", "povoliť položku plánovania", "povolit položku plánování") // TODO translate
MAKE_WORD_TRANSLATION(entity_cmd, "set custom value on ems", "Sende eigene Entitäten zu EMS", "verstuur custom waarde naar EMS", "sätt ett eget värde i EMS", "wyślij własną wartość na EMS", "", "", "emp üzerinde özel değer ayarla", "imposta valori personalizzati su EMS", "nastaviť vlastnú hodnotu na ems", "nastavit vlastní hodnotu na ems") // TODO translate
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ść", "", "", "emp üzerinde özel değer ayarla", "imposta valori personalizzati", "nastaviť vlastnú hodnotu", "nastavit vlastní hodnotu") // TODO translate
MAKE_WORD_TRANSLATION(commands_response, "get response", "Hole Antwort", "Verzoek om antwoord", "hämta svar", "uzyskaj odpowiedź", "", "", "gelen cevap", "", "získať odpoveď", "získat odpověď") // TODO translate
MAKE_WORD_TRANSLATION(coldshot_cmd, "send a cold shot of water", "Zugabe einer Menge kalten Wassers", "", "sckicka en liten mängd kallvatten", "uruchom tryśnięcie zimnej wody", "", "", "soğuk su gönder", "", "pošlite studenú dávku vody", "poslat studenou vodu") // TODO translate
MAKE_WORD_TRANSLATION(message_cmd, "send a message", "Eine Nachricht senden", "", "skicka ett meddelande", "", "", "", "", "", "poslať správu", "odeslat zprávu") // TODO translate

View File

@@ -125,13 +125,13 @@ void Mqtt::resubscribe() {
// Main MQTT loop - sends out top item on publish queue
void Mqtt::loop() {
queuecount_ = mqttClient_->queueSize();
// exit if MQTT is not enabled or if there is no network connection
if (!connected()) {
return;
}
queuecount_ = mqttClient_->queueSize();
uint32_t currentMillis = uuid::get_uptime();
// send heartbeat

View File

@@ -17,37 +17,10 @@
// copy from https://gist.github.com/t-mat/b9f681b7591cdae712f6
// modified MDvP, 06.2024
//
#include <string>
#include <vector>
#include <deque>
#include <math.h>
class Token {
public:
enum class Type : uint8_t {
Unknown,
Number,
String,
Operator,
Compare,
Logic,
Unary,
LeftParen,
RightParen,
};
#include "emsesp.h"
Token(Type type, const std::string & s, int8_t precedence = -1, bool rightAssociative = false)
: type{type}
, str(s)
, precedence{precedence}
, rightAssociative{rightAssociative} {
}
const Type type;
const std::string str;
const int8_t precedence;
const bool rightAssociative;
};
#include "shuntingYard.h"
// find tokens
std::deque<Token> exprToTokens(const std::string & expr) {
@@ -362,7 +335,7 @@ bool isnum(const std::string & s) {
// replace commands like "<device>/<hc>/<cmd>" with its value"
std::string commands(std::string & expr, bool quotes = true) {
std::string commands(std::string & expr, bool quotes) {
auto expr_new = emsesp::Helpers::toLower(expr);
for (uint8_t device = 0; device < emsesp::EMSdevice::DeviceType::UNKNOWN; device++) {
std::string d = (std::string)emsesp::EMSdevice::device_type_2_device_name(device) + "/";
@@ -380,6 +353,7 @@ std::string commands(std::string & expr, bool quotes = true) {
}
expr_new.copy(cmd, l, f);
cmd[l] = '\0';
if (strstr(cmd, "/value") == nullptr) {
strlcat(cmd, "/value", sizeof(cmd) - 6);
}
@@ -458,23 +432,21 @@ std::string calculate(const std::string & expr) {
// commands(expr_new);
const auto tokens = exprToTokens(expr_new);
// for debugging only
// for (const auto & t : tokens) {
// emsesp::EMSESP::logger().debug("shunt token: %s(%d)", t.str.c_str(), t.type);
// Serial.printf("shunt token: %s(%d)\n", t.str.c_str(), t.type);
// Serial.println();
// }
if (tokens.empty()) {
return "";
}
auto queue = shuntingYard(tokens);
if (queue.empty()) {
return "";
}
/*
// debug only print tokens
#ifdef EMSESP_STANDALONE
for (const auto & t : queue) {
emsesp::EMSESP::logger().debug("shunt token: %s(%d)", t.str.c_str(), t.type);
}
#endif
*/
std::vector<std::string> stack;
while (!queue.empty()) {

86
src/core/shuntingYard.h Normal file
View File

@@ -0,0 +1,86 @@
// 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:
// This code 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
//
#ifndef EMSESP_SHUNTING_YARD_H_
#define EMSESP_SHUNTING_YARD_H_
#include <HTTPClient.h>
#include <ArduinoJson.h>
#include <string>
#include <vector>
#include <deque>
#include <math.h>
class Token {
public:
enum class Type : uint8_t {
Unknown,
Number,
String,
Operator,
Compare,
Logic,
Unary,
LeftParen,
RightParen,
};
Token(Type type, const std::string & s, int8_t precedence = -1, bool rightAssociative = false)
: type{type}
, str(s)
, precedence{precedence}
, rightAssociative{rightAssociative} {
}
const Type type;
const std::string str;
const int8_t precedence;
const bool rightAssociative;
};
// find tokens
std::deque<Token> exprToTokens(const std::string & expr);
// sort tokens to RPN form
std::deque<Token> shuntingYard(const std::deque<Token> & tokens);
// check if string is a number
bool isnum(const std::string & s);
// replace commands like "<device>/<hc>/<cmd>" with its value"
std::string commands(std::string & expr, bool quotes = true);
// checks for logic value
int to_logic(const std::string & s);
// number to string, remove trailing zeros
std::string to_string(double d);
// number to hex string
std::string to_hex(uint32_t i);
// RPN calculator
std::string calculate(const std::string & expr);
// check for multiple instances of <cond> ? <expr1> : <expr2>
std::string compute(const std::string & expr);
#endif

View File

@@ -19,6 +19,8 @@
#include "system.h"
#include "emsesp.h" // for send_raw_telegram() command
#include "shuntingYard.h"
#ifndef EMSESP_STANDALONE
#include "esp_ota_ops.h"
#endif
@@ -48,8 +50,6 @@
#include <esp_mac.h>
#endif
#include <HTTPClient.h>
#ifndef EMSESP_STANDALONE
#include "esp_efuse.h"
#endif
@@ -205,14 +205,23 @@ bool System::command_syslog_level(const char * value, const int8_t id) {
}
*/
// send message - to log and MQTT
bool System::command_message(const char * value, const int8_t id) {
// send message - to system log and MQTT
bool System::command_message(const char * value, const int8_t id, JsonObject output) {
if (value == nullptr || value[0] == '\0') {
LOG_WARNING("Message is empty");
return false; // must have a string value
}
LOG_INFO("Message: %s", value);
Mqtt::queue_publish(F_(message), value);
auto computed_value = compute(value); // process the message via Shunting Yard
if (computed_value.length() == 0) {
LOG_WARNING("Message result is empty");
return false;
}
LOG_INFO("Message: %s", computed_value.c_str()); // send to log
Mqtt::queue_publish(F_(message), computed_value); // send to MQTT if enabled
output["api_data"] = computed_value; // send to API
return true;
}
@@ -831,9 +840,9 @@ void System::system_check() {
// everything is healthy, show LED permanently on or off depending on setting
if (led_gpio_) {
#if ESP_ARDUINO_VERSION_MAJOR < 3
led_type_ ? neopixelWrite(led_gpio_, 0, hide_led_ ? 0 : 128, 0) : digitalWrite(led_gpio_, hide_led_ ? !LED_ON : LED_ON);
led_type_ ? neopixelWrite(led_gpio_, 0, hide_led_ ? 0 : RGB_LED_BRIGHTNESS, 0) : digitalWrite(led_gpio_, hide_led_ ? !LED_ON : LED_ON);
#else
led_type_ ? rgbLedWrite(led_gpio_, 0, hide_led_ ? 0 : 128, 0) : digitalWrite(led_gpio_, hide_led_ ? !LED_ON : LED_ON);
led_type_ ? rgbLedWrite(led_gpio_, 0, hide_led_ ? 0 : RGB_LED_BRIGHTNESS, 0) : digitalWrite(led_gpio_, hide_led_ ? !LED_ON : LED_ON);
#endif
}
} else {
@@ -913,36 +922,35 @@ void System::led_monitor() {
// 1 flash is the EMS bus is not connected
// 2 flashes if the network (wifi or ethernet) is not connected
// 3 flashes is both the bus and the network are not connected. Then you know you're truly f*cked.
if (led_type_) {
if (led_flash_step_ == 3) {
if ((healthcheck_ & HEALTHCHECK_NO_NETWORK) == HEALTHCHECK_NO_NETWORK) {
#if ESP_ARDUINO_VERSION_MAJOR < 3
neopixelWrite(led_gpio_, 128, 0, 0); // red
neopixelWrite(led_gpio_, RGB_LED_BRIGHTNESS, 0, 0); // red
#else
rgbLedWrite(led_gpio_, 128, 0, 0); // red
rgbLedWrite(led_gpio_, RGB_LED_BRIGHTNESS, 0, 0); // red
#endif
} else if ((healthcheck_ & HEALTHCHECK_NO_BUS) == HEALTHCHECK_NO_BUS) {
#if ESP_ARDUINO_VERSION_MAJOR < 3
neopixelWrite(led_gpio_, 0, 0, 128); // blue
neopixelWrite(led_gpio_, 0, 0, RGB_LED_BRIGHTNESS); // blue
#else
rgbLedWrite(led_gpio_, 0, 0, 128); // blue
rgbLedWrite(led_gpio_, 0, 0, RGB_LED_BRIGHTNESS); // blue
#endif
}
}
if (led_flash_step_ == 5 && (healthcheck_ & HEALTHCHECK_NO_NETWORK) == HEALTHCHECK_NO_NETWORK) {
#if ESP_ARDUINO_VERSION_MAJOR < 3
neopixelWrite(led_gpio_, 128, 0, 0); // red
neopixelWrite(led_gpio_, RGB_LED_BRIGHTNESS, 0, 0); // red
#else
rgbLedWrite(led_gpio_, 128, 0, 0); // red
rgbLedWrite(led_gpio_, RGB_LED_BRIGHTNESS, 0, 0); // red
#endif
}
if ((led_flash_step_ == 7) && ((healthcheck_ & HEALTHCHECK_NO_NETWORK) == HEALTHCHECK_NO_NETWORK)
&& ((healthcheck_ & HEALTHCHECK_NO_BUS) == HEALTHCHECK_NO_BUS)) {
#if ESP_ARDUINO_VERSION_MAJOR < 3
neopixelWrite(led_gpio_, 0, 0, 128); // blue
neopixelWrite(led_gpio_, 0, 0, RGB_LED_BRIGHTNESS); // blue
#else
rgbLedWrite(led_gpio_, 0, 0, 128); // blue
rgbLedWrite(led_gpio_, 0, 0, RGB_LED_BRIGHTNESS); // blue
#endif
}
} else {
@@ -1433,7 +1441,6 @@ bool System::command_service(const char * cmd, const char * value) {
if (!strcmp(cmd, "fuse/mfg")) {
ok = esp_efuse_write_reg(EFUSE_BLK3, 0, (uint32_t)n) == ESP_OK;
ok ? LOG_INFO("fuse programed with value '%X': successful", n) : LOG_ERROR("fuse programed with value '%X': failed", n);
}
if (!strcmp(cmd, "fuse/mfgadd")) {
uint8_t reg = 0;
@@ -1521,7 +1528,7 @@ bool System::get_value_info(JsonObject output, const char * cmd) {
return true;
}
}
} // else skipt, but we don't have value pairs in system root
} // else skip, but we don't have value pairs in system root
}
}
return false;
@@ -1569,11 +1576,11 @@ bool System::command_info(const char * value, const int8_t id, JsonObject output
node["sdk"] = ESP.getSdkVersion();
node["freeMem"] = getHeapMem();
node["maxAlloc"] = getMaxAllocMem();
node["freeCaps"] = heap_caps_get_free_size(MALLOC_CAP_8BIT) / 1024; // includes heap and psram
node["usedApp"] = EMSESP::system_.appUsed(); // kilobytes
node["freeApp"] = EMSESP::system_.appFree(); // kilobytes
node["partition"] = (const char *)esp_ota_get_running_partition()->label; // active partition
node["flash_chip_size"] = ESP.getFlashChipSize() / 1024; // kilobytes
node["freeCaps"] = heap_caps_get_free_size(MALLOC_CAP_8BIT) / 1024; // includes heap and psram
node["usedApp"] = EMSESP::system_.appUsed(); // kilobytes
node["freeApp"] = EMSESP::system_.appFree(); // kilobytes
node["partition"] = (const char *)esp_ota_get_running_partition()->label; // active partition
node["flash_chip_size"] = ESP.getFlashChipSize() / 1024; // kilobytes
#endif
node["resetReason"] = EMSESP::system_.reset_reason(0) + " / " + EMSESP::system_.reset_reason(1);
#ifndef EMSESP_STANDALONE
@@ -1842,6 +1849,7 @@ bool System::command_info(const char * value, const int8_t id, JsonObject output
}
}
}
// Also show EMSESP devices if we have any
if (EMSESP::temperaturesensor_.count_entities()) {
JsonObject obj = devices.add<JsonObject>();

View File

@@ -55,6 +55,8 @@ using uuid::console::Shell;
#define EMSESP_CUSTOMSUPPORT_FILE "/config/customSupport.json"
#define RGB_LED_BRIGHTNESS 20
namespace emsesp {
enum PHY_type : uint8_t { PHY_TYPE_NONE = 0, PHY_TYPE_LAN8720, PHY_TYPE_TLK110 };
@@ -83,7 +85,7 @@ class System {
static bool command_restart(const char * value, const int8_t id);
static bool command_format(const char * value, const int8_t id);
static bool command_watch(const char * value, const int8_t id);
static bool command_message(const char * value, const int8_t id);
static bool command_message(const char * value, const int8_t id, JsonObject output);
static bool command_info(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_service(const char * cmd, const char * value);

View File

@@ -399,7 +399,7 @@ bool TemperatureSensor::get_value_info(JsonObject output, const char * cmd, cons
// match custom name or sensor ID
if (cmd == Helpers::toLower(sensor.name()) || cmd == Helpers::toLower(sensor.id())) {
get_value_json(output, sensor);
return Command::set_attribute(output, cmd, attribute_s);
return Command::get_attribute(output, cmd, attribute_s);
}
}
@@ -618,7 +618,9 @@ bool TemperatureSensor::Sensor::apply_customization() {
// hard coded tests
#if defined(EMSESP_TEST)
void TemperatureSensor::test() {
void TemperatureSensor::load_test_data() {
sensors_.clear(); // delete all existing sensors
// add 2 temperature sensors
// Sensor ID: 01_0203_0405_0607
uint8_t addr[ADDR_LEN] = {1, 2, 3, 4, 5, 6, 7, 8};

View File

@@ -113,7 +113,7 @@ class TemperatureSensor {
bool update(const std::string & id, const std::string & name, int16_t offset);
#if defined(EMSESP_TEST)
void test();
void load_test_data();
#endif
private: