From 5aadf7234f0785fd7307e8d93fe528768619eeb5 Mon Sep 17 00:00:00 2001 From: proddy Date: Sat, 13 Jun 2026 09:19:15 +0200 Subject: [PATCH 1/3] Reduce DeviceValue heap usage with on-demand custom name Store the per-entity custom_fullname as a heap-allocated string that is only created when a custom name is actually set, instead of an inline std::string on every DeviceValue. Saves ~20 bytes per entity (hundreds to ~1000 entities), reclaiming internal heap on no-PSRAM boards. Co-authored-by: Cursor --- src/core/emsdevice.cpp | 10 +++--- src/core/emsdevicevalue.cpp | 54 +++++++++++++++++++++-------- src/core/emsdevicevalue.h | 15 +++++++- src/web/WebCustomizationService.cpp | 4 +-- 4 files changed, 60 insertions(+), 23 deletions(-) diff --git a/src/core/emsdevice.cpp b/src/core/emsdevice.cpp index 570cb4470..7ec42f8fd 100644 --- a/src/core/emsdevice.cpp +++ b/src/core/emsdevice.cpp @@ -1271,9 +1271,9 @@ void EMSdevice::setCustomizationEntity(const std::string & entity_id) { // set the custom name if it has one, or clear it if (has_custom_name) { - dv.custom_fullname = entity_id.substr(custom_name_pos + 1); + dv.set_custom_fullname(entity_id.substr(custom_name_pos + 1)); } else { - dv.custom_fullname = ""; + dv.set_custom_fullname(""); } auto min = dv.min; @@ -1312,11 +1312,11 @@ void EMSdevice::getCustomizationEntities(std::vector & entity_ids) break; } } - if (!is_set && (mask || !dv.custom_fullname.empty())) { - if (dv.custom_fullname.empty()) { + if (!is_set && (mask || dv.has_custom_fullname())) { + if (!dv.has_custom_fullname()) { entity_ids.push_back(Helpers::hextoa(mask, false) + entity_name); } else { - entity_ids.push_back(Helpers::hextoa(mask, false) + entity_name + "|" + dv.custom_fullname); + entity_ids.push_back(Helpers::hextoa(mask, false) + entity_name + "|" + dv.custom_fullname()); } } } diff --git a/src/core/emsdevicevalue.cpp b/src/core/emsdevicevalue.cpp index c085bd94b..9a4f1cf89 100644 --- a/src/core/emsdevicevalue.cpp +++ b/src/core/emsdevicevalue.cpp @@ -47,7 +47,6 @@ DeviceValue::DeviceValue(uint8_t device_type, , numeric_operator(numeric_operator) , short_name(short_name) , fullname(fullname) - , custom_fullname(custom_fullname) , uom(uom) , has_cmd(has_cmd) , min(min) @@ -60,7 +59,12 @@ DeviceValue::DeviceValue(uint8_t device_type, options_size = Helpers::count_items(options); } - // set the min/max + // store the custom name on the heap, but only if one was actually provided + if (!custom_fullname.empty()) { + custom_fullname_ = std::make_unique(custom_fullname); + } + + // set the min/max (reads back the custom name set above) set_custom_minmax(); /* @@ -343,11 +347,12 @@ bool DeviceValue::get_min_max(int16_t & dv_set_min, uint32_t & dv_set_max) { // extract custom min from custom_fullname bool DeviceValue::get_custom_min(int16_t & val) { - auto min_pos = custom_fullname.find('>'); - bool has_min = (min_pos != std::string::npos); + const auto & cf = custom_fullname(); + auto min_pos = cf.find('>'); + bool has_min = (min_pos != std::string::npos); uint8_t fahrenheit = !EMSESP::system_.fahrenheit() ? 0 : (uom == DeviceValueUOM::DEGREES) ? 2 : (uom == DeviceValueUOM::DEGREES_R) ? 1 : 0; if (has_min) { - int32_t v = Helpers::atoint(custom_fullname.substr(min_pos + 1).c_str()); + int32_t v = Helpers::atoint(cf.substr(min_pos + 1).c_str()); if (fahrenheit) { v = (v - (32 * (fahrenheit - 1))) / 1.8; // reset to °C } @@ -361,11 +366,12 @@ bool DeviceValue::get_custom_min(int16_t & val) { // extract custom max from custom_fullname bool DeviceValue::get_custom_max(uint32_t & val) { - auto max_pos = custom_fullname.find('<'); - bool has_max = (max_pos != std::string::npos); + const auto & cf = custom_fullname(); + auto max_pos = cf.find('<'); + bool has_max = (max_pos != std::string::npos); uint8_t fahrenheit = !EMSESP::system_.fahrenheit() ? 0 : (uom == DeviceValueUOM::DEGREES) ? 2 : (uom == DeviceValueUOM::DEGREES_R) ? 1 : 0; if (has_max) { - int32_t v = Helpers::atoint(custom_fullname.substr(max_pos + 1).c_str()); + int32_t v = Helpers::atoint(cf.substr(max_pos + 1).c_str()); if (fahrenheit) { v = (v - (32 * (fahrenheit - 1))) / 1.8; // reset to °C } @@ -383,14 +389,32 @@ void DeviceValue::set_custom_minmax() { get_custom_max(max); } -std::string DeviceValue::get_custom_fullname() const { - auto min_pos = custom_fullname.find('>'); - auto max_pos = custom_fullname.find('<'); - auto minmax_pos = min_pos < max_pos ? min_pos : max_pos; - if (minmax_pos != std::string::npos) { - return custom_fullname.substr(0, minmax_pos); +// raw stored custom name (empty string if none was set) +const std::string & DeviceValue::custom_fullname() const { + static const std::string empty_string; + return custom_fullname_ ? *custom_fullname_ : empty_string; +} + +// set or clear the custom name, only allocating heap when there's actually a name +void DeviceValue::set_custom_fullname(const std::string & name) { + if (name.empty()) { + custom_fullname_.reset(); + } else if (custom_fullname_) { + *custom_fullname_ = name; + } else { + custom_fullname_ = std::make_unique(name); } - return custom_fullname; +} + +std::string DeviceValue::get_custom_fullname() const { + const auto & cf = custom_fullname(); + auto min_pos = cf.find('>'); + auto max_pos = cf.find('<'); + auto minmax_pos = min_pos < max_pos ? min_pos : max_pos; + if (minmax_pos != std::string::npos) { + return cf.substr(0, minmax_pos); + } + return cf; } // returns the translated fullname or the custom fullname (if provided) diff --git a/src/core/emsdevicevalue.h b/src/core/emsdevicevalue.h index 83763d947..a7d62d8d1 100644 --- a/src/core/emsdevicevalue.h +++ b/src/core/emsdevicevalue.h @@ -23,6 +23,8 @@ #include #include +#include + #include "helpers.h" // for conversions #include "default_settings.h" // for enum types @@ -177,7 +179,6 @@ class DeviceValue { int8_t numeric_operator; const char * const short_name; // used in MQTT and API const char * const * fullname; // used in Web and Console, is translated - std::string custom_fullname; // optional, from customization uint8_t uom; // DeviceValueUOM::* bool has_cmd; // true if there is a Console/MQTT command which matches the short_name int16_t min; // min range @@ -214,6 +215,13 @@ class DeviceValue { std::string get_fullname() const; static std::string get_name(const std::string & entity); + // raw stored custom name (including any >min custom_fullname_; }; }; // namespace emsesp diff --git a/src/web/WebCustomizationService.cpp b/src/web/WebCustomizationService.cpp index 537a7ba09..f7d9d41fb 100644 --- a/src/web/WebCustomizationService.cpp +++ b/src/web/WebCustomizationService.cpp @@ -491,8 +491,8 @@ void WebCustomizationService::load_test_data() { // find the device value and set the mask and custom name to match the above fake data for (auto & dv : emsdevice->devicevalues_) { if (strcmp(dv.short_name, "heatingactive") == 0) { - dv.state = DeviceValueState::DV_FAVORITE; // set as favorite - dv.custom_fullname = "is my heating on?"; + dv.state = DeviceValueState::DV_FAVORITE; // set as favorite + dv.set_custom_fullname("is my heating on?"); } else if (strcmp(dv.short_name, "tapwateractive") == 0) { dv.state = DeviceValueState::DV_FAVORITE; // set as favorite } else if (strcmp(dv.short_name, "selflowtemp") == 0) { From eceb097cc7b6801e9041bb3ebcf9b1874bc47e41 Mon Sep 17 00:00:00 2001 From: proddy Date: Sun, 28 Jun 2026 16:08:31 +0200 Subject: [PATCH 2/3] fix thermostat date/time for HT3 --- CHANGELOG_LATEST.md | 3 ++- src/devices/thermostat.cpp | 5 ++++- src/emsesp_version.h | 2 +- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/CHANGELOG_LATEST.md b/CHANGELOG_LATEST.md index f47f825d9..991434ef8 100644 --- a/CHANGELOG_LATEST.md +++ b/CHANGELOG_LATEST.md @@ -13,7 +13,8 @@ For more details go to [emsesp.org](https://emsesp.org/). - signed value for solarInfuence [#3077](https://github.com/emsesp/EMS-ESP32/issues/3077) - set bin file upload limit to 1M again [3086](https://github.com/emsesp/EMS-ESP32/issues/3086) - check arithmetric operations on strings [#3127](https://github.com/emsesp/EMS-ESP32/discussions/3127) -- Fix: Prevent system crash during WiFi network discovery on ESP32-S3 hardware [#3128](https://github.com/emsesp/EMS-ESP32/pull/3128) +- prevent system crash during WiFi network discovery on ESP32-S3 hardware [#3128](https://github.com/emsesp/EMS-ESP32/pull/3128) +- fix setting date/time on Junkers thermostats ## Changed diff --git a/src/devices/thermostat.cpp b/src/devices/thermostat.cpp index 8f5441cc3..3cbfec93d 100644 --- a/src/devices/thermostat.cpp +++ b/src/devices/thermostat.cpp @@ -3070,7 +3070,6 @@ bool Thermostat::set_datetime(const char * value, const int8_t id) { data[6] = (tm_->tm_wday + 6) % 7; // Bosch counts from Mo, time from Su data[7] = (id == 0) ? 2 : tm_->tm_isdst + 2; // set DST and flag for ext. clock if (model() == EMSdevice::EMS_DEVICE_FLAG_JUNKERS) { - data[6]++; // Junkers use 1-7; data[7] = 0; } } else if (dt.length() == 23) { @@ -3094,6 +3093,10 @@ bool Thermostat::set_datetime(const char * value, const int8_t id) { return false; } + if (model() == EMSdevice::EMS_DEVICE_FLAG_JUNKERS) { + data[6]++; // Junkers use 1-7 for day of the week + } + // LOG_INFO("Setting date and time: %02d.%02d.2%03d-%02d:%02d:%02d-%d-%d", data[3], data[1], data[0], data[2], data[4], data[5], data[6], data[7]); write_command(EMS_TYPE_time, 0, data, 8, EMS_TYPE_time); diff --git a/src/emsesp_version.h b/src/emsesp_version.h index 74e1e397b..3928edd22 100644 --- a/src/emsesp_version.h +++ b/src/emsesp_version.h @@ -1 +1 @@ -#define EMSESP_APP_VERSION "3.8.3-dev.7" +#define EMSESP_APP_VERSION "3.8.3-dev.8" From 55a0e4b2b5933c547d04ec046a98c0760eeece57 Mon Sep 17 00:00:00 2001 From: proddy Date: Sun, 28 Jun 2026 16:30:24 +0200 Subject: [PATCH 3/3] Revert "Reduce DeviceValue heap usage with on-demand custom name" This reverts commit 5aadf7234f0785fd7307e8d93fe528768619eeb5. --- src/core/emsdevice.cpp | 10 +++--- src/core/emsdevicevalue.cpp | 50 ++++++++--------------------- src/core/emsdevicevalue.h | 15 +-------- src/web/WebCustomizationService.cpp | 4 +-- 4 files changed, 21 insertions(+), 58 deletions(-) diff --git a/src/core/emsdevice.cpp b/src/core/emsdevice.cpp index 7ec42f8fd..570cb4470 100644 --- a/src/core/emsdevice.cpp +++ b/src/core/emsdevice.cpp @@ -1271,9 +1271,9 @@ void EMSdevice::setCustomizationEntity(const std::string & entity_id) { // set the custom name if it has one, or clear it if (has_custom_name) { - dv.set_custom_fullname(entity_id.substr(custom_name_pos + 1)); + dv.custom_fullname = entity_id.substr(custom_name_pos + 1); } else { - dv.set_custom_fullname(""); + dv.custom_fullname = ""; } auto min = dv.min; @@ -1312,11 +1312,11 @@ void EMSdevice::getCustomizationEntities(std::vector & entity_ids) break; } } - if (!is_set && (mask || dv.has_custom_fullname())) { - if (!dv.has_custom_fullname()) { + if (!is_set && (mask || !dv.custom_fullname.empty())) { + if (dv.custom_fullname.empty()) { entity_ids.push_back(Helpers::hextoa(mask, false) + entity_name); } else { - entity_ids.push_back(Helpers::hextoa(mask, false) + entity_name + "|" + dv.custom_fullname()); + entity_ids.push_back(Helpers::hextoa(mask, false) + entity_name + "|" + dv.custom_fullname); } } } diff --git a/src/core/emsdevicevalue.cpp b/src/core/emsdevicevalue.cpp index 9a4f1cf89..c085bd94b 100644 --- a/src/core/emsdevicevalue.cpp +++ b/src/core/emsdevicevalue.cpp @@ -47,6 +47,7 @@ DeviceValue::DeviceValue(uint8_t device_type, , numeric_operator(numeric_operator) , short_name(short_name) , fullname(fullname) + , custom_fullname(custom_fullname) , uom(uom) , has_cmd(has_cmd) , min(min) @@ -59,12 +60,7 @@ DeviceValue::DeviceValue(uint8_t device_type, options_size = Helpers::count_items(options); } - // store the custom name on the heap, but only if one was actually provided - if (!custom_fullname.empty()) { - custom_fullname_ = std::make_unique(custom_fullname); - } - - // set the min/max (reads back the custom name set above) + // set the min/max set_custom_minmax(); /* @@ -347,12 +343,11 @@ bool DeviceValue::get_min_max(int16_t & dv_set_min, uint32_t & dv_set_max) { // extract custom min from custom_fullname bool DeviceValue::get_custom_min(int16_t & val) { - const auto & cf = custom_fullname(); - auto min_pos = cf.find('>'); - bool has_min = (min_pos != std::string::npos); + auto min_pos = custom_fullname.find('>'); + bool has_min = (min_pos != std::string::npos); uint8_t fahrenheit = !EMSESP::system_.fahrenheit() ? 0 : (uom == DeviceValueUOM::DEGREES) ? 2 : (uom == DeviceValueUOM::DEGREES_R) ? 1 : 0; if (has_min) { - int32_t v = Helpers::atoint(cf.substr(min_pos + 1).c_str()); + int32_t v = Helpers::atoint(custom_fullname.substr(min_pos + 1).c_str()); if (fahrenheit) { v = (v - (32 * (fahrenheit - 1))) / 1.8; // reset to °C } @@ -366,12 +361,11 @@ bool DeviceValue::get_custom_min(int16_t & val) { // extract custom max from custom_fullname bool DeviceValue::get_custom_max(uint32_t & val) { - const auto & cf = custom_fullname(); - auto max_pos = cf.find('<'); - bool has_max = (max_pos != std::string::npos); + auto max_pos = custom_fullname.find('<'); + bool has_max = (max_pos != std::string::npos); uint8_t fahrenheit = !EMSESP::system_.fahrenheit() ? 0 : (uom == DeviceValueUOM::DEGREES) ? 2 : (uom == DeviceValueUOM::DEGREES_R) ? 1 : 0; if (has_max) { - int32_t v = Helpers::atoint(cf.substr(max_pos + 1).c_str()); + int32_t v = Helpers::atoint(custom_fullname.substr(max_pos + 1).c_str()); if (fahrenheit) { v = (v - (32 * (fahrenheit - 1))) / 1.8; // reset to °C } @@ -389,32 +383,14 @@ void DeviceValue::set_custom_minmax() { get_custom_max(max); } -// raw stored custom name (empty string if none was set) -const std::string & DeviceValue::custom_fullname() const { - static const std::string empty_string; - return custom_fullname_ ? *custom_fullname_ : empty_string; -} - -// set or clear the custom name, only allocating heap when there's actually a name -void DeviceValue::set_custom_fullname(const std::string & name) { - if (name.empty()) { - custom_fullname_.reset(); - } else if (custom_fullname_) { - *custom_fullname_ = name; - } else { - custom_fullname_ = std::make_unique(name); - } -} - std::string DeviceValue::get_custom_fullname() const { - const auto & cf = custom_fullname(); - auto min_pos = cf.find('>'); - auto max_pos = cf.find('<'); - auto minmax_pos = min_pos < max_pos ? min_pos : max_pos; + auto min_pos = custom_fullname.find('>'); + auto max_pos = custom_fullname.find('<'); + auto minmax_pos = min_pos < max_pos ? min_pos : max_pos; if (minmax_pos != std::string::npos) { - return cf.substr(0, minmax_pos); + return custom_fullname.substr(0, minmax_pos); } - return cf; + return custom_fullname; } // returns the translated fullname or the custom fullname (if provided) diff --git a/src/core/emsdevicevalue.h b/src/core/emsdevicevalue.h index a7d62d8d1..83763d947 100644 --- a/src/core/emsdevicevalue.h +++ b/src/core/emsdevicevalue.h @@ -23,8 +23,6 @@ #include #include -#include - #include "helpers.h" // for conversions #include "default_settings.h" // for enum types @@ -179,6 +177,7 @@ class DeviceValue { int8_t numeric_operator; const char * const short_name; // used in MQTT and API const char * const * fullname; // used in Web and Console, is translated + std::string custom_fullname; // optional, from customization uint8_t uom; // DeviceValueUOM::* bool has_cmd; // true if there is a Console/MQTT command which matches the short_name int16_t min; // min range @@ -215,13 +214,6 @@ class DeviceValue { std::string get_fullname() const; static std::string get_name(const std::string & entity); - // raw stored custom name (including any >min custom_fullname_; }; }; // namespace emsesp diff --git a/src/web/WebCustomizationService.cpp b/src/web/WebCustomizationService.cpp index f7d9d41fb..537a7ba09 100644 --- a/src/web/WebCustomizationService.cpp +++ b/src/web/WebCustomizationService.cpp @@ -491,8 +491,8 @@ void WebCustomizationService::load_test_data() { // find the device value and set the mask and custom name to match the above fake data for (auto & dv : emsdevice->devicevalues_) { if (strcmp(dv.short_name, "heatingactive") == 0) { - dv.state = DeviceValueState::DV_FAVORITE; // set as favorite - dv.set_custom_fullname("is my heating on?"); + dv.state = DeviceValueState::DV_FAVORITE; // set as favorite + dv.custom_fullname = "is my heating on?"; } else if (strcmp(dv.short_name, "tapwateractive") == 0) { dv.state = DeviceValueState::DV_FAVORITE; // set as favorite } else if (strcmp(dv.short_name, "selflowtemp") == 0) {