Compare commits

..

4 Commits

Author SHA1 Message Date
J. Nick Koston
48e7e7aeb3 hdr 2026-01-18 23:18:38 -10:00
J. Nick Koston
54a4d60f5d [datetime] Add const char * overloads for string parsing to avoid heap allocation 2026-01-18 23:09:24 -10:00
J. Nick Koston
d41980d0d2 [datetime] Add const char * overloads for string parsing to avoid heap allocation 2026-01-18 23:06:17 -10:00
J. Nick Koston
86a1b4cf69 [select][fan] Use StringRef for on_value/on_preset_set triggers to avoid heap allocation (#13324) 2026-01-18 19:51:11 -10:00
18 changed files with 383 additions and 133 deletions

View File

@@ -69,6 +69,7 @@ from esphome.cpp_types import ( # noqa: F401
JsonObjectConst,
Parented,
PollingComponent,
StringRef,
arduino_json_ns,
bool_,
const_char_ptr,

View File

@@ -106,9 +106,9 @@ DateCall &DateCall::set_date(uint16_t year, uint8_t month, uint8_t day) {
DateCall &DateCall::set_date(ESPTime time) { return this->set_date(time.year, time.month, time.day_of_month); };
DateCall &DateCall::set_date(const std::string &date) {
DateCall &DateCall::set_date(const char *date, size_t len) {
ESPTime val{};
if (!ESPTime::strptime(date, val)) {
if (!ESPTime::strptime(date, len, val)) {
ESP_LOGE(TAG, "Could not convert the date string to an ESPTime object");
return *this;
}

View File

@@ -67,7 +67,9 @@ class DateCall {
void perform();
DateCall &set_date(uint16_t year, uint8_t month, uint8_t day);
DateCall &set_date(ESPTime time);
DateCall &set_date(const std::string &date);
DateCall &set_date(const char *date, size_t len);
DateCall &set_date(const char *date) { return this->set_date(date, strlen(date)); }
DateCall &set_date(const std::string &date) { return this->set_date(date.c_str(), date.size()); }
DateCall &set_year(uint16_t year) {
this->year_ = year;

View File

@@ -163,9 +163,9 @@ DateTimeCall &DateTimeCall::set_datetime(ESPTime datetime) {
datetime.second);
};
DateTimeCall &DateTimeCall::set_datetime(const std::string &datetime) {
DateTimeCall &DateTimeCall::set_datetime(const char *datetime, size_t len) {
ESPTime val{};
if (!ESPTime::strptime(datetime, val)) {
if (!ESPTime::strptime(datetime, len, val)) {
ESP_LOGE(TAG, "Could not convert the time string to an ESPTime object");
return *this;
}

View File

@@ -71,7 +71,11 @@ class DateTimeCall {
void perform();
DateTimeCall &set_datetime(uint16_t year, uint8_t month, uint8_t day, uint8_t hour, uint8_t minute, uint8_t second);
DateTimeCall &set_datetime(ESPTime datetime);
DateTimeCall &set_datetime(const std::string &datetime);
DateTimeCall &set_datetime(const char *datetime, size_t len);
DateTimeCall &set_datetime(const char *datetime) { return this->set_datetime(datetime, strlen(datetime)); }
DateTimeCall &set_datetime(const std::string &datetime) {
return this->set_datetime(datetime.c_str(), datetime.size());
}
DateTimeCall &set_datetime(time_t epoch_seconds);
DateTimeCall &set_year(uint16_t year) {

View File

@@ -74,9 +74,9 @@ TimeCall &TimeCall::set_time(uint8_t hour, uint8_t minute, uint8_t second) {
TimeCall &TimeCall::set_time(ESPTime time) { return this->set_time(time.hour, time.minute, time.second); };
TimeCall &TimeCall::set_time(const std::string &time) {
TimeCall &TimeCall::set_time(const char *time, size_t len) {
ESPTime val{};
if (!ESPTime::strptime(time, val)) {
if (!ESPTime::strptime(time, len, val)) {
ESP_LOGE(TAG, "Could not convert the time string to an ESPTime object");
return *this;
}

View File

@@ -69,7 +69,9 @@ class TimeCall {
void perform();
TimeCall &set_time(uint8_t hour, uint8_t minute, uint8_t second);
TimeCall &set_time(ESPTime time);
TimeCall &set_time(const std::string &time);
TimeCall &set_time(const char *time, size_t len);
TimeCall &set_time(const char *time) { return this->set_time(time, strlen(time)); }
TimeCall &set_time(const std::string &time) { return this->set_time(time.c_str(), time.size()); }
TimeCall &set_hour(uint8_t hour) {
this->hour_ = hour;

View File

@@ -3,80 +3,21 @@
#include "esphome/core/log.h"
#include <Esp.h>
extern "C" {
#include <user_interface.h>
// Global reset info struct populated by SDK at boot
extern struct rst_info resetInfo;
// Core version - either a string pointer or a version number to format as hex
extern uint32_t core_version;
extern const char *core_release;
}
namespace esphome {
namespace debug {
static const char *const TAG = "debug";
// Get reset reason string from reason code (no heap allocation)
// Returns LogString* pointing to flash (PROGMEM) on ESP8266
static const LogString *get_reset_reason_str(uint32_t reason) {
switch (reason) {
case REASON_DEFAULT_RST:
return LOG_STR("Power On");
case REASON_WDT_RST:
return LOG_STR("Hardware Watchdog");
case REASON_EXCEPTION_RST:
return LOG_STR("Exception");
case REASON_SOFT_WDT_RST:
return LOG_STR("Software Watchdog");
case REASON_SOFT_RESTART:
return LOG_STR("Software/System restart");
case REASON_DEEP_SLEEP_AWAKE:
return LOG_STR("Deep-Sleep Wake");
case REASON_EXT_SYS_RST:
return LOG_STR("External System");
default:
return LOG_STR("Unknown");
}
}
// Size for core version hex buffer
static constexpr size_t CORE_VERSION_BUFFER_SIZE = 12;
// Get core version string (no heap allocation)
// Returns either core_release directly or formats core_version as hex into provided buffer
static const char *get_core_version_str(std::span<char, CORE_VERSION_BUFFER_SIZE> buffer) {
if (core_release != nullptr) {
return core_release;
}
snprintf_P(buffer.data(), CORE_VERSION_BUFFER_SIZE, PSTR("%08x"), core_version);
return buffer.data();
}
// Size for reset info buffer
static constexpr size_t RESET_INFO_BUFFER_SIZE = 200;
// Get detailed reset info string (no heap allocation)
// For watchdog/exception resets, includes detailed exception info
static const char *get_reset_info_str(std::span<char, RESET_INFO_BUFFER_SIZE> buffer, uint32_t reason) {
if (reason >= REASON_WDT_RST && reason <= REASON_SOFT_WDT_RST) {
snprintf_P(buffer.data(), RESET_INFO_BUFFER_SIZE,
PSTR("Fatal exception:%d flag:%d (%s) epc1:0x%08x epc2:0x%08x epc3:0x%08x excvaddr:0x%08x depc:0x%08x"),
static_cast<int>(resetInfo.exccause), static_cast<int>(reason),
LOG_STR_ARG(get_reset_reason_str(reason)), resetInfo.epc1, resetInfo.epc2, resetInfo.epc3,
resetInfo.excvaddr, resetInfo.depc);
return buffer.data();
}
return LOG_STR_ARG(get_reset_reason_str(reason));
}
const char *DebugComponent::get_reset_reason_(std::span<char, RESET_REASON_BUFFER_SIZE> buffer) {
// Copy from flash to provided buffer
strncpy_P(buffer.data(), (PGM_P) get_reset_reason_str(resetInfo.reason), RESET_REASON_BUFFER_SIZE - 1);
buffer[RESET_REASON_BUFFER_SIZE - 1] = '\0';
return buffer.data();
char *buf = buffer.data();
#if !defined(CLANG_TIDY)
String reason = ESP.getResetReason(); // NOLINT
snprintf_P(buf, RESET_REASON_BUFFER_SIZE, PSTR("%s"), reason.c_str());
return buf;
#else
buf[0] = '\0';
return buf;
#endif
}
const char *DebugComponent::get_wakeup_cause_(std::span<char, RESET_REASON_BUFFER_SIZE> buffer) {
@@ -92,42 +33,37 @@ size_t DebugComponent::get_device_info_(std::span<char, DEVICE_INFO_BUFFER_SIZE>
constexpr size_t size = DEVICE_INFO_BUFFER_SIZE;
char *buf = buffer.data();
const LogString *flash_mode;
const char *flash_mode;
switch (ESP.getFlashChipMode()) { // NOLINT(readability-static-accessed-through-instance)
case FM_QIO:
flash_mode = LOG_STR("QIO");
flash_mode = "QIO";
break;
case FM_QOUT:
flash_mode = LOG_STR("QOUT");
flash_mode = "QOUT";
break;
case FM_DIO:
flash_mode = LOG_STR("DIO");
flash_mode = "DIO";
break;
case FM_DOUT:
flash_mode = LOG_STR("DOUT");
flash_mode = "DOUT";
break;
default:
flash_mode = LOG_STR("UNKNOWN");
flash_mode = "UNKNOWN";
}
uint32_t flash_size = ESP.getFlashChipSize() / 1024; // NOLINT(readability-static-accessed-through-instance)
uint32_t flash_speed = ESP.getFlashChipSpeed() / 1000000; // NOLINT(readability-static-accessed-through-instance)
ESP_LOGD(TAG, "Flash Chip: Size=%" PRIu32 "kB Speed=%" PRIu32 "MHz Mode=%s", flash_size, flash_speed,
LOG_STR_ARG(flash_mode));
pos = buf_append_printf(buf, size, pos, "|Flash: %" PRIu32 "kB Speed:%" PRIu32 "MHz Mode:%s", flash_size, flash_speed,
LOG_STR_ARG(flash_mode));
uint32_t flash_size = ESP.getFlashChipSize() / 1024; // NOLINT
uint32_t flash_speed = ESP.getFlashChipSpeed() / 1000000; // NOLINT
ESP_LOGD(TAG, "Flash Chip: Size=%" PRIu32 "kB Speed=%" PRIu32 "MHz Mode=%s", flash_size, flash_speed, flash_mode);
pos = buf_append(buf, size, pos, "|Flash: %" PRIu32 "kB Speed:%" PRIu32 "MHz Mode:%s", flash_size, flash_speed,
flash_mode);
#if !defined(CLANG_TIDY)
char reason_buffer[RESET_REASON_BUFFER_SIZE];
const char *reset_reason = get_reset_reason_(reason_buffer);
char core_version_buffer[CORE_VERSION_BUFFER_SIZE];
char reset_info_buffer[RESET_INFO_BUFFER_SIZE];
// NOLINTBEGIN(readability-static-accessed-through-instance)
const char *reset_reason = get_reset_reason_(std::span<char, RESET_REASON_BUFFER_SIZE>(reason_buffer));
uint32_t chip_id = ESP.getChipId();
uint8_t boot_version = ESP.getBootVersion();
uint8_t boot_mode = ESP.getBootMode();
uint8_t cpu_freq = ESP.getCpuFreqMHz();
uint32_t flash_chip_id = ESP.getFlashChipId();
const char *sdk_version = ESP.getSdkVersion();
// NOLINTEND(readability-static-accessed-through-instance)
ESP_LOGD(TAG,
"Chip ID: 0x%08" PRIX32 "\n"
@@ -138,18 +74,19 @@ size_t DebugComponent::get_device_info_(std::span<char, DEVICE_INFO_BUFFER_SIZE>
"Flash Chip ID=0x%08" PRIX32 "\n"
"Reset Reason: %s\n"
"Reset Info: %s",
chip_id, sdk_version, get_core_version_str(core_version_buffer), boot_version, boot_mode, cpu_freq,
flash_chip_id, reset_reason, get_reset_info_str(reset_info_buffer, resetInfo.reason));
chip_id, ESP.getSdkVersion(), ESP.getCoreVersion().c_str(), boot_version, boot_mode, cpu_freq, flash_chip_id,
reset_reason, ESP.getResetInfo().c_str());
pos = buf_append_printf(buf, size, pos, "|Chip: 0x%08" PRIX32, chip_id);
pos = buf_append_printf(buf, size, pos, "|SDK: %s", sdk_version);
pos = buf_append_printf(buf, size, pos, "|Core: %s", get_core_version_str(core_version_buffer));
pos = buf_append_printf(buf, size, pos, "|Boot: %u", boot_version);
pos = buf_append_printf(buf, size, pos, "|Mode: %u", boot_mode);
pos = buf_append_printf(buf, size, pos, "|CPU: %u", cpu_freq);
pos = buf_append_printf(buf, size, pos, "|Flash: 0x%08" PRIX32, flash_chip_id);
pos = buf_append_printf(buf, size, pos, "|Reset: %s", reset_reason);
pos = buf_append_printf(buf, size, pos, "|%s", get_reset_info_str(reset_info_buffer, resetInfo.reason));
pos = buf_append(buf, size, pos, "|Chip: 0x%08" PRIX32, chip_id);
pos = buf_append(buf, size, pos, "|SDK: %s", ESP.getSdkVersion());
pos = buf_append(buf, size, pos, "|Core: %s", ESP.getCoreVersion().c_str());
pos = buf_append(buf, size, pos, "|Boot: %u", boot_version);
pos = buf_append(buf, size, pos, "|Mode: %u", boot_mode);
pos = buf_append(buf, size, pos, "|CPU: %u", cpu_freq);
pos = buf_append(buf, size, pos, "|Flash: 0x%08" PRIX32, flash_chip_id);
pos = buf_append(buf, size, pos, "|Reset: %s", reset_reason);
pos = buf_append(buf, size, pos, "|%s", ESP.getResetInfo().c_str());
#endif
return pos;
}

View File

@@ -77,7 +77,7 @@ FanSpeedSetTrigger = fan_ns.class_(
"FanSpeedSetTrigger", automation.Trigger.template(cg.int_)
)
FanPresetSetTrigger = fan_ns.class_(
"FanPresetSetTrigger", automation.Trigger.template(cg.std_string)
"FanPresetSetTrigger", automation.Trigger.template(cg.StringRef)
)
FanIsOnCondition = fan_ns.class_("FanIsOnCondition", automation.Condition.template())
@@ -287,7 +287,7 @@ async def setup_fan_core_(var, config):
await automation.build_automation(trigger, [(cg.int_, "x")], conf)
for conf in config.get(CONF_ON_PRESET_SET, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(trigger, [(cg.std_string, "x")], conf)
await automation.build_automation(trigger, [(cg.StringRef, "x")], conf)
async def register_fan(var, config):

View File

@@ -208,7 +208,7 @@ class FanSpeedSetTrigger : public Trigger<int> {
int last_speed_;
};
class FanPresetSetTrigger : public Trigger<std::string> {
class FanPresetSetTrigger : public Trigger<StringRef> {
public:
FanPresetSetTrigger(Fan *state) {
state->add_on_state_callback([this, state]() {
@@ -216,7 +216,7 @@ class FanPresetSetTrigger : public Trigger<std::string> {
auto should_trigger = preset_mode != this->last_preset_mode_;
this->last_preset_mode_ = preset_mode;
if (should_trigger) {
this->trigger(std::string(preset_mode));
this->trigger(preset_mode);
}
});
this->last_preset_mode_ = state->get_preset_mode();

View File

@@ -33,7 +33,7 @@ SelectPtr = Select.operator("ptr")
# Triggers
SelectStateTrigger = select_ns.class_(
"SelectStateTrigger",
automation.Trigger.template(cg.std_string, cg.size_t),
automation.Trigger.template(cg.StringRef, cg.size_t),
)
# Actions
@@ -100,7 +100,7 @@ async def setup_select_core_(var, config, *, options: list[str]):
for conf in config.get(CONF_ON_VALUE, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(
trigger, [(cg.std_string, "x"), (cg.size_t, "i")], conf
trigger, [(cg.StringRef, "x"), (cg.size_t, "i")], conf
)
if (mqtt_id := config.get(CONF_MQTT_ID)) is not None:

View File

@@ -6,11 +6,11 @@
namespace esphome::select {
class SelectStateTrigger : public Trigger<std::string, size_t> {
class SelectStateTrigger : public Trigger<StringRef, size_t> {
public:
explicit SelectStateTrigger(Select *parent) : parent_(parent) {
parent->add_on_state_callback(
[this](size_t index) { this->trigger(std::string(this->parent_->option_at(index)), index); });
[this](size_t index) { this->trigger(StringRef(this->parent_->option_at(index)), index); });
}
protected:

View File

@@ -72,6 +72,7 @@ class StringRef {
constexpr const char *c_str() const { return base_; }
constexpr size_type size() const { return len_; }
constexpr size_type length() const { return len_; }
constexpr bool empty() const { return len_ == 0; }
constexpr const_reference operator[](size_type pos) const { return *(base_ + pos); }
@@ -80,6 +81,32 @@ class StringRef {
operator std::string() const { return str(); }
/// Find first occurrence of substring, returns std::string::npos if not found.
/// Note: Requires the underlying string to be null-terminated.
size_type find(const char *s, size_type pos = 0) const {
if (pos >= len_)
return std::string::npos;
const char *result = std::strstr(base_ + pos, s);
// Verify entire match is within bounds (strstr searches to null terminator)
if (result && result + std::strlen(s) <= base_ + len_)
return static_cast<size_type>(result - base_);
return std::string::npos;
}
size_type find(char c, size_type pos = 0) const {
if (pos >= len_)
return std::string::npos;
const void *result = std::memchr(base_ + pos, static_cast<unsigned char>(c), len_ - pos);
return result ? static_cast<size_type>(static_cast<const char *>(result) - base_) : std::string::npos;
}
/// Return substring as std::string
std::string substr(size_type pos = 0, size_type count = std::string::npos) const {
if (pos >= len_)
return std::string();
size_type actual_count = (count == std::string::npos || pos + count > len_) ? len_ - pos : count;
return std::string(base_ + pos, actual_count);
}
private:
const char *base_;
size_type len_;
@@ -160,6 +187,43 @@ inline std::string operator+(const std::string &lhs, const StringRef &rhs) {
str.append(rhs.c_str(), rhs.size());
return str;
}
// String conversion functions for ADL compatibility (allows stoi(x) where x is StringRef)
// Must be in esphome namespace for ADL to find them. Uses strtol/strtod directly to avoid heap allocation.
namespace internal {
// NOLINTBEGIN(google-runtime-int)
template<typename R, typename F> inline R parse_number(const StringRef &str, size_t *pos, F conv) {
char *end;
R result = conv(str.c_str(), &end);
// Set pos to 0 on conversion failure (when no characters consumed), otherwise index after number
if (pos)
*pos = (end == str.c_str()) ? 0 : static_cast<size_t>(end - str.c_str());
return result;
}
template<typename R, typename F> inline R parse_number(const StringRef &str, size_t *pos, int base, F conv) {
char *end;
R result = conv(str.c_str(), &end, base);
// Set pos to 0 on conversion failure (when no characters consumed), otherwise index after number
if (pos)
*pos = (end == str.c_str()) ? 0 : static_cast<size_t>(end - str.c_str());
return result;
}
// NOLINTEND(google-runtime-int)
} // namespace internal
// NOLINTBEGIN(readability-identifier-naming,google-runtime-int)
inline int stoi(const StringRef &str, size_t *pos = nullptr, int base = 10) {
return static_cast<int>(internal::parse_number<long>(str, pos, base, std::strtol));
}
inline long stol(const StringRef &str, size_t *pos = nullptr, int base = 10) {
return internal::parse_number<long>(str, pos, base, std::strtol);
}
inline float stof(const StringRef &str, size_t *pos = nullptr) {
return internal::parse_number<float>(str, pos, std::strtof);
}
inline double stod(const StringRef &str, size_t *pos = nullptr) {
return internal::parse_number<double>(str, pos, std::strtod);
}
// NOLINTEND(readability-identifier-naming,google-runtime-int)
#ifdef USE_JSON
// NOLINTNEXTLINE(readability-identifier-naming)
inline void convertToJson(const StringRef &src, JsonVariant dst) { dst.set(src.c_str()); }

View File

@@ -67,7 +67,7 @@ std::string ESPTime::strftime(const char *format) {
std::string ESPTime::strftime(const std::string &format) { return this->strftime(format.c_str()); }
bool ESPTime::strptime(const std::string &time_to_parse, ESPTime &esp_time) {
bool ESPTime::strptime(const char *time_to_parse, size_t len, ESPTime &esp_time) {
uint16_t year;
uint8_t month;
uint8_t day;
@@ -75,40 +75,41 @@ bool ESPTime::strptime(const std::string &time_to_parse, ESPTime &esp_time) {
uint8_t minute;
uint8_t second;
int num;
const int ilen = static_cast<int>(len);
if (sscanf(time_to_parse.c_str(), "%04hu-%02hhu-%02hhu %02hhu:%02hhu:%02hhu %n", &year, &month, &day, // NOLINT
&hour, // NOLINT
&minute, // NOLINT
&second, &num) == 6 && // NOLINT
num == static_cast<int>(time_to_parse.size())) {
if (sscanf(time_to_parse, "%04hu-%02hhu-%02hhu %02hhu:%02hhu:%02hhu %n", &year, &month, &day, // NOLINT
&hour, // NOLINT
&minute, // NOLINT
&second, &num) == 6 && // NOLINT
num == ilen) {
esp_time.year = year;
esp_time.month = month;
esp_time.day_of_month = day;
esp_time.hour = hour;
esp_time.minute = minute;
esp_time.second = second;
} else if (sscanf(time_to_parse.c_str(), "%04hu-%02hhu-%02hhu %02hhu:%02hhu %n", &year, &month, &day, // NOLINT
&hour, // NOLINT
&minute, &num) == 5 && // NOLINT
num == static_cast<int>(time_to_parse.size())) {
} else if (sscanf(time_to_parse, "%04hu-%02hhu-%02hhu %02hhu:%02hhu %n", &year, &month, &day, // NOLINT
&hour, // NOLINT
&minute, &num) == 5 && // NOLINT
num == ilen) {
esp_time.year = year;
esp_time.month = month;
esp_time.day_of_month = day;
esp_time.hour = hour;
esp_time.minute = minute;
esp_time.second = 0;
} else if (sscanf(time_to_parse.c_str(), "%02hhu:%02hhu:%02hhu %n", &hour, &minute, &second, &num) == 3 && // NOLINT
num == static_cast<int>(time_to_parse.size())) {
} else if (sscanf(time_to_parse, "%02hhu:%02hhu:%02hhu %n", &hour, &minute, &second, &num) == 3 && // NOLINT
num == ilen) {
esp_time.hour = hour;
esp_time.minute = minute;
esp_time.second = second;
} else if (sscanf(time_to_parse.c_str(), "%02hhu:%02hhu %n", &hour, &minute, &num) == 2 && // NOLINT
num == static_cast<int>(time_to_parse.size())) {
} else if (sscanf(time_to_parse, "%02hhu:%02hhu %n", &hour, &minute, &num) == 2 && // NOLINT
num == ilen) {
esp_time.hour = hour;
esp_time.minute = minute;
esp_time.second = 0;
} else if (sscanf(time_to_parse.c_str(), "%04hu-%02hhu-%02hhu %n", &year, &month, &day, &num) == 3 && // NOLINT
num == static_cast<int>(time_to_parse.size())) {
} else if (sscanf(time_to_parse, "%04hu-%02hhu-%02hhu %n", &year, &month, &day, &num) == 3 && // NOLINT
num == ilen) {
esp_time.year = year;
esp_time.month = month;
esp_time.day_of_month = day;

View File

@@ -2,6 +2,7 @@
#include <cstdint>
#include <cstdlib>
#include <cstring>
#include <ctime>
#include <span>
#include <string>
@@ -80,11 +81,20 @@ struct ESPTime {
}
/** Convert a string to ESPTime struct as specified by the format argument.
* @param time_to_parse null-terminated c string formatet like this: 2020-08-25 05:30:00.
* @param time_to_parse c string formatted like this: 2020-08-25 05:30:00.
* @param len length of the string (not including null terminator if present)
* @param esp_time an instance of a ESPTime struct
* @return the success sate of the parsing
* @return the success state of the parsing
*/
static bool strptime(const std::string &time_to_parse, ESPTime &esp_time);
static bool strptime(const char *time_to_parse, size_t len, ESPTime &esp_time);
/// @copydoc strptime(const char *, size_t, ESPTime &)
static bool strptime(const char *time_to_parse, ESPTime &esp_time) {
return strptime(time_to_parse, strlen(time_to_parse), esp_time);
}
/// @copydoc strptime(const char *, size_t, ESPTime &)
static bool strptime(const std::string &time_to_parse, ESPTime &esp_time) {
return strptime(time_to_parse.c_str(), time_to_parse.size(), esp_time);
}
/// Convert a C tm struct instance with a C unix epoch timestamp to an ESPTime instance.
static ESPTime from_c_tm(struct tm *c_tm, time_t c_time);

View File

@@ -44,3 +44,4 @@ gpio_Flags = gpio_ns.enum("Flags", is_class=True)
EntityCategory = esphome_ns.enum("EntityCategory")
Parented = esphome_ns.class_("Parented")
ESPTime = esphome_ns.struct("ESPTime")
StringRef = esphome_ns.class_("StringRef")

View File

@@ -0,0 +1,85 @@
esphome:
name: select-stringref-test
friendly_name: Select StringRef Test
host:
logger:
level: DEBUG
api:
select:
- platform: template
name: "Test Select"
id: test_select
optimistic: true
options:
- "Option A"
- "Option B"
- "Option C"
initial_option: "Option A"
on_value:
then:
# Test 1: Log the value directly (StringRef -> const char* via c_str())
- logger.log:
format: "Select value: %s"
args: ['x.c_str()']
# Test 2: String concatenation (StringRef + const char* -> std::string)
- lambda: |-
std::string with_suffix = x + " selected";
ESP_LOGI("test", "Concatenated: %s", with_suffix.c_str());
# Test 3: Comparison (StringRef == const char*)
- lambda: |-
if (x == "Option B") {
ESP_LOGI("test", "Option B was selected");
}
# Test 4: Use index parameter (variable name is 'i')
- lambda: |-
ESP_LOGI("test", "Select index: %d", (int)i);
# Test 5: StringRef.length() method
- lambda: |-
ESP_LOGI("test", "Length: %d", (int)x.length());
# Test 6: StringRef.find() method with substring
- lambda: |-
if (x.find("Option") != std::string::npos) {
ESP_LOGI("test", "Found 'Option' in value");
}
# Test 7: StringRef.find() method with character
- lambda: |-
size_t space_pos = x.find(' ');
if (space_pos != std::string::npos) {
ESP_LOGI("test", "Space at position: %d", (int)space_pos);
}
# Test 8: StringRef.substr() method
- lambda: |-
std::string prefix = x.substr(0, 6);
ESP_LOGI("test", "Substr prefix: %s", prefix.c_str());
# Second select with numeric options to test ADL functions
- platform: template
name: "Baud Rate"
id: baud_select
optimistic: true
options:
- "9600"
- "115200"
initial_option: "9600"
on_value:
then:
# Test 9: stoi via ADL
- lambda: |-
int baud = stoi(x);
ESP_LOGI("test", "stoi result: %d", baud);
# Test 10: stol via ADL
- lambda: |-
long baud_long = stol(x);
ESP_LOGI("test", "stol result: %ld", baud_long);
# Test 11: stof via ADL
- lambda: |-
float baud_float = stof(x);
ESP_LOGI("test", "stof result: %.0f", baud_float);
# Test 12: stod via ADL
- lambda: |-
double baud_double = stod(x);
ESP_LOGI("test", "stod result: %.0f", baud_double);

View File

@@ -0,0 +1,143 @@
"""Integration test for select on_value trigger with StringRef parameter."""
from __future__ import annotations
import asyncio
import re
import pytest
from .types import APIClientConnectedFactory, RunCompiledFunction
@pytest.mark.asyncio
async def test_select_stringref_trigger(
yaml_config: str,
run_compiled: RunCompiledFunction,
api_client_connected: APIClientConnectedFactory,
) -> None:
"""Test select on_value trigger passes StringRef that works with string operations."""
loop = asyncio.get_running_loop()
# Track log messages to verify StringRef operations work
value_logged_future = loop.create_future()
concatenated_future = loop.create_future()
comparison_future = loop.create_future()
index_logged_future = loop.create_future()
length_future = loop.create_future()
find_substr_future = loop.create_future()
find_char_future = loop.create_future()
substr_future = loop.create_future()
# ADL functions
stoi_future = loop.create_future()
stol_future = loop.create_future()
stof_future = loop.create_future()
stod_future = loop.create_future()
# Patterns to match in logs
value_pattern = re.compile(r"Select value: Option B")
concatenated_pattern = re.compile(r"Concatenated: Option B selected")
comparison_pattern = re.compile(r"Option B was selected")
index_pattern = re.compile(r"Select index: 1")
length_pattern = re.compile(r"Length: 8") # "Option B" is 8 chars
find_substr_pattern = re.compile(r"Found 'Option' in value")
find_char_pattern = re.compile(r"Space at position: 6") # space at index 6
substr_pattern = re.compile(r"Substr prefix: Option")
# ADL function patterns (115200 from baud rate select)
stoi_pattern = re.compile(r"stoi result: 115200")
stol_pattern = re.compile(r"stol result: 115200")
stof_pattern = re.compile(r"stof result: 115200")
stod_pattern = re.compile(r"stod result: 115200")
def check_output(line: str) -> None:
"""Check log output for expected messages."""
if not value_logged_future.done() and value_pattern.search(line):
value_logged_future.set_result(True)
if not concatenated_future.done() and concatenated_pattern.search(line):
concatenated_future.set_result(True)
if not comparison_future.done() and comparison_pattern.search(line):
comparison_future.set_result(True)
if not index_logged_future.done() and index_pattern.search(line):
index_logged_future.set_result(True)
if not length_future.done() and length_pattern.search(line):
length_future.set_result(True)
if not find_substr_future.done() and find_substr_pattern.search(line):
find_substr_future.set_result(True)
if not find_char_future.done() and find_char_pattern.search(line):
find_char_future.set_result(True)
if not substr_future.done() and substr_pattern.search(line):
substr_future.set_result(True)
# ADL functions
if not stoi_future.done() and stoi_pattern.search(line):
stoi_future.set_result(True)
if not stol_future.done() and stol_pattern.search(line):
stol_future.set_result(True)
if not stof_future.done() and stof_pattern.search(line):
stof_future.set_result(True)
if not stod_future.done() and stod_pattern.search(line):
stod_future.set_result(True)
async with (
run_compiled(yaml_config, line_callback=check_output),
api_client_connected() as client,
):
# Verify device info
device_info = await client.device_info()
assert device_info is not None
assert device_info.name == "select-stringref-test"
# List entities to find our select
entities, _ = await client.list_entities_services()
select_entity = next(
(e for e in entities if hasattr(e, "options") and e.name == "Test Select"),
None,
)
assert select_entity is not None, "Test Select entity not found"
baud_entity = next(
(e for e in entities if hasattr(e, "options") and e.name == "Baud Rate"),
None,
)
assert baud_entity is not None, "Baud Rate entity not found"
# Change select to Option B - this should trigger on_value with StringRef
client.select_command(select_entity.key, "Option B")
# Change baud to 115200 - this tests ADL functions (stoi, stol, stof, stod)
client.select_command(baud_entity.key, "115200")
# Wait for all log messages confirming StringRef operations work
try:
await asyncio.wait_for(
asyncio.gather(
value_logged_future,
concatenated_future,
comparison_future,
index_logged_future,
length_future,
find_substr_future,
find_char_future,
substr_future,
stoi_future,
stol_future,
stof_future,
stod_future,
),
timeout=5.0,
)
except TimeoutError:
results = {
"value_logged": value_logged_future.done(),
"concatenated": concatenated_future.done(),
"comparison": comparison_future.done(),
"index_logged": index_logged_future.done(),
"length": length_future.done(),
"find_substr": find_substr_future.done(),
"find_char": find_char_future.done(),
"substr": substr_future.done(),
"stoi": stoi_future.done(),
"stol": stol_future.done(),
"stof": stof_future.done(),
"stod": stod_future.done(),
}
pytest.fail(f"StringRef operations failed - received: {results}")