From 2b2c42eff551fcf1ced0e7d98dc63396a2699798 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 5 Mar 2026 15:12:09 -1000 Subject: [PATCH 1/6] [core] Replace Application name/friendly_name with StringRef Replace std::string members with StringRef for Application::name_ and Application::friendly_name_. These are set once during setup() and never modified, so std::string overhead is unnecessary. For the MAC suffix case, codegen emits static mutable char buffers with a placeholder suffix that pre_setup() overwrites with the actual MAC address. For the non-suffix case, StringRef points directly at the string literal. Saves ~2.5KB flash and ~48 bytes RAM by eliminating std::string template instantiations (constructor, _M_assign, _M_dispose, _M_construct, _M_replace_cold, _S_copy). --- .../components/api/api_frame_helper_noise.cpp | 2 +- esphome/components/esp32_ble/ble.cpp | 2 +- esphome/components/mdns/mdns_component.cpp | 2 +- esphome/components/mqtt/mqtt_component.cpp | 6 +-- esphome/components/openthread/openthread.cpp | 2 +- .../components/web_server/web_server_v1.cpp | 2 +- esphome/components/wifi/wifi_component.cpp | 2 +- .../wifi/wifi_component_esp8266.cpp | 2 +- esphome/core/application.h | 26 +++++------ esphome/core/config.py | 45 +++++++++++++++++-- esphome/core/entity_base.cpp | 6 +-- 11 files changed, 68 insertions(+), 29 deletions(-) diff --git a/esphome/components/api/api_frame_helper_noise.cpp b/esphome/components/api/api_frame_helper_noise.cpp index 3ae35e9be8..c9e7cd7fe1 100644 --- a/esphome/components/api/api_frame_helper_noise.cpp +++ b/esphome/components/api/api_frame_helper_noise.cpp @@ -269,7 +269,7 @@ APIError APINoiseFrameHelper::state_action_() { } if (state_ == State::SERVER_HELLO) { // send server hello - const std::string &name = App.get_name(); + const StringRef &name = App.get_name(); char mac[MAC_ADDRESS_BUFFER_SIZE]; get_mac_address_into_buffer(mac); diff --git a/esphome/components/esp32_ble/ble.cpp b/esphome/components/esp32_ble/ble.cpp index 9d26018800..c643721e91 100644 --- a/esphome/components/esp32_ble/ble.cpp +++ b/esphome/components/esp32_ble/ble.cpp @@ -273,7 +273,7 @@ bool ESP32BLE::ble_setup_() { device_name = this->name_; } } else { - const std::string &app_name = App.get_name(); + const StringRef &app_name = App.get_name(); size_t name_len = app_name.length(); if (name_len > 20) { if (App.is_name_add_mac_suffix_enabled()) { diff --git a/esphome/components/mdns/mdns_component.cpp b/esphome/components/mdns/mdns_component.cpp index 5e5e1279d9..5681b65ce4 100644 --- a/esphome/components/mdns/mdns_component.cpp +++ b/esphome/components/mdns/mdns_component.cpp @@ -59,7 +59,7 @@ void MDNSComponent::compile_records_(StaticVectorget_port(); - const std::string &friendly_name = App.get_friendly_name(); + const StringRef &friendly_name = App.get_friendly_name(); bool friendly_name_empty = friendly_name.empty(); // Calculate exact capacity for txt_records diff --git a/esphome/components/mqtt/mqtt_component.cpp b/esphome/components/mqtt/mqtt_component.cpp index f49069960b..8c10114eb9 100644 --- a/esphome/components/mqtt/mqtt_component.cpp +++ b/esphome/components/mqtt/mqtt_component.cpp @@ -268,7 +268,7 @@ bool MQTTComponent::send_discovery_() { root[MQTT_UNIQUE_ID] = unique_id_buf; } - const std::string &node_name = App.get_name(); + const StringRef &node_name = App.get_name(); if (discovery_info.object_id_generator == MQTT_DEVICE_NAME_OBJECT_ID_GENERATOR) { // node_name (max 31) + "_" (1) + object_id (max 128) + null char object_id_full[ESPHOME_DEVICE_NAME_MAX_LEN + 1 + OBJECT_ID_MAX_LEN + 1]; @@ -276,8 +276,8 @@ bool MQTTComponent::send_discovery_() { root[MQTT_OBJECT_ID] = object_id_full; } - const std::string &friendly_name_ref = App.get_friendly_name(); - const std::string &node_friendly_name = friendly_name_ref.empty() ? node_name : friendly_name_ref; + const StringRef &friendly_name_ref = App.get_friendly_name(); + const StringRef &node_friendly_name = friendly_name_ref.empty() ? node_name : friendly_name_ref; const char *node_area = App.get_area(); JsonObject device_info = root[MQTT_DEVICE].to(); diff --git a/esphome/components/openthread/openthread.cpp b/esphome/components/openthread/openthread.cpp index 9452f5a41e..c1aa5d5620 100644 --- a/esphome/components/openthread/openthread.cpp +++ b/esphome/components/openthread/openthread.cpp @@ -132,7 +132,7 @@ void OpenThreadSrpComponent::setup() { // set the host name uint16_t size; char *existing_host_name = otSrpClientBuffersGetHostNameString(instance, &size); - const std::string &host_name = App.get_name(); + const StringRef &host_name = App.get_name(); uint16_t host_name_len = host_name.size(); if (host_name_len > size) { ESP_LOGW(TAG, "Hostname is too long, choose a shorter project name"); diff --git a/esphome/components/web_server/web_server_v1.cpp b/esphome/components/web_server/web_server_v1.cpp index f7b90018dc..21980e544e 100644 --- a/esphome/components/web_server/web_server_v1.cpp +++ b/esphome/components/web_server/web_server_v1.cpp @@ -75,7 +75,7 @@ void WebServer::set_js_url(const char *js_url) { this->js_url_ = js_url; } void WebServer::handle_index_request(AsyncWebServerRequest *request) { AsyncResponseStream *stream = request->beginResponseStream(ESPHOME_F("text/html")); - const std::string &title = App.get_name(); + const StringRef &title = App.get_name(); stream->print(ESPHOME_F("")); stream->print(title.c_str()); diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index 8b60810d28..cea25a388e 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -913,7 +913,7 @@ void WiFiComponent::setup_ap_config_() { static constexpr size_t AP_SSID_PREFIX_LEN = 25; static constexpr size_t AP_SSID_SUFFIX_LEN = 7; - const std::string &app_name = App.get_name(); + const StringRef &app_name = App.get_name(); const char *name_ptr = app_name.c_str(); size_t name_len = app_name.length(); diff --git a/esphome/components/wifi/wifi_component_esp8266.cpp b/esphome/components/wifi/wifi_component_esp8266.cpp index 355832b434..c901252eb2 100644 --- a/esphome/components/wifi/wifi_component_esp8266.cpp +++ b/esphome/components/wifi/wifi_component_esp8266.cpp @@ -212,7 +212,7 @@ network::IPAddresses WiFiComponent::wifi_sta_ip_addresses() { return addresses; } bool WiFiComponent::wifi_apply_hostname_() { - const std::string &hostname = App.get_name(); + const StringRef &hostname = App.get_name(); bool ret = wifi_station_set_hostname(const_cast<char *>(hostname.c_str())); if (!ret) { ESP_LOGV(TAG, "Set hostname failed"); diff --git a/esphome/core/application.h b/esphome/core/application.h index 40f8a00edd..efac4d5aff 100644 --- a/esphome/core/application.h +++ b/esphome/core/application.h @@ -138,7 +138,7 @@ static constexpr uint32_t TEARDOWN_TIMEOUT_REBOOT_MS = 1000; // 1 second for qu class Application { public: - void pre_setup(const std::string &name, const std::string &friendly_name, bool name_add_mac_suffix) { + void pre_setup(char *name, size_t name_len, char *friendly_name, size_t friendly_name_len, bool name_add_mac_suffix) { arch_init(); this->name_add_mac_suffix_ = name_add_mac_suffix; if (name_add_mac_suffix) { @@ -148,15 +148,15 @@ class Application { constexpr size_t mac_address_suffix_len = 6; char mac_addr[mac_address_len]; get_mac_address_into_buffer(mac_addr); - const char *mac_suffix_ptr = mac_addr + mac_address_suffix_len; - this->name_ = make_name_with_suffix(name, '-', mac_suffix_ptr, mac_address_suffix_len); - if (!friendly_name.empty()) { - this->friendly_name_ = make_name_with_suffix(friendly_name, ' ', mac_suffix_ptr, mac_address_suffix_len); + // Overwrite the placeholder suffix in the static buffers with actual MAC + memcpy(name + name_len - mac_address_suffix_len, mac_addr + mac_address_suffix_len, mac_address_suffix_len); + if (friendly_name_len > 0) { + memcpy(friendly_name + friendly_name_len - mac_address_suffix_len, mac_addr + mac_address_suffix_len, + mac_address_suffix_len); } - } else { - this->name_ = name; - this->friendly_name_ = friendly_name; } + this->name_ = StringRef(name, name_len); + this->friendly_name_ = StringRef(friendly_name, friendly_name_len); } #ifdef USE_DEVICES @@ -274,10 +274,10 @@ class Application { void loop(); /// Get the name of this Application set by pre_setup(). - const std::string &get_name() const { return this->name_; } + const StringRef &get_name() const { return this->name_; } /// Get the friendly name of this Application set by pre_setup(). - const std::string &get_friendly_name() const { return this->friendly_name_; } + const StringRef &get_friendly_name() const { return this->friendly_name_; } /// Get the area of this Application set by pre_setup(). const char *get_area() const { @@ -627,9 +627,9 @@ class Application { #endif #endif - // std::string members (typically 24-32 bytes each) - std::string name_; - std::string friendly_name_; + // StringRef members (8 bytes each: pointer + size) + StringRef name_; + StringRef friendly_name_; // 4-byte members uint32_t last_loop_{0}; diff --git a/esphome/core/config.py b/esphome/core/config.py index 4f526404fe..08bb1252f2 100644 --- a/esphome/core/config.py +++ b/esphome/core/config.py @@ -50,6 +50,7 @@ from esphome.core import ( ) from esphome.helpers import ( copy_file_if_changed, + cpp_string_escape, fnv1a_32bit_hash, get_str_env, walk_files, @@ -58,6 +59,12 @@ from esphome.types import ConfigType _LOGGER = logging.getLogger(__name__) +# C++ variable names and separators for app name buffers (used with MAC suffix) +_APP_NAME_BUF_VAR = "esphome_app_name_buf" +_APP_NAME_MAC_SEP = "-" +_APP_FRIENDLY_NAME_BUF_VAR = "esphome_app_friendly_name_buf" +_APP_FRIENDLY_NAME_MAC_SEP = " " + StartupTrigger = cg.esphome_ns.class_( "StartupTrigger", cg.Component, automation.Trigger.template() ) @@ -551,11 +558,43 @@ async def to_code(config: ConfigType) -> None: # Construct App via placement new — see application.cpp for storage details cg.add_global(cg.RawStatement("#include <new>")) cg.add(cg.RawExpression("new (&App) Application()")) + name = config[CONF_NAME] + friendly_name = config[CONF_FRIENDLY_NAME] + name_add_mac_suffix = config[CONF_NAME_ADD_MAC_SUFFIX] + + def _make_app_name_expr( + value: str, var_name: str, sep: str + ) -> tuple[cg.Expression, int]: + """Create a name expression for pre_setup. + + With MAC suffix: emits a static mutable buffer with placeholder suffix. + Without: casts the string literal to char*. + Returns (expression, length). + """ + if not value: + return cg.RawExpression('(char *) ""'), 0 + if name_add_mac_suffix: + value_with_placeholder = f"{value}{sep}XXXXXX" + cg.add_global( + cg.RawStatement( + f"static char {var_name}[] = {cpp_string_escape(value_with_placeholder)};" + ) + ) + return cg.RawExpression(var_name), len(value_with_placeholder) + return ( + cg.RawExpression(f"(char *) {cpp_string_escape(value)}"), + len(value), + ) + + name_expr, name_len = _make_app_name_expr( + name, _APP_NAME_BUF_VAR, _APP_NAME_MAC_SEP + ) + friendly_expr, friendly_len = _make_app_name_expr( + friendly_name, _APP_FRIENDLY_NAME_BUF_VAR, _APP_FRIENDLY_NAME_MAC_SEP + ) cg.add( cg.App.pre_setup( - config[CONF_NAME], - config[CONF_FRIENDLY_NAME], - config[CONF_NAME_ADD_MAC_SUFFIX], + name_expr, name_len, friendly_expr, friendly_len, name_add_mac_suffix ) ) # Define component count for static allocation diff --git a/esphome/core/entity_base.cpp b/esphome/core/entity_base.cpp index eafc04f92a..071acb02d7 100644 --- a/esphome/core/entity_base.cpp +++ b/esphome/core/entity_base.cpp @@ -22,13 +22,13 @@ void EntityBase::set_name(const char *name, uint32_t object_id_hash) { // Bug-for-bug compatibility with OLD behavior: // - With MAC suffix: OLD code used App.get_friendly_name() directly (no fallback) // - Without MAC suffix: OLD code used pre-computed object_id with fallback to device name - const std::string &friendly = App.get_friendly_name(); + const StringRef &friendly = App.get_friendly_name(); if (App.is_name_add_mac_suffix_enabled()) { // MAC suffix enabled - use friendly_name directly (even if empty) for compatibility - this->name_ = StringRef(friendly); + this->name_ = friendly; } else { // No MAC suffix - fallback to device name if friendly_name is empty - this->name_ = StringRef(!friendly.empty() ? friendly : App.get_name()); + this->name_ = !friendly.empty() ? friendly : App.get_name(); } } this->flags_.has_own_name = false; From 2b799b1eded1e6703aa61dcc8cbaad1f54886248 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@home-assistant.io> Date: Thu, 5 Mar 2026 15:13:13 -1000 Subject: [PATCH 2/6] Use auto for App.get_name()/get_friendly_name() bindings --- esphome/components/api/api_frame_helper_noise.cpp | 2 +- esphome/components/esp32_ble/ble.cpp | 2 +- esphome/components/mdns/mdns_component.cpp | 2 +- esphome/components/mqtt/mqtt_component.cpp | 6 +++--- esphome/components/openthread/openthread.cpp | 2 +- esphome/components/web_server/web_server_v1.cpp | 2 +- esphome/components/wifi/wifi_component.cpp | 2 +- esphome/components/wifi/wifi_component_esp8266.cpp | 2 +- esphome/core/entity_base.cpp | 2 +- 9 files changed, 11 insertions(+), 11 deletions(-) diff --git a/esphome/components/api/api_frame_helper_noise.cpp b/esphome/components/api/api_frame_helper_noise.cpp index c9e7cd7fe1..ba4f2f0642 100644 --- a/esphome/components/api/api_frame_helper_noise.cpp +++ b/esphome/components/api/api_frame_helper_noise.cpp @@ -269,7 +269,7 @@ APIError APINoiseFrameHelper::state_action_() { } if (state_ == State::SERVER_HELLO) { // send server hello - const StringRef &name = App.get_name(); + const auto &name = App.get_name(); char mac[MAC_ADDRESS_BUFFER_SIZE]; get_mac_address_into_buffer(mac); diff --git a/esphome/components/esp32_ble/ble.cpp b/esphome/components/esp32_ble/ble.cpp index c643721e91..bbe972b9f3 100644 --- a/esphome/components/esp32_ble/ble.cpp +++ b/esphome/components/esp32_ble/ble.cpp @@ -273,7 +273,7 @@ bool ESP32BLE::ble_setup_() { device_name = this->name_; } } else { - const StringRef &app_name = App.get_name(); + const auto &app_name = App.get_name(); size_t name_len = app_name.length(); if (name_len > 20) { if (App.is_name_add_mac_suffix_enabled()) { diff --git a/esphome/components/mdns/mdns_component.cpp b/esphome/components/mdns/mdns_component.cpp index 5681b65ce4..342a6e6c64 100644 --- a/esphome/components/mdns/mdns_component.cpp +++ b/esphome/components/mdns/mdns_component.cpp @@ -59,7 +59,7 @@ void MDNSComponent::compile_records_(StaticVector<MDNSService, MDNS_SERVICE_COUN service.proto = MDNS_STR(SERVICE_TCP); service.port = api::global_api_server->get_port(); - const StringRef &friendly_name = App.get_friendly_name(); + const auto &friendly_name = App.get_friendly_name(); bool friendly_name_empty = friendly_name.empty(); // Calculate exact capacity for txt_records diff --git a/esphome/components/mqtt/mqtt_component.cpp b/esphome/components/mqtt/mqtt_component.cpp index 8c10114eb9..5d9b4d1157 100644 --- a/esphome/components/mqtt/mqtt_component.cpp +++ b/esphome/components/mqtt/mqtt_component.cpp @@ -268,7 +268,7 @@ bool MQTTComponent::send_discovery_() { root[MQTT_UNIQUE_ID] = unique_id_buf; } - const StringRef &node_name = App.get_name(); + const auto &node_name = App.get_name(); if (discovery_info.object_id_generator == MQTT_DEVICE_NAME_OBJECT_ID_GENERATOR) { // node_name (max 31) + "_" (1) + object_id (max 128) + null char object_id_full[ESPHOME_DEVICE_NAME_MAX_LEN + 1 + OBJECT_ID_MAX_LEN + 1]; @@ -276,8 +276,8 @@ bool MQTTComponent::send_discovery_() { root[MQTT_OBJECT_ID] = object_id_full; } - const StringRef &friendly_name_ref = App.get_friendly_name(); - const StringRef &node_friendly_name = friendly_name_ref.empty() ? node_name : friendly_name_ref; + const auto &friendly_name_ref = App.get_friendly_name(); + const auto &node_friendly_name = friendly_name_ref.empty() ? node_name : friendly_name_ref; const char *node_area = App.get_area(); JsonObject device_info = root[MQTT_DEVICE].to<JsonObject>(); diff --git a/esphome/components/openthread/openthread.cpp b/esphome/components/openthread/openthread.cpp index c1aa5d5620..fb81481299 100644 --- a/esphome/components/openthread/openthread.cpp +++ b/esphome/components/openthread/openthread.cpp @@ -132,7 +132,7 @@ void OpenThreadSrpComponent::setup() { // set the host name uint16_t size; char *existing_host_name = otSrpClientBuffersGetHostNameString(instance, &size); - const StringRef &host_name = App.get_name(); + const auto &host_name = App.get_name(); uint16_t host_name_len = host_name.size(); if (host_name_len > size) { ESP_LOGW(TAG, "Hostname is too long, choose a shorter project name"); diff --git a/esphome/components/web_server/web_server_v1.cpp b/esphome/components/web_server/web_server_v1.cpp index 21980e544e..85a4e80541 100644 --- a/esphome/components/web_server/web_server_v1.cpp +++ b/esphome/components/web_server/web_server_v1.cpp @@ -75,7 +75,7 @@ void WebServer::set_js_url(const char *js_url) { this->js_url_ = js_url; } void WebServer::handle_index_request(AsyncWebServerRequest *request) { AsyncResponseStream *stream = request->beginResponseStream(ESPHOME_F("text/html")); - const StringRef &title = App.get_name(); + const auto &title = App.get_name(); stream->print(ESPHOME_F("<!DOCTYPE html><html lang=\"en\"><head><meta charset=UTF-8><meta " "name=viewport content=\"width=device-width, initial-scale=1,user-scalable=no\"><title>")); stream->print(title.c_str()); diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index cea25a388e..60764955cc 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -913,7 +913,7 @@ void WiFiComponent::setup_ap_config_() { static constexpr size_t AP_SSID_PREFIX_LEN = 25; static constexpr size_t AP_SSID_SUFFIX_LEN = 7; - const StringRef &app_name = App.get_name(); + const auto &app_name = App.get_name(); const char *name_ptr = app_name.c_str(); size_t name_len = app_name.length(); diff --git a/esphome/components/wifi/wifi_component_esp8266.cpp b/esphome/components/wifi/wifi_component_esp8266.cpp index c901252eb2..a9b26c5935 100644 --- a/esphome/components/wifi/wifi_component_esp8266.cpp +++ b/esphome/components/wifi/wifi_component_esp8266.cpp @@ -212,7 +212,7 @@ network::IPAddresses WiFiComponent::wifi_sta_ip_addresses() { return addresses; } bool WiFiComponent::wifi_apply_hostname_() { - const StringRef &hostname = App.get_name(); + const auto &hostname = App.get_name(); bool ret = wifi_station_set_hostname(const_cast<char *>(hostname.c_str())); if (!ret) { ESP_LOGV(TAG, "Set hostname failed"); diff --git a/esphome/core/entity_base.cpp b/esphome/core/entity_base.cpp index 071acb02d7..62cf8a77c0 100644 --- a/esphome/core/entity_base.cpp +++ b/esphome/core/entity_base.cpp @@ -22,7 +22,7 @@ void EntityBase::set_name(const char *name, uint32_t object_id_hash) { // Bug-for-bug compatibility with OLD behavior: // - With MAC suffix: OLD code used App.get_friendly_name() directly (no fallback) // - Without MAC suffix: OLD code used pre-computed object_id with fallback to device name - const StringRef &friendly = App.get_friendly_name(); + const auto &friendly = App.get_friendly_name(); if (App.is_name_add_mac_suffix_enabled()) { // MAC suffix enabled - use friendly_name directly (even if empty) for compatibility this->name_ = friendly; From 0f10a13631700dace1473fd20b82388c2933fa2a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@home-assistant.io> Date: Thu, 5 Mar 2026 15:20:20 -1000 Subject: [PATCH 3/6] Use ifdef to split pre_setup signatures for const correctness When MAC suffix is not used, pre_setup takes const char* parameters so string literals stay in flash. When MAC suffix is used, it takes mutable char* for the static buffers that get overwritten with the actual MAC address. This avoids const_cast entirely. Also adds ESPHOME_NAME_ADD_MAC_SUFFIX define for static analysis. --- esphome/core/application.h | 39 +++++++++++++++++++++++--------------- esphome/core/config.py | 17 ++++++----------- esphome/core/defines.h | 1 + 3 files changed, 31 insertions(+), 26 deletions(-) diff --git a/esphome/core/application.h b/esphome/core/application.h index efac4d5aff..ef30193607 100644 --- a/esphome/core/application.h +++ b/esphome/core/application.h @@ -138,26 +138,35 @@ static constexpr uint32_t TEARDOWN_TIMEOUT_REBOOT_MS = 1000; // 1 second for qu class Application { public: - void pre_setup(char *name, size_t name_len, char *friendly_name, size_t friendly_name_len, bool name_add_mac_suffix) { +#ifdef ESPHOME_NAME_ADD_MAC_SUFFIX + /// Pre-setup with MAC suffix: overwrites placeholder in mutable static buffers with actual MAC. + void pre_setup(char *name, size_t name_len, char *friendly_name, size_t friendly_name_len) { arch_init(); - this->name_add_mac_suffix_ = name_add_mac_suffix; - if (name_add_mac_suffix) { - // MAC address length: 12 hex chars + null terminator - constexpr size_t mac_address_len = 13; - // MAC address suffix length (last 6 characters of 12-char MAC address string) - constexpr size_t mac_address_suffix_len = 6; - char mac_addr[mac_address_len]; - get_mac_address_into_buffer(mac_addr); - // Overwrite the placeholder suffix in the static buffers with actual MAC - memcpy(name + name_len - mac_address_suffix_len, mac_addr + mac_address_suffix_len, mac_address_suffix_len); - if (friendly_name_len > 0) { - memcpy(friendly_name + friendly_name_len - mac_address_suffix_len, mac_addr + mac_address_suffix_len, - mac_address_suffix_len); - } + this->name_add_mac_suffix_ = true; + // MAC address length: 12 hex chars + null terminator + constexpr size_t mac_address_len = 13; + // MAC address suffix length (last 6 characters of 12-char MAC address string) + constexpr size_t mac_address_suffix_len = 6; + char mac_addr[mac_address_len]; + get_mac_address_into_buffer(mac_addr); + // Overwrite the placeholder suffix in the mutable static buffers with actual MAC + memcpy(name + name_len - mac_address_suffix_len, mac_addr + mac_address_suffix_len, mac_address_suffix_len); + if (friendly_name_len > 0) { + memcpy(friendly_name + friendly_name_len - mac_address_suffix_len, mac_addr + mac_address_suffix_len, + mac_address_suffix_len); } this->name_ = StringRef(name, name_len); this->friendly_name_ = StringRef(friendly_name, friendly_name_len); } +#else + /// Pre-setup without MAC suffix: StringRef points directly at const string literals in flash. + void pre_setup(const char *name, size_t name_len, const char *friendly_name, size_t friendly_name_len) { + arch_init(); + this->name_add_mac_suffix_ = false; + this->name_ = StringRef(name, name_len); + this->friendly_name_ = StringRef(friendly_name, friendly_name_len); + } +#endif #ifdef USE_DEVICES void register_device(Device *device) { this->devices_.push_back(device); } diff --git a/esphome/core/config.py b/esphome/core/config.py index 08bb1252f2..be3c6466e2 100644 --- a/esphome/core/config.py +++ b/esphome/core/config.py @@ -568,11 +568,11 @@ async def to_code(config: ConfigType) -> None: """Create a name expression for pre_setup. With MAC suffix: emits a static mutable buffer with placeholder suffix. - Without: casts the string literal to char*. + Without: passes the string literal directly as const char*. Returns (expression, length). """ if not value: - return cg.RawExpression('(char *) ""'), 0 + return cg.RawExpression('""'), 0 if name_add_mac_suffix: value_with_placeholder = f"{value}{sep}XXXXXX" cg.add_global( @@ -581,10 +581,7 @@ async def to_code(config: ConfigType) -> None: ) ) return cg.RawExpression(var_name), len(value_with_placeholder) - return ( - cg.RawExpression(f"(char *) {cpp_string_escape(value)}"), - len(value), - ) + return cg.RawExpression(cpp_string_escape(value)), len(value) name_expr, name_len = _make_app_name_expr( name, _APP_NAME_BUF_VAR, _APP_NAME_MAC_SEP @@ -592,11 +589,9 @@ async def to_code(config: ConfigType) -> None: friendly_expr, friendly_len = _make_app_name_expr( friendly_name, _APP_FRIENDLY_NAME_BUF_VAR, _APP_FRIENDLY_NAME_MAC_SEP ) - cg.add( - cg.App.pre_setup( - name_expr, name_len, friendly_expr, friendly_len, name_add_mac_suffix - ) - ) + if name_add_mac_suffix: + cg.add_define("ESPHOME_NAME_ADD_MAC_SUFFIX") + cg.add(cg.App.pre_setup(name_expr, name_len, friendly_expr, friendly_len)) # Define component count for static allocation cg.add_define("ESPHOME_COMPONENT_COUNT", len(CORE.component_ids)) diff --git a/esphome/core/defines.h b/esphome/core/defines.h index 1a6d9b3a80..e6aa0e068b 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -13,6 +13,7 @@ #define ESPHOME_PROJECT_VERSION "v2" #define ESPHOME_PROJECT_VERSION_30 "v2" #define ESPHOME_VARIANT "ESP32" +#define ESPHOME_NAME_ADD_MAC_SUFFIX #define ESPHOME_DEBUG_SCHEDULER #define ESPHOME_DEBUG_API From 3d9ed604a0c9cfe861b29e0c9f68f28666b7fcad Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@home-assistant.io> Date: Thu, 5 Mar 2026 15:32:14 -1000 Subject: [PATCH 4/6] Fix empty friendly_name with MAC suffix and use UTF-8 byte lengths When name_add_mac_suffix is true and friendly_name is empty, emit a mutable static char[] buffer instead of a string literal to match the char* signature. Also use UTF-8 byte lengths instead of Python str len() to handle non-ASCII characters correctly. --- esphome/core/config.py | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/esphome/core/config.py b/esphome/core/config.py index be3c6466e2..f21f8ae374 100644 --- a/esphome/core/config.py +++ b/esphome/core/config.py @@ -569,19 +569,25 @@ async def to_code(config: ConfigType) -> None: With MAC suffix: emits a static mutable buffer with placeholder suffix. Without: passes the string literal directly as const char*. - Returns (expression, length). + Returns (expression, byte_length). """ - if not value: - return cg.RawExpression('""'), 0 if name_add_mac_suffix: - value_with_placeholder = f"{value}{sep}XXXXXX" + value_with_placeholder = "" if not value else f"{value}{sep}XXXXXX" cg.add_global( cg.RawStatement( f"static char {var_name}[] = {cpp_string_escape(value_with_placeholder)};" ) ) - return cg.RawExpression(var_name), len(value_with_placeholder) - return cg.RawExpression(cpp_string_escape(value)), len(value) + return ( + cg.RawExpression(var_name), + len(value_with_placeholder.encode("utf-8")), + ) + if not value: + return cg.RawExpression('""'), 0 + return ( + cg.RawExpression(cpp_string_escape(value)), + len(value.encode("utf-8")), + ) name_expr, name_len = _make_app_name_expr( name, _APP_NAME_BUF_VAR, _APP_NAME_MAC_SEP From 363086b4f17c0a56bad9b53f62ef30202495eeb7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@home-assistant.io> Date: Thu, 5 Mar 2026 15:35:31 -1000 Subject: [PATCH 5/6] Extract make_app_name_cpp as testable free function with unit tests Move the pure logic (string building, escaping, byte length calculation) out of the nested closure into a module-level function that can be tested without codegen mocks. Tests cover both copilot review concerns: empty friendly_name with MAC suffix, and UTF-8 byte length for non-ASCII characters. --- esphome/core/config.py | 62 ++++++++++++---------- tests/unit_tests/core/test_config.py | 77 ++++++++++++++++++++++++++++ 2 files changed, 112 insertions(+), 27 deletions(-) diff --git a/esphome/core/config.py b/esphome/core/config.py index f21f8ae374..a1b2c0d281 100644 --- a/esphome/core/config.py +++ b/esphome/core/config.py @@ -64,6 +64,32 @@ _APP_NAME_BUF_VAR = "esphome_app_name_buf" _APP_NAME_MAC_SEP = "-" _APP_FRIENDLY_NAME_BUF_VAR = "esphome_app_friendly_name_buf" _APP_FRIENDLY_NAME_MAC_SEP = " " +# Placeholder suffix for MAC address (last 6 hex chars) +_MAC_SUFFIX_PLACEHOLDER = "XXXXXX" + + +def make_app_name_cpp( + value: str, var_name: str, sep: str, *, add_mac_suffix: bool +) -> tuple[str, str | None, int]: + """Compute C++ expression and optional global declaration for an app name. + + Returns (cpp_expr, global_decl_or_none, byte_length). + - cpp_expr: The C++ expression to pass to pre_setup (var name or string literal). + - global_decl: A static char[] declaration string, or None if not needed. + - byte_length: The UTF-8 byte length of the string value. + """ + if add_mac_suffix: + buf_value = "" if not value else f"{value}{sep}{_MAC_SUFFIX_PLACEHOLDER}" + escaped = cpp_string_escape(buf_value) + return ( + var_name, + f"static char {var_name}[] = {escaped};", + len(buf_value.encode("utf-8")), + ) + if not value: + return '""', None, 0 + return cpp_string_escape(value), None, len(value.encode("utf-8")) + StartupTrigger = cg.esphome_ns.class_( "StartupTrigger", cg.Component, automation.Trigger.template() @@ -562,37 +588,19 @@ async def to_code(config: ConfigType) -> None: friendly_name = config[CONF_FRIENDLY_NAME] name_add_mac_suffix = config[CONF_NAME_ADD_MAC_SUFFIX] - def _make_app_name_expr( + def _emit_app_name( value: str, var_name: str, sep: str ) -> tuple[cg.Expression, int]: - """Create a name expression for pre_setup. - - With MAC suffix: emits a static mutable buffer with placeholder suffix. - Without: passes the string literal directly as const char*. - Returns (expression, byte_length). - """ - if name_add_mac_suffix: - value_with_placeholder = "" if not value else f"{value}{sep}XXXXXX" - cg.add_global( - cg.RawStatement( - f"static char {var_name}[] = {cpp_string_escape(value_with_placeholder)};" - ) - ) - return ( - cg.RawExpression(var_name), - len(value_with_placeholder.encode("utf-8")), - ) - if not value: - return cg.RawExpression('""'), 0 - return ( - cg.RawExpression(cpp_string_escape(value)), - len(value.encode("utf-8")), + """Emit codegen for an app name and return (expression, byte_length).""" + cpp_expr, global_decl, byte_len = make_app_name_cpp( + value, var_name, sep, add_mac_suffix=name_add_mac_suffix ) + if global_decl is not None: + cg.add_global(cg.RawStatement(global_decl)) + return cg.RawExpression(cpp_expr), byte_len - name_expr, name_len = _make_app_name_expr( - name, _APP_NAME_BUF_VAR, _APP_NAME_MAC_SEP - ) - friendly_expr, friendly_len = _make_app_name_expr( + name_expr, name_len = _emit_app_name(name, _APP_NAME_BUF_VAR, _APP_NAME_MAC_SEP) + friendly_expr, friendly_len = _emit_app_name( friendly_name, _APP_FRIENDLY_NAME_BUF_VAR, _APP_FRIENDLY_NAME_MAC_SEP ) if name_add_mac_suffix: diff --git a/tests/unit_tests/core/test_config.py b/tests/unit_tests/core/test_config.py index 88801a9ca0..474d31a90a 100644 --- a/tests/unit_tests/core/test_config.py +++ b/tests/unit_tests/core/test_config.py @@ -23,6 +23,7 @@ from esphome.const import ( from esphome.core import CORE, config from esphome.core.config import ( Area, + make_app_name_cpp, preload_core_config, valid_include, valid_project_name, @@ -969,3 +970,79 @@ def test_config_hash_different_for_different_configs() -> None: hash2 = CORE.config_hash assert hash1 != hash2 + + +def test_make_app_name_cpp_no_mac_simple() -> None: + """Test simple name without MAC suffix returns string literal.""" + cpp_expr, global_decl, byte_len = make_app_name_cpp( + "my-device", "buf", "-", add_mac_suffix=False + ) + assert cpp_expr == '"my-device"' + assert global_decl is None + assert byte_len == 9 + + +def test_make_app_name_cpp_no_mac_empty() -> None: + """Test empty name without MAC suffix.""" + cpp_expr, global_decl, byte_len = make_app_name_cpp( + "", "buf", "-", add_mac_suffix=False + ) + assert cpp_expr == '""' + assert global_decl is None + assert byte_len == 0 + + +def test_make_app_name_cpp_mac_suffix() -> None: + """Test name with MAC suffix emits static buffer.""" + cpp_expr, global_decl, byte_len = make_app_name_cpp( + "my-device", "esphome_app_name_buf", "-", add_mac_suffix=True + ) + assert cpp_expr == "esphome_app_name_buf" + assert global_decl is not None + assert "static char esphome_app_name_buf[]" in global_decl + assert "my-device-XXXXXX" in global_decl + assert byte_len == len("my-device-XXXXXX") + + +def test_make_app_name_cpp_mac_suffix_empty() -> None: + """Test empty name with MAC suffix emits empty static buffer.""" + cpp_expr, global_decl, byte_len = make_app_name_cpp( + "", "esphome_app_name_buf", "-", add_mac_suffix=True + ) + assert cpp_expr == "esphome_app_name_buf" + assert global_decl is not None + assert "static char esphome_app_name_buf[]" in global_decl + assert byte_len == 0 + + +def test_make_app_name_cpp_mac_suffix_space_sep() -> None: + """Test friendly name uses space separator for MAC suffix.""" + cpp_expr, global_decl, byte_len = make_app_name_cpp( + "My Device", "esphome_app_friendly_name_buf", " ", add_mac_suffix=True + ) + assert cpp_expr == "esphome_app_friendly_name_buf" + assert global_decl is not None + assert "My Device XXXXXX" in global_decl + assert byte_len == len("My Device XXXXXX") + + +def test_make_app_name_cpp_non_ascii_utf8_length() -> None: + """Test non-ASCII characters use UTF-8 byte length.""" + _, global_decl, byte_len = make_app_name_cpp( + "café", "buf", "-", add_mac_suffix=False + ) + assert byte_len == len("café".encode()) # 5 bytes, not 4 chars + assert global_decl is None + + +def test_make_app_name_cpp_non_ascii_mac_suffix_utf8_length() -> None: + """Test non-ASCII with MAC suffix uses UTF-8 byte length.""" + _, _, byte_len = make_app_name_cpp("café", "buf", "-", add_mac_suffix=True) + assert byte_len == len("café-XXXXXX".encode()) + + +def test_make_app_name_cpp_special_chars_escaped() -> None: + """Test special characters are properly escaped in C++ string.""" + cpp_expr, _, _ = make_app_name_cpp('my "device"', "buf", "-", add_mac_suffix=False) + # cpp_string_escape uses octal escapes for quotes + assert '"' not in cpp_expr[1:-1] # no unescaped quotes inside the outer quotes From 8a6a7e0eeba46fd785ea91fc9cb53f73d3d64357 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <nick@home-assistant.io> Date: Thu, 5 Mar 2026 15:42:53 -1000 Subject: [PATCH 6/6] Add comment noting name is always non-empty in MAC suffix path --- esphome/core/application.h | 1 + 1 file changed, 1 insertion(+) diff --git a/esphome/core/application.h b/esphome/core/application.h index ef30193607..87f9fdf59a 100644 --- a/esphome/core/application.h +++ b/esphome/core/application.h @@ -150,6 +150,7 @@ class Application { char mac_addr[mac_address_len]; get_mac_address_into_buffer(mac_addr); // Overwrite the placeholder suffix in the mutable static buffers with actual MAC + // name is always non-empty (validated by validate_hostname in Python config) memcpy(name + name_len - mac_address_suffix_len, mac_addr + mac_address_suffix_len, mac_address_suffix_len); if (friendly_name_len > 0) { memcpy(friendly_name + friendly_name_len - mac_address_suffix_len, mac_addr + mac_address_suffix_len,