From 37025d62e0006c4fee4357bc5679c158000a1dad Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 17 Jan 2026 08:28:40 -1000 Subject: [PATCH 01/10] [select][fan] Use StringRef for on_value/on_preset_set triggers to avoid heap allocation --- esphome/codegen.py | 1 + esphome/components/fan/__init__.py | 4 +- esphome/components/fan/automation.h | 4 +- esphome/components/select/__init__.py | 4 +- esphome/components/select/automation.h | 4 +- esphome/cpp_types.py | 1 + .../fixtures/select_stringref_trigger.yaml | 39 +++++++++ .../test_select_stringref_trigger.py | 84 +++++++++++++++++++ 8 files changed, 133 insertions(+), 8 deletions(-) create mode 100644 tests/integration/fixtures/select_stringref_trigger.yaml create mode 100644 tests/integration/test_select_stringref_trigger.py diff --git a/esphome/codegen.py b/esphome/codegen.py index 6d55c6023d..4a2a5975c6 100644 --- a/esphome/codegen.py +++ b/esphome/codegen.py @@ -69,6 +69,7 @@ from esphome.cpp_types import ( # noqa: F401 JsonObjectConst, Parented, PollingComponent, + StringRef, arduino_json_ns, bool_, const_char_ptr, diff --git a/esphome/components/fan/__init__.py b/esphome/components/fan/__init__.py index 35a351e8f1..6010aa8ed4 100644 --- a/esphome/components/fan/__init__.py +++ b/esphome/components/fan/__init__.py @@ -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): diff --git a/esphome/components/fan/automation.h b/esphome/components/fan/automation.h index 77abc2f13f..3c3b0ce519 100644 --- a/esphome/components/fan/automation.h +++ b/esphome/components/fan/automation.h @@ -208,7 +208,7 @@ class FanSpeedSetTrigger : public Trigger { int last_speed_; }; -class FanPresetSetTrigger : public Trigger { +class FanPresetSetTrigger : public Trigger { public: FanPresetSetTrigger(Fan *state) { state->add_on_state_callback([this, state]() { @@ -216,7 +216,7 @@ class FanPresetSetTrigger : public Trigger { 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(); diff --git a/esphome/components/select/__init__.py b/esphome/components/select/__init__.py index c51131a292..84ad591ba1 100644 --- a/esphome/components/select/__init__.py +++ b/esphome/components/select/__init__.py @@ -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: diff --git a/esphome/components/select/automation.h b/esphome/components/select/automation.h index 81e8a3561d..ffdabd5f7c 100644 --- a/esphome/components/select/automation.h +++ b/esphome/components/select/automation.h @@ -6,11 +6,11 @@ namespace esphome::select { -class SelectStateTrigger : public Trigger { +class SelectStateTrigger : public Trigger { 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: diff --git a/esphome/cpp_types.py b/esphome/cpp_types.py index 0d1813f63b..7001c38857 100644 --- a/esphome/cpp_types.py +++ b/esphome/cpp_types.py @@ -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") diff --git a/tests/integration/fixtures/select_stringref_trigger.yaml b/tests/integration/fixtures/select_stringref_trigger.yaml new file mode 100644 index 0000000000..ca9b81ae26 --- /dev/null +++ b/tests/integration/fixtures/select_stringref_trigger.yaml @@ -0,0 +1,39 @@ +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); diff --git a/tests/integration/test_select_stringref_trigger.py b/tests/integration/test_select_stringref_trigger.py new file mode 100644 index 0000000000..6a1ecdac2a --- /dev/null +++ b/tests/integration/test_select_stringref_trigger.py @@ -0,0 +1,84 @@ +"""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() + + # 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") + + 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) + + 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" + + # Change select to Option B - this should trigger on_value with StringRef + client.select_command(select_entity.key, "Option B") + + # 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, + ), + 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(), + } + pytest.fail(f"StringRef operations failed - received: {results}") From 65cdb97f0657a7d80b14a07be28c6a763d5f2ca5 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 17 Jan 2026 08:32:31 -1000 Subject: [PATCH 02/10] avoid breaking --- esphome/core/string_ref.h | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/esphome/core/string_ref.h b/esphome/core/string_ref.h index 44ca79c81b..5febb75d96 100644 --- a/esphome/core/string_ref.h +++ b/esphome/core/string_ref.h @@ -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,29 @@ class StringRef { operator std::string() const { return str(); } + /// Find first occurrence of substring, returns npos if not found + static constexpr size_type npos = static_cast(-1); + size_type find(const char *s, size_type pos = 0) const { + if (pos >= len_) + return npos; + const char *result = std::strstr(base_ + pos, s); + return result ? static_cast(result - base_) : npos; + } + size_type find(char c, size_type pos = 0) const { + if (pos >= len_) + return npos; + const char *result = std::strchr(base_ + pos, c); + return (result && result < base_ + len_) ? static_cast(result - base_) : npos; + } + + /// Return substring as std::string + std::string substr(size_type pos = 0, size_type count = npos) const { + if (pos >= len_) + return std::string(); + size_type actual_count = (count == npos || pos + count > len_) ? len_ - pos : count; + return std::string(base_ + pos, actual_count); + } + private: const char *base_; size_type len_; From 18c3dd8af70430e563ae7d4983000bef20a97c2e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 17 Jan 2026 08:35:46 -1000 Subject: [PATCH 03/10] make sure new stringref functions work --- .../fixtures/select_stringref_trigger.yaml | 18 ++++++++++++++ .../test_select_stringref_trigger.py | 24 +++++++++++++++++++ 2 files changed, 42 insertions(+) diff --git a/tests/integration/fixtures/select_stringref_trigger.yaml b/tests/integration/fixtures/select_stringref_trigger.yaml index ca9b81ae26..2ee64741fd 100644 --- a/tests/integration/fixtures/select_stringref_trigger.yaml +++ b/tests/integration/fixtures/select_stringref_trigger.yaml @@ -37,3 +37,21 @@ select: # 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") != StringRef::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 != StringRef::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()); diff --git a/tests/integration/test_select_stringref_trigger.py b/tests/integration/test_select_stringref_trigger.py index 6a1ecdac2a..f6c3efb72d 100644 --- a/tests/integration/test_select_stringref_trigger.py +++ b/tests/integration/test_select_stringref_trigger.py @@ -24,12 +24,20 @@ async def test_select_stringref_trigger( 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() # 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") def check_output(line: str) -> None: """Check log output for expected messages.""" @@ -41,6 +49,14 @@ async def test_select_stringref_trigger( 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) async with ( run_compiled(yaml_config, line_callback=check_output), @@ -71,6 +87,10 @@ async def test_select_stringref_trigger( concatenated_future, comparison_future, index_logged_future, + length_future, + find_substr_future, + find_char_future, + substr_future, ), timeout=5.0, ) @@ -80,5 +100,9 @@ async def test_select_stringref_trigger( "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(), } pytest.fail(f"StringRef operations failed - received: {results}") From 1550a6af7252782ade0eeb8e02d81ffb61bf62c9 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 17 Jan 2026 08:42:11 -1000 Subject: [PATCH 04/10] make sure new stringref functions work --- esphome/core/string_ref.h | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/esphome/core/string_ref.h b/esphome/core/string_ref.h index 5febb75d96..35d04dab0a 100644 --- a/esphome/core/string_ref.h +++ b/esphome/core/string_ref.h @@ -81,26 +81,26 @@ class StringRef { operator std::string() const { return str(); } - /// Find first occurrence of substring, returns npos if not found - static constexpr size_type npos = static_cast(-1); + /// Find first occurrence of substring, returns NPOS if not found + static constexpr size_type NPOS = static_cast(-1); size_type find(const char *s, size_type pos = 0) const { if (pos >= len_) - return npos; + return NPOS; const char *result = std::strstr(base_ + pos, s); - return result ? static_cast(result - base_) : npos; + return result ? static_cast(result - base_) : NPOS; } size_type find(char c, size_type pos = 0) const { if (pos >= len_) - return npos; + return NPOS; const char *result = std::strchr(base_ + pos, c); - return (result && result < base_ + len_) ? static_cast(result - base_) : npos; + return (result && result < base_ + len_) ? static_cast(result - base_) : NPOS; } /// Return substring as std::string - std::string substr(size_type pos = 0, size_type count = npos) const { + std::string substr(size_type pos = 0, size_type count = NPOS) const { if (pos >= len_) return std::string(); - size_type actual_count = (count == npos || pos + count > len_) ? len_ - pos : count; + size_type actual_count = (count == NPOS || pos + count > len_) ? len_ - pos : count; return std::string(base_ + pos, actual_count); } From 83d164c2132ae7bce908ddaec6970eb691255625 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 17 Jan 2026 08:42:16 -1000 Subject: [PATCH 05/10] make sure new stringref functions work --- tests/integration/fixtures/select_stringref_trigger.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/integration/fixtures/select_stringref_trigger.yaml b/tests/integration/fixtures/select_stringref_trigger.yaml index 2ee64741fd..8a391e509e 100644 --- a/tests/integration/fixtures/select_stringref_trigger.yaml +++ b/tests/integration/fixtures/select_stringref_trigger.yaml @@ -42,13 +42,13 @@ select: ESP_LOGI("test", "Length: %d", (int)x.length()); # Test 6: StringRef.find() method with substring - lambda: |- - if (x.find("Option") != StringRef::npos) { + if (x.find("Option") != StringRef::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 != StringRef::npos) { + if (space_pos != StringRef::NPOS) { ESP_LOGI("test", "Space at position: %d", (int)space_pos); } # Test 8: StringRef.substr() method From f3226b108ff59312fbc19dfa7c2243ee5c7ffb5b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 17 Jan 2026 08:42:56 -1000 Subject: [PATCH 06/10] make sure new stringref functions work --- esphome/core/string_ref.h | 15 +++++++-------- .../fixtures/select_stringref_trigger.yaml | 4 ++-- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/esphome/core/string_ref.h b/esphome/core/string_ref.h index 35d04dab0a..7501d06ce4 100644 --- a/esphome/core/string_ref.h +++ b/esphome/core/string_ref.h @@ -81,26 +81,25 @@ class StringRef { operator std::string() const { return str(); } - /// Find first occurrence of substring, returns NPOS if not found - static constexpr size_type NPOS = static_cast(-1); + /// Find first occurrence of substring, returns std::string::npos if not found size_type find(const char *s, size_type pos = 0) const { if (pos >= len_) - return NPOS; + return std::string::npos; const char *result = std::strstr(base_ + pos, s); - return result ? static_cast(result - base_) : NPOS; + return result ? static_cast(result - base_) : std::string::npos; } size_type find(char c, size_type pos = 0) const { if (pos >= len_) - return NPOS; + return std::string::npos; const char *result = std::strchr(base_ + pos, c); - return (result && result < base_ + len_) ? static_cast(result - base_) : NPOS; + return (result && result < base_ + len_) ? static_cast(result - base_) : std::string::npos; } /// Return substring as std::string - std::string substr(size_type pos = 0, size_type count = NPOS) const { + 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 == NPOS || pos + count > len_) ? len_ - pos : count; + size_type actual_count = (count == std::string::npos || pos + count > len_) ? len_ - pos : count; return std::string(base_ + pos, actual_count); } diff --git a/tests/integration/fixtures/select_stringref_trigger.yaml b/tests/integration/fixtures/select_stringref_trigger.yaml index 8a391e509e..207da844f2 100644 --- a/tests/integration/fixtures/select_stringref_trigger.yaml +++ b/tests/integration/fixtures/select_stringref_trigger.yaml @@ -42,13 +42,13 @@ select: ESP_LOGI("test", "Length: %d", (int)x.length()); # Test 6: StringRef.find() method with substring - lambda: |- - if (x.find("Option") != StringRef::NPOS) { + 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 != StringRef::NPOS) { + if (space_pos != std::string::npos) { ESP_LOGI("test", "Space at position: %d", (int)space_pos); } # Test 8: StringRef.substr() method From 620667f9d8768e33e96c98ac499365cabc5dd67a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 17 Jan 2026 08:44:43 -1000 Subject: [PATCH 07/10] bot review --- esphome/core/string_ref.h | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/esphome/core/string_ref.h b/esphome/core/string_ref.h index 7501d06ce4..3b209a7c7f 100644 --- a/esphome/core/string_ref.h +++ b/esphome/core/string_ref.h @@ -81,7 +81,8 @@ class StringRef { operator std::string() const { return str(); } - /// Find first occurrence of substring, returns std::string::npos if not found + /// 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; @@ -91,8 +92,8 @@ class StringRef { size_type find(char c, size_type pos = 0) const { if (pos >= len_) return std::string::npos; - const char *result = std::strchr(base_ + pos, c); - return (result && result < base_ + len_) ? static_cast(result - base_) : std::string::npos; + const void *result = std::memchr(base_ + pos, static_cast(c), len_ - pos); + return result ? static_cast(static_cast(result) - base_) : std::string::npos; } /// Return substring as std::string From 3cfca5228c1cde5cb8c6810f7e64cc4f5f0cc6bf Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 17 Jan 2026 08:45:10 -1000 Subject: [PATCH 08/10] bot review --- esphome/core/string_ref.h | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/esphome/core/string_ref.h b/esphome/core/string_ref.h index 3b209a7c7f..59aedbebda 100644 --- a/esphome/core/string_ref.h +++ b/esphome/core/string_ref.h @@ -87,7 +87,8 @@ class StringRef { if (pos >= len_) return std::string::npos; const char *result = std::strstr(base_ + pos, s); - return result ? static_cast(result - base_) : std::string::npos; + // Verify match is within bounds (strstr searches to null terminator) + return (result && result < base_ + len_) ? static_cast(result - base_) : std::string::npos; } size_type find(char c, size_type pos = 0) const { if (pos >= len_) From 451447b0fc0ede1f0e7a01a109af4bca59de3426 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 17 Jan 2026 10:54:13 -1000 Subject: [PATCH 09/10] adl --- esphome/core/string_ref.h | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/esphome/core/string_ref.h b/esphome/core/string_ref.h index 59aedbebda..c5ee64941e 100644 --- a/esphome/core/string_ref.h +++ b/esphome/core/string_ref.h @@ -185,6 +185,33 @@ 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) +// Uses strtol/strtod directly to avoid heap allocation +// NOLINTBEGIN(readability-identifier-naming) +template inline R parse_number(const StringRef &str, size_t *pos, F conv) { + char *end; + R result = conv(str.c_str(), &end); + if (pos) + *pos = static_cast(end - str.c_str()); + return result; +} +template inline R parse_number(const StringRef &str, size_t *pos, int base, F conv) { + char *end; + R result = conv(str.c_str(), &end, base); + if (pos) + *pos = static_cast(end - str.c_str()); + return result; +} +inline int stoi(const StringRef &str, size_t *pos = nullptr, int base = 10) { + return static_cast(parse_number(str, pos, base, std::strtol)); +} +inline long stol(const StringRef &str, size_t *pos = nullptr, int base = 10) { + return parse_number(str, pos, base, std::strtol); +} +inline float stof(const StringRef &str, size_t *pos = nullptr) { return parse_number(str, pos, std::strtof); } +inline double stod(const StringRef &str, size_t *pos = nullptr) { return parse_number(str, pos, std::strtod); } +// NOLINTEND(readability-identifier-naming) + #ifdef USE_JSON // NOLINTNEXTLINE(readability-identifier-naming) inline void convertToJson(const StringRef &src, JsonVariant dst) { dst.set(src.c_str()); } From 1dc4a5432f54e9beaf41c9d2ac6f02b17b845ea2 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 17 Jan 2026 10:55:48 -1000 Subject: [PATCH 10/10] adl --- esphome/core/string_ref.h | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/esphome/core/string_ref.h b/esphome/core/string_ref.h index c5ee64941e..d5d2897e82 100644 --- a/esphome/core/string_ref.h +++ b/esphome/core/string_ref.h @@ -186,8 +186,8 @@ inline std::string operator+(const std::string &lhs, const StringRef &rhs) { return str; } // String conversion functions for ADL compatibility (allows stoi(x) where x is StringRef) -// Uses strtol/strtod directly to avoid heap allocation -// NOLINTBEGIN(readability-identifier-naming) +// Must be in esphome namespace for ADL to find them. Uses strtol/strtod directly to avoid heap allocation. +namespace internal { template inline R parse_number(const StringRef &str, size_t *pos, F conv) { char *end; R result = conv(str.c_str(), &end); @@ -202,14 +202,20 @@ template inline R parse_number(const StringRef &str, siz *pos = static_cast(end - str.c_str()); return result; } +} // namespace internal +// NOLINTBEGIN(readability-identifier-naming) inline int stoi(const StringRef &str, size_t *pos = nullptr, int base = 10) { - return static_cast(parse_number(str, pos, base, std::strtol)); + return static_cast(internal::parse_number(str, pos, base, std::strtol)); } inline long stol(const StringRef &str, size_t *pos = nullptr, int base = 10) { - return parse_number(str, pos, base, std::strtol); + return internal::parse_number(str, pos, base, std::strtol); +} +inline float stof(const StringRef &str, size_t *pos = nullptr) { + return internal::parse_number(str, pos, std::strtof); +} +inline double stod(const StringRef &str, size_t *pos = nullptr) { + return internal::parse_number(str, pos, std::strtod); } -inline float stof(const StringRef &str, size_t *pos = nullptr) { return parse_number(str, pos, std::strtof); } -inline double stod(const StringRef &str, size_t *pos = nullptr) { return parse_number(str, pos, std::strtod); } // NOLINTEND(readability-identifier-naming) #ifdef USE_JSON