From 6dcbc248648bfdc5190129ef7f063784b7baa5f0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 9 Jan 2026 07:43:01 -1000 Subject: [PATCH 1/8] [climate] Return std::string_view from get_custom_fan_mode() and get_custom_preset() --- esphome/components/api/api_connection.cpp | 6 ++-- .../bedjet/climate/bedjet_climate.cpp | 21 ++++++------- esphome/components/climate/climate.cpp | 10 +++---- esphome/components/climate/climate.h | 30 ++++++++++++++----- esphome/components/midea/air_conditioner.cpp | 6 ++-- esphome/components/mqtt/mqtt_climate.cpp | 4 +-- .../thermostat/thermostat_climate.cpp | 4 +-- esphome/components/web_server/web_server.cpp | 6 ++-- tests/components/midea/common.yaml | 19 ++++++++++++ 9 files changed, 73 insertions(+), 33 deletions(-) diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index fb3548d117..79516666b7 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -675,13 +675,15 @@ uint16_t APIConnection::try_send_climate_state(EntityBase *entity, APIConnection if (traits.get_supports_fan_modes() && climate->fan_mode.has_value()) resp.fan_mode = static_cast(climate->fan_mode.value()); if (!traits.get_supported_custom_fan_modes().empty() && climate->has_custom_fan_mode()) { - resp.custom_fan_mode = StringRef(climate->get_custom_fan_mode()); + auto mode = climate->get_custom_fan_mode(); + resp.custom_fan_mode = StringRef(mode.data(), mode.size()); } if (traits.get_supports_presets() && climate->preset.has_value()) { resp.preset = static_cast(climate->preset.value()); } if (!traits.get_supported_custom_presets().empty() && climate->has_custom_preset()) { - resp.custom_preset = StringRef(climate->get_custom_preset()); + auto preset = climate->get_custom_preset(); + resp.custom_preset = StringRef(preset.data(), preset.size()); } if (traits.get_supports_swing_modes()) resp.swing_mode = static_cast(climate->swing_mode); diff --git a/esphome/components/bedjet/climate/bedjet_climate.cpp b/esphome/components/bedjet/climate/bedjet_climate.cpp index 716d4d4241..d8b2d40bb1 100644 --- a/esphome/components/bedjet/climate/bedjet_climate.cpp +++ b/esphome/components/bedjet/climate/bedjet_climate.cpp @@ -164,21 +164,21 @@ void BedJetClimate::control(const ClimateCall &call) { return; } } else if (call.has_custom_preset()) { - const char *preset = call.get_custom_preset(); + auto preset = call.get_custom_preset(); bool result; - if (strcmp(preset, "M1") == 0) { + if (preset == "M1") { result = this->parent_->button_memory1(); - } else if (strcmp(preset, "M2") == 0) { + } else if (preset == "M2") { result = this->parent_->button_memory2(); - } else if (strcmp(preset, "M3") == 0) { + } else if (preset == "M3") { result = this->parent_->button_memory3(); - } else if (strcmp(preset, "LTD HT") == 0) { + } else if (preset == "LTD HT") { result = this->parent_->button_heat(); - } else if (strcmp(preset, "EXT HT") == 0) { + } else if (preset == "EXT HT") { result = this->parent_->button_ext_heat(); } else { - ESP_LOGW(TAG, "Unsupported preset: %s", preset); + ESP_LOGW(TAG, "Unsupported preset: %.*s", (int) preset.size(), preset.data()); return; } @@ -208,10 +208,11 @@ void BedJetClimate::control(const ClimateCall &call) { this->set_fan_mode_(fan_mode); } } else if (call.has_custom_fan_mode()) { - const char *fan_mode = call.get_custom_fan_mode(); - auto fan_index = bedjet_fan_speed_to_step(fan_mode); + auto fan_mode = call.get_custom_fan_mode(); + auto fan_index = bedjet_fan_speed_to_step(fan_mode.data()); if (fan_index <= 19) { - ESP_LOGV(TAG, "[%s] Converted fan mode %s to bedjet fan step %d", this->get_name().c_str(), fan_mode, fan_index); + ESP_LOGV(TAG, "[%s] Converted fan mode %.*s to bedjet fan step %d", this->get_name().c_str(), + (int) fan_mode.size(), fan_mode.data(), fan_index); bool result = this->parent_->set_fan_index(fan_index); if (result) { this->set_custom_fan_mode_(fan_mode); diff --git a/esphome/components/climate/climate.cpp b/esphome/components/climate/climate.cpp index 2d35509493..7611d33cbf 100644 --- a/esphome/components/climate/climate.cpp +++ b/esphome/components/climate/climate.cpp @@ -682,19 +682,19 @@ bool Climate::set_fan_mode_(ClimateFanMode mode) { return set_primary_mode(this->fan_mode, this->custom_fan_mode_, mode); } -bool Climate::set_custom_fan_mode_(const char *mode) { +bool Climate::set_custom_fan_mode_(const char *mode, size_t len) { auto traits = this->get_traits(); - return set_custom_mode(this->custom_fan_mode_, this->fan_mode, traits.find_custom_fan_mode_(mode), - this->has_custom_fan_mode()); + return set_custom_mode(this->custom_fan_mode_, this->fan_mode, + traits.find_custom_fan_mode_(mode, len), this->has_custom_fan_mode()); } void Climate::clear_custom_fan_mode_() { this->custom_fan_mode_ = nullptr; } bool Climate::set_preset_(ClimatePreset preset) { return set_primary_mode(this->preset, this->custom_preset_, preset); } -bool Climate::set_custom_preset_(const char *preset) { +bool Climate::set_custom_preset_(const char *preset, size_t len) { auto traits = this->get_traits(); - return set_custom_mode(this->custom_preset_, this->preset, traits.find_custom_preset_(preset), + return set_custom_mode(this->custom_preset_, this->preset, traits.find_custom_preset_(preset, len), this->has_custom_preset()); } diff --git a/esphome/components/climate/climate.h b/esphome/components/climate/climate.h index 06adb580cf..f6d3d10a93 100644 --- a/esphome/components/climate/climate.h +++ b/esphome/components/climate/climate.h @@ -1,5 +1,7 @@ #pragma once +#include + #include "esphome/core/component.h" #include "esphome/core/entity_base.h" #include "esphome/core/helpers.h" @@ -110,8 +112,12 @@ class ClimateCall { const optional &get_fan_mode() const; const optional &get_swing_mode() const; const optional &get_preset() const; - const char *get_custom_fan_mode() const { return this->custom_fan_mode_; } - const char *get_custom_preset() const { return this->custom_preset_; } + std::string_view get_custom_fan_mode() const { + return this->custom_fan_mode_ != nullptr ? std::string_view(this->custom_fan_mode_) : std::string_view(); + } + std::string_view get_custom_preset() const { + return this->custom_preset_ != nullptr ? std::string_view(this->custom_preset_) : std::string_view(); + } bool has_custom_fan_mode() const { return this->custom_fan_mode_ != nullptr; } bool has_custom_preset() const { return this->custom_preset_ != nullptr; } @@ -266,11 +272,15 @@ class Climate : public EntityBase { /// The active swing mode of the climate device. ClimateSwingMode swing_mode{CLIMATE_SWING_OFF}; - /// Get the active custom fan mode (read-only access). - const char *get_custom_fan_mode() const { return this->custom_fan_mode_; } + /// Get the active custom fan mode (read-only access). Returns std::string_view. + std::string_view get_custom_fan_mode() const { + return this->custom_fan_mode_ != nullptr ? std::string_view(this->custom_fan_mode_) : std::string_view(); + } - /// Get the active custom preset (read-only access). - const char *get_custom_preset() const { return this->custom_preset_; } + /// Get the active custom preset (read-only access). Returns std::string_view. + std::string_view get_custom_preset() const { + return this->custom_preset_ != nullptr ? std::string_view(this->custom_preset_) : std::string_view(); + } protected: friend ClimateCall; @@ -280,7 +290,9 @@ class Climate : public EntityBase { bool set_fan_mode_(ClimateFanMode mode); /// Set custom fan mode. Reset primary fan mode. Return true if fan mode has been changed. - bool set_custom_fan_mode_(const char *mode); + bool set_custom_fan_mode_(const char *mode) { return this->set_custom_fan_mode_(mode, strlen(mode)); } + bool set_custom_fan_mode_(const char *mode, size_t len); + bool set_custom_fan_mode_(std::string_view mode) { return this->set_custom_fan_mode_(mode.data(), mode.size()); } /// Clear custom fan mode. void clear_custom_fan_mode_(); @@ -288,7 +300,9 @@ class Climate : public EntityBase { bool set_preset_(ClimatePreset preset); /// Set custom preset. Reset primary preset. Return true if preset has been changed. - bool set_custom_preset_(const char *preset); + bool set_custom_preset_(const char *preset) { return this->set_custom_preset_(preset, strlen(preset)); } + bool set_custom_preset_(const char *preset, size_t len); + bool set_custom_preset_(std::string_view preset) { return this->set_custom_preset_(preset.data(), preset.size()); } /// Clear custom preset. void clear_custom_preset_(); diff --git a/esphome/components/midea/air_conditioner.cpp b/esphome/components/midea/air_conditioner.cpp index a6a8d52549..9f67296d2b 100644 --- a/esphome/components/midea/air_conditioner.cpp +++ b/esphome/components/midea/air_conditioner.cpp @@ -65,12 +65,14 @@ void AirConditioner::control(const ClimateCall &call) { if (call.get_preset().has_value()) { ctrl.preset = Converters::to_midea_preset(call.get_preset().value()); } else if (call.has_custom_preset()) { - ctrl.preset = Converters::to_midea_preset(call.get_custom_preset()); + // get_custom_preset() returns string_view; Converters expects null-terminated const char* + ctrl.preset = Converters::to_midea_preset(call.get_custom_preset().data()); } if (call.get_fan_mode().has_value()) { ctrl.fanMode = Converters::to_midea_fan_mode(call.get_fan_mode().value()); } else if (call.has_custom_fan_mode()) { - ctrl.fanMode = Converters::to_midea_fan_mode(call.get_custom_fan_mode()); + // get_custom_fan_mode() returns string_view; Converters expects null-terminated const char* + ctrl.fanMode = Converters::to_midea_fan_mode(call.get_custom_fan_mode().data()); } this->base_.control(ctrl); } diff --git a/esphome/components/mqtt/mqtt_climate.cpp b/esphome/components/mqtt/mqtt_climate.cpp index 77aabb2461..9723a44638 100644 --- a/esphome/components/mqtt/mqtt_climate.cpp +++ b/esphome/components/mqtt/mqtt_climate.cpp @@ -357,7 +357,7 @@ bool MQTTClimateComponent::publish_state_() { } } if (this->device_->has_custom_preset()) - payload = this->device_->get_custom_preset(); + payload = this->device_->get_custom_preset().data(); if (!this->publish(this->get_preset_state_topic(), payload)) success = false; } @@ -429,7 +429,7 @@ bool MQTTClimateComponent::publish_state_() { } } if (this->device_->has_custom_fan_mode()) - payload = this->device_->get_custom_fan_mode(); + payload = this->device_->get_custom_fan_mode().data(); if (!this->publish(this->get_fan_mode_state_topic(), payload)) success = false; } diff --git a/esphome/components/thermostat/thermostat_climate.cpp b/esphome/components/thermostat/thermostat_climate.cpp index d5fb259dad..4d21526225 100644 --- a/esphome/components/thermostat/thermostat_climate.cpp +++ b/esphome/components/thermostat/thermostat_climate.cpp @@ -222,7 +222,7 @@ void ThermostatClimate::control(const climate::ClimateCall &call) { if (call.has_custom_preset()) { // setup_complete_ blocks modifying/resetting the temps immediately after boot if (this->setup_complete_) { - this->change_custom_preset_(call.get_custom_preset()); + this->change_custom_preset_(call.get_custom_preset().data()); } else { // Use the base class method which handles pointer lookup internally this->set_custom_preset_(call.get_custom_preset()); @@ -1231,7 +1231,7 @@ void ThermostatClimate::change_custom_preset_(const char *custom_preset) { if (config != nullptr) { ESP_LOGV(TAG, "Custom preset %s requested", custom_preset); if (this->change_preset_internal_(*config) || !this->has_custom_preset() || - strcmp(this->get_custom_preset(), custom_preset) != 0) { + this->get_custom_preset() != custom_preset) { // Fire any preset changed trigger if defined Trigger<> *trig = this->preset_change_trigger_; // Use the base class method which handles pointer lookup and preset reset internally diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index cab177c182..0ff83a119a 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -1543,13 +1543,15 @@ std::string WebServer::climate_json_(climate::Climate *obj, JsonDetail start_con root[ESPHOME_F("fan_mode")] = PSTR_LOCAL(climate_fan_mode_to_string(obj->fan_mode.value())); } if (!traits.get_supported_custom_fan_modes().empty() && obj->has_custom_fan_mode()) { - root[ESPHOME_F("custom_fan_mode")] = obj->get_custom_fan_mode(); + // get_custom_fan_mode() returns string_view pointing to null-terminated string literals from codegen + root[ESPHOME_F("custom_fan_mode")] = obj->get_custom_fan_mode().data(); } if (traits.get_supports_presets() && obj->preset.has_value()) { root[ESPHOME_F("preset")] = PSTR_LOCAL(climate_preset_to_string(obj->preset.value())); } if (!traits.get_supported_custom_presets().empty() && obj->has_custom_preset()) { - root[ESPHOME_F("custom_preset")] = obj->get_custom_preset(); + // get_custom_preset() returns string_view pointing to null-terminated string literals from codegen + root[ESPHOME_F("custom_preset")] = obj->get_custom_preset().data(); } if (traits.get_supports_swing_modes()) { root[ESPHOME_F("swing_mode")] = PSTR_LOCAL(climate_swing_mode_to_string(obj->swing_mode)); diff --git a/tests/components/midea/common.yaml b/tests/components/midea/common.yaml index fec85aee96..957e9830c9 100644 --- a/tests/components/midea/common.yaml +++ b/tests/components/midea/common.yaml @@ -12,6 +12,25 @@ climate: x.set_mode(CLIMATE_MODE_FAN_ONLY); on_state: - logger.log: State changed! + - lambda: |- + // Test get_custom_fan_mode() returns std::string_view + if (id(midea_unit).has_custom_fan_mode()) { + auto fan_mode = id(midea_unit).get_custom_fan_mode(); + // Compare with string literal using == + if (fan_mode == "SILENT") { + ESP_LOGD("test", "Fan mode is SILENT"); + } + // Log using %.*s format for string_view + ESP_LOGD("test", "Custom fan mode: %.*s", (int) fan_mode.size(), fan_mode.data()); + } + // Test get_custom_preset() returns std::string_view + if (id(midea_unit).has_custom_preset()) { + auto preset = id(midea_unit).get_custom_preset(); + // Check if empty + if (!preset.empty()) { + ESP_LOGD("test", "Custom preset: %.*s", (int) preset.size(), preset.data()); + } + } transmitter_id: xmitr period: 1s num_attempts: 5 From 265bc55c28c8f9904e491209a676d19f3e485c96 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 9 Jan 2026 07:43:52 -1000 Subject: [PATCH 2/8] [climate] Return std::string_view from get_custom_fan_mode() and get_custom_preset() --- tests/components/thermostat/common.yaml | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/tests/components/thermostat/common.yaml b/tests/components/thermostat/common.yaml index 63bd174e14..756027f4a6 100644 --- a/tests/components/thermostat/common.yaml +++ b/tests/components/thermostat/common.yaml @@ -5,6 +5,7 @@ sensor: climate: - platform: thermostat + id: test_thermostat name: Test Thermostat sensor: thermostat_sensor humidity_sensor: thermostat_sensor @@ -15,6 +16,25 @@ climate: - name: Away default_target_temperature_low: 16°C default_target_temperature_high: 20°C + custom_preset: + - name: Eco Mode + default_target_temperature_low: 16°C + default_target_temperature_high: 22°C + - name: Sleep Mode + default_target_temperature_low: 17°C + default_target_temperature_high: 21°C + on_state: + - lambda: |- + // Test get_custom_preset() returns std::string_view + if (id(test_thermostat).has_custom_preset()) { + auto preset = id(test_thermostat).get_custom_preset(); + // Compare with string literal using == + if (preset == "Eco Mode") { + ESP_LOGD("test", "Preset is Eco Mode"); + } + // Log using %.*s format for string_view + ESP_LOGD("test", "Custom preset: %.*s", (int) preset.size(), preset.data()); + } idle_action: - logger.log: idle_action cool_action: From 56ced4a40314f9392be968c914d657936988d58d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 9 Jan 2026 07:47:42 -1000 Subject: [PATCH 3/8] [climate] Return std::string_view from get_custom_fan_mode() and get_custom_preset() --- tests/components/thermostat/common.yaml | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/tests/components/thermostat/common.yaml b/tests/components/thermostat/common.yaml index 756027f4a6..ff49c7f3ee 100644 --- a/tests/components/thermostat/common.yaml +++ b/tests/components/thermostat/common.yaml @@ -16,21 +16,15 @@ climate: - name: Away default_target_temperature_low: 16°C default_target_temperature_high: 20°C - custom_preset: - - name: Eco Mode - default_target_temperature_low: 16°C - default_target_temperature_high: 22°C - - name: Sleep Mode - default_target_temperature_low: 17°C - default_target_temperature_high: 21°C on_state: - lambda: |- // Test get_custom_preset() returns std::string_view + // "Default Preset" is a custom preset (not a standard ClimatePreset name) if (id(test_thermostat).has_custom_preset()) { auto preset = id(test_thermostat).get_custom_preset(); // Compare with string literal using == - if (preset == "Eco Mode") { - ESP_LOGD("test", "Preset is Eco Mode"); + if (preset == "Default Preset") { + ESP_LOGD("test", "Preset is Default Preset"); } // Log using %.*s format for string_view ESP_LOGD("test", "Custom preset: %.*s", (int) preset.size(), preset.data()); From 3dbca6692e9b4316303354f786881da8741dce4a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 9 Jan 2026 16:28:21 -1000 Subject: [PATCH 4/8] stringref --- esphome/components/api/api_connection.cpp | 6 ++--- .../bedjet/climate/bedjet_climate.cpp | 6 ++--- esphome/components/climate/climate.h | 27 +++++++------------ esphome/components/midea/air_conditioner.cpp | 8 +++--- esphome/components/mqtt/mqtt_climate.cpp | 4 +-- .../thermostat/thermostat_climate.cpp | 7 ++--- .../thermostat/thermostat_climate.h | 8 +++++- esphome/components/web_server/web_server.cpp | 8 +++--- tests/components/thermostat/common.yaml | 4 +-- 9 files changed, 37 insertions(+), 41 deletions(-) diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 79516666b7..62f538fd8b 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -675,15 +675,13 @@ uint16_t APIConnection::try_send_climate_state(EntityBase *entity, APIConnection if (traits.get_supports_fan_modes() && climate->fan_mode.has_value()) resp.fan_mode = static_cast(climate->fan_mode.value()); if (!traits.get_supported_custom_fan_modes().empty() && climate->has_custom_fan_mode()) { - auto mode = climate->get_custom_fan_mode(); - resp.custom_fan_mode = StringRef(mode.data(), mode.size()); + resp.custom_fan_mode = climate->get_custom_fan_mode(); } if (traits.get_supports_presets() && climate->preset.has_value()) { resp.preset = static_cast(climate->preset.value()); } if (!traits.get_supported_custom_presets().empty() && climate->has_custom_preset()) { - auto preset = climate->get_custom_preset(); - resp.custom_preset = StringRef(preset.data(), preset.size()); + resp.custom_preset = climate->get_custom_preset(); } if (traits.get_supports_swing_modes()) resp.swing_mode = static_cast(climate->swing_mode); diff --git a/esphome/components/bedjet/climate/bedjet_climate.cpp b/esphome/components/bedjet/climate/bedjet_climate.cpp index d8b2d40bb1..68a0342873 100644 --- a/esphome/components/bedjet/climate/bedjet_climate.cpp +++ b/esphome/components/bedjet/climate/bedjet_climate.cpp @@ -178,7 +178,7 @@ void BedJetClimate::control(const ClimateCall &call) { } else if (preset == "EXT HT") { result = this->parent_->button_ext_heat(); } else { - ESP_LOGW(TAG, "Unsupported preset: %.*s", (int) preset.size(), preset.data()); + ESP_LOGW(TAG, "Unsupported preset: %.*s", (int) preset.size(), preset.c_str()); return; } @@ -209,10 +209,10 @@ void BedJetClimate::control(const ClimateCall &call) { } } else if (call.has_custom_fan_mode()) { auto fan_mode = call.get_custom_fan_mode(); - auto fan_index = bedjet_fan_speed_to_step(fan_mode.data()); + auto fan_index = bedjet_fan_speed_to_step(fan_mode.c_str()); if (fan_index <= 19) { ESP_LOGV(TAG, "[%s] Converted fan mode %.*s to bedjet fan step %d", this->get_name().c_str(), - (int) fan_mode.size(), fan_mode.data(), fan_index); + (int) fan_mode.size(), fan_mode.c_str(), fan_index); bool result = this->parent_->set_fan_index(fan_index); if (result) { this->set_custom_fan_mode_(fan_mode); diff --git a/esphome/components/climate/climate.h b/esphome/components/climate/climate.h index f6d3d10a93..6fac254502 100644 --- a/esphome/components/climate/climate.h +++ b/esphome/components/climate/climate.h @@ -1,12 +1,11 @@ #pragma once -#include - #include "esphome/core/component.h" #include "esphome/core/entity_base.h" #include "esphome/core/helpers.h" #include "esphome/core/log.h" #include "esphome/core/preferences.h" +#include "esphome/core/string_ref.h" #include "climate_mode.h" #include "climate_traits.h" @@ -112,12 +111,8 @@ class ClimateCall { const optional &get_fan_mode() const; const optional &get_swing_mode() const; const optional &get_preset() const; - std::string_view get_custom_fan_mode() const { - return this->custom_fan_mode_ != nullptr ? std::string_view(this->custom_fan_mode_) : std::string_view(); - } - std::string_view get_custom_preset() const { - return this->custom_preset_ != nullptr ? std::string_view(this->custom_preset_) : std::string_view(); - } + StringRef get_custom_fan_mode() const { return StringRef::from_maybe_nullptr(this->custom_fan_mode_); } + StringRef get_custom_preset() const { return StringRef::from_maybe_nullptr(this->custom_preset_); } bool has_custom_fan_mode() const { return this->custom_fan_mode_ != nullptr; } bool has_custom_preset() const { return this->custom_preset_ != nullptr; } @@ -272,15 +267,11 @@ class Climate : public EntityBase { /// The active swing mode of the climate device. ClimateSwingMode swing_mode{CLIMATE_SWING_OFF}; - /// Get the active custom fan mode (read-only access). Returns std::string_view. - std::string_view get_custom_fan_mode() const { - return this->custom_fan_mode_ != nullptr ? std::string_view(this->custom_fan_mode_) : std::string_view(); - } + /// Get the active custom fan mode (read-only access). Returns StringRef. + StringRef get_custom_fan_mode() const { return StringRef::from_maybe_nullptr(this->custom_fan_mode_); } - /// Get the active custom preset (read-only access). Returns std::string_view. - std::string_view get_custom_preset() const { - return this->custom_preset_ != nullptr ? std::string_view(this->custom_preset_) : std::string_view(); - } + /// Get the active custom preset (read-only access). Returns StringRef. + StringRef get_custom_preset() const { return StringRef::from_maybe_nullptr(this->custom_preset_); } protected: friend ClimateCall; @@ -292,7 +283,7 @@ class Climate : public EntityBase { /// Set custom fan mode. Reset primary fan mode. Return true if fan mode has been changed. bool set_custom_fan_mode_(const char *mode) { return this->set_custom_fan_mode_(mode, strlen(mode)); } bool set_custom_fan_mode_(const char *mode, size_t len); - bool set_custom_fan_mode_(std::string_view mode) { return this->set_custom_fan_mode_(mode.data(), mode.size()); } + bool set_custom_fan_mode_(StringRef mode) { return this->set_custom_fan_mode_(mode.c_str(), mode.size()); } /// Clear custom fan mode. void clear_custom_fan_mode_(); @@ -302,7 +293,7 @@ class Climate : public EntityBase { /// Set custom preset. Reset primary preset. Return true if preset has been changed. bool set_custom_preset_(const char *preset) { return this->set_custom_preset_(preset, strlen(preset)); } bool set_custom_preset_(const char *preset, size_t len); - bool set_custom_preset_(std::string_view preset) { return this->set_custom_preset_(preset.data(), preset.size()); } + bool set_custom_preset_(StringRef preset) { return this->set_custom_preset_(preset.c_str(), preset.size()); } /// Clear custom preset. void clear_custom_preset_(); diff --git a/esphome/components/midea/air_conditioner.cpp b/esphome/components/midea/air_conditioner.cpp index 9f67296d2b..a24e41752b 100644 --- a/esphome/components/midea/air_conditioner.cpp +++ b/esphome/components/midea/air_conditioner.cpp @@ -65,14 +65,14 @@ void AirConditioner::control(const ClimateCall &call) { if (call.get_preset().has_value()) { ctrl.preset = Converters::to_midea_preset(call.get_preset().value()); } else if (call.has_custom_preset()) { - // get_custom_preset() returns string_view; Converters expects null-terminated const char* - ctrl.preset = Converters::to_midea_preset(call.get_custom_preset().data()); + // c_str() is safe as custom presets are null-terminated strings from codegen + ctrl.preset = Converters::to_midea_preset(call.get_custom_preset().c_str()); } if (call.get_fan_mode().has_value()) { ctrl.fanMode = Converters::to_midea_fan_mode(call.get_fan_mode().value()); } else if (call.has_custom_fan_mode()) { - // get_custom_fan_mode() returns string_view; Converters expects null-terminated const char* - ctrl.fanMode = Converters::to_midea_fan_mode(call.get_custom_fan_mode().data()); + // c_str() is safe as custom fan modes are null-terminated strings from codegen + ctrl.fanMode = Converters::to_midea_fan_mode(call.get_custom_fan_mode().c_str()); } this->base_.control(ctrl); } diff --git a/esphome/components/mqtt/mqtt_climate.cpp b/esphome/components/mqtt/mqtt_climate.cpp index 9723a44638..625fb715a7 100644 --- a/esphome/components/mqtt/mqtt_climate.cpp +++ b/esphome/components/mqtt/mqtt_climate.cpp @@ -357,7 +357,7 @@ bool MQTTClimateComponent::publish_state_() { } } if (this->device_->has_custom_preset()) - payload = this->device_->get_custom_preset().data(); + payload = this->device_->get_custom_preset().c_str(); if (!this->publish(this->get_preset_state_topic(), payload)) success = false; } @@ -429,7 +429,7 @@ bool MQTTClimateComponent::publish_state_() { } } if (this->device_->has_custom_fan_mode()) - payload = this->device_->get_custom_fan_mode().data(); + payload = this->device_->get_custom_fan_mode().c_str(); if (!this->publish(this->get_fan_mode_state_topic(), payload)) success = false; } diff --git a/esphome/components/thermostat/thermostat_climate.cpp b/esphome/components/thermostat/thermostat_climate.cpp index 4d21526225..0416438dcd 100644 --- a/esphome/components/thermostat/thermostat_climate.cpp +++ b/esphome/components/thermostat/thermostat_climate.cpp @@ -222,7 +222,7 @@ void ThermostatClimate::control(const climate::ClimateCall &call) { if (call.has_custom_preset()) { // setup_complete_ blocks modifying/resetting the temps immediately after boot if (this->setup_complete_) { - this->change_custom_preset_(call.get_custom_preset().data()); + this->change_custom_preset_(call.get_custom_preset()); } else { // Use the base class method which handles pointer lookup internally this->set_custom_preset_(call.get_custom_preset()); @@ -1218,11 +1218,12 @@ void ThermostatClimate::change_preset_(climate::ClimatePreset preset) { } } -void ThermostatClimate::change_custom_preset_(const char *custom_preset) { +void ThermostatClimate::change_custom_preset_(const char *custom_preset, size_t len) { // Linear search through custom preset configurations const ThermostatClimateTargetTempConfig *config = nullptr; for (const auto &entry : this->custom_preset_config_) { - if (strcmp(entry.name, custom_preset) == 0) { + // Compare first len chars, then verify entry.name ends there (same length) + if (strncmp(entry.name, custom_preset, len) == 0 && entry.name[len] == '\0') { config = &entry.config; break; } diff --git a/esphome/components/thermostat/thermostat_climate.h b/esphome/components/thermostat/thermostat_climate.h index 564b6127b3..d37c9a68a6 100644 --- a/esphome/components/thermostat/thermostat_climate.h +++ b/esphome/components/thermostat/thermostat_climate.h @@ -214,7 +214,13 @@ class ThermostatClimate : public climate::Climate, public Component { /// Change to a provided preset setting; will reset temperature, mode, fan, and swing modes accordingly void change_preset_(climate::ClimatePreset preset); /// Change to a provided custom preset setting; will reset temperature, mode, fan, and swing modes accordingly - void change_custom_preset_(const char *custom_preset); + void change_custom_preset_(const char *custom_preset) { + this->change_custom_preset_(custom_preset, strlen(custom_preset)); + } + void change_custom_preset_(const char *custom_preset, size_t len); + void change_custom_preset_(StringRef custom_preset) { + this->change_custom_preset_(custom_preset.c_str(), custom_preset.size()); + } /// Applies the temperature, mode, fan, and swing modes of the provided config. /// This is agnostic of custom vs built in preset diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index 0ff83a119a..9d98e7fa46 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -1543,15 +1543,15 @@ std::string WebServer::climate_json_(climate::Climate *obj, JsonDetail start_con root[ESPHOME_F("fan_mode")] = PSTR_LOCAL(climate_fan_mode_to_string(obj->fan_mode.value())); } if (!traits.get_supported_custom_fan_modes().empty() && obj->has_custom_fan_mode()) { - // get_custom_fan_mode() returns string_view pointing to null-terminated string literals from codegen - root[ESPHOME_F("custom_fan_mode")] = obj->get_custom_fan_mode().data(); + // c_str() is safe as custom fan modes are null-terminated strings from codegen + root[ESPHOME_F("custom_fan_mode")] = obj->get_custom_fan_mode().c_str(); } if (traits.get_supports_presets() && obj->preset.has_value()) { root[ESPHOME_F("preset")] = PSTR_LOCAL(climate_preset_to_string(obj->preset.value())); } if (!traits.get_supported_custom_presets().empty() && obj->has_custom_preset()) { - // get_custom_preset() returns string_view pointing to null-terminated string literals from codegen - root[ESPHOME_F("custom_preset")] = obj->get_custom_preset().data(); + // c_str() is safe as custom presets are null-terminated strings from codegen + root[ESPHOME_F("custom_preset")] = obj->get_custom_preset().c_str(); } if (traits.get_supports_swing_modes()) { root[ESPHOME_F("swing_mode")] = PSTR_LOCAL(climate_swing_mode_to_string(obj->swing_mode)); diff --git a/tests/components/thermostat/common.yaml b/tests/components/thermostat/common.yaml index ff49c7f3ee..69e258f2e3 100644 --- a/tests/components/thermostat/common.yaml +++ b/tests/components/thermostat/common.yaml @@ -26,8 +26,8 @@ climate: if (preset == "Default Preset") { ESP_LOGD("test", "Preset is Default Preset"); } - // Log using %.*s format for string_view - ESP_LOGD("test", "Custom preset: %.*s", (int) preset.size(), preset.data()); + // Log using %.*s format for StringRef + ESP_LOGD("test", "Custom preset: %.*s", (int) preset.size(), preset.c_str()); } idle_action: - logger.log: idle_action From 7bc970809a41a0aead0155f38ff8ec4bbb6001e3 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 9 Jan 2026 16:29:38 -1000 Subject: [PATCH 5/8] tweak comments --- esphome/components/midea/air_conditioner.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/midea/air_conditioner.cpp b/esphome/components/midea/air_conditioner.cpp index a24e41752b..bc750e3713 100644 --- a/esphome/components/midea/air_conditioner.cpp +++ b/esphome/components/midea/air_conditioner.cpp @@ -65,13 +65,13 @@ void AirConditioner::control(const ClimateCall &call) { if (call.get_preset().has_value()) { ctrl.preset = Converters::to_midea_preset(call.get_preset().value()); } else if (call.has_custom_preset()) { - // c_str() is safe as custom presets are null-terminated strings from codegen + // get_custom_preset() returns StringRef pointing to null-terminated string literals from codegen ctrl.preset = Converters::to_midea_preset(call.get_custom_preset().c_str()); } if (call.get_fan_mode().has_value()) { ctrl.fanMode = Converters::to_midea_fan_mode(call.get_fan_mode().value()); } else if (call.has_custom_fan_mode()) { - // c_str() is safe as custom fan modes are null-terminated strings from codegen + // get_custom_fan_mode() returns StringRef pointing to null-terminated string literals from codegen ctrl.fanMode = Converters::to_midea_fan_mode(call.get_custom_fan_mode().c_str()); } this->base_.control(ctrl); From 682b2104f24d84704bec7d5fd283f779080128eb Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 9 Jan 2026 16:29:38 -1000 Subject: [PATCH 6/8] tweak comments --- esphome/components/midea/air_conditioner.cpp | 4 ++-- esphome/components/web_server/web_server.cpp | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/esphome/components/midea/air_conditioner.cpp b/esphome/components/midea/air_conditioner.cpp index a24e41752b..bc750e3713 100644 --- a/esphome/components/midea/air_conditioner.cpp +++ b/esphome/components/midea/air_conditioner.cpp @@ -65,13 +65,13 @@ void AirConditioner::control(const ClimateCall &call) { if (call.get_preset().has_value()) { ctrl.preset = Converters::to_midea_preset(call.get_preset().value()); } else if (call.has_custom_preset()) { - // c_str() is safe as custom presets are null-terminated strings from codegen + // get_custom_preset() returns StringRef pointing to null-terminated string literals from codegen ctrl.preset = Converters::to_midea_preset(call.get_custom_preset().c_str()); } if (call.get_fan_mode().has_value()) { ctrl.fanMode = Converters::to_midea_fan_mode(call.get_fan_mode().value()); } else if (call.has_custom_fan_mode()) { - // c_str() is safe as custom fan modes are null-terminated strings from codegen + // get_custom_fan_mode() returns StringRef pointing to null-terminated string literals from codegen ctrl.fanMode = Converters::to_midea_fan_mode(call.get_custom_fan_mode().c_str()); } this->base_.control(ctrl); diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index 9d98e7fa46..800564685e 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -1543,14 +1543,14 @@ std::string WebServer::climate_json_(climate::Climate *obj, JsonDetail start_con root[ESPHOME_F("fan_mode")] = PSTR_LOCAL(climate_fan_mode_to_string(obj->fan_mode.value())); } if (!traits.get_supported_custom_fan_modes().empty() && obj->has_custom_fan_mode()) { - // c_str() is safe as custom fan modes are null-terminated strings from codegen + // get_custom_fan_mode() returns StringRef pointing to null-terminated string literals from codegen root[ESPHOME_F("custom_fan_mode")] = obj->get_custom_fan_mode().c_str(); } if (traits.get_supports_presets() && obj->preset.has_value()) { root[ESPHOME_F("preset")] = PSTR_LOCAL(climate_preset_to_string(obj->preset.value())); } if (!traits.get_supported_custom_presets().empty() && obj->has_custom_preset()) { - // c_str() is safe as custom presets are null-terminated strings from codegen + // get_custom_preset() returns StringRef pointing to null-terminated string literals from codegen root[ESPHOME_F("custom_preset")] = obj->get_custom_preset().c_str(); } if (traits.get_supports_swing_modes()) { From f01aeded4d1af02ac8d16dea900b6245cf0111cd Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 9 Jan 2026 16:36:17 -1000 Subject: [PATCH 7/8] tests update --- tests/components/midea/common.yaml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/components/midea/common.yaml b/tests/components/midea/common.yaml index 957e9830c9..c7b18a6701 100644 --- a/tests/components/midea/common.yaml +++ b/tests/components/midea/common.yaml @@ -13,22 +13,22 @@ climate: on_state: - logger.log: State changed! - lambda: |- - // Test get_custom_fan_mode() returns std::string_view + // Test get_custom_fan_mode() returns StringRef if (id(midea_unit).has_custom_fan_mode()) { auto fan_mode = id(midea_unit).get_custom_fan_mode(); // Compare with string literal using == if (fan_mode == "SILENT") { ESP_LOGD("test", "Fan mode is SILENT"); } - // Log using %.*s format for string_view - ESP_LOGD("test", "Custom fan mode: %.*s", (int) fan_mode.size(), fan_mode.data()); + // Log using %.*s format for StringRef + ESP_LOGD("test", "Custom fan mode: %.*s", (int) fan_mode.size(), fan_mode.c_str()); } - // Test get_custom_preset() returns std::string_view + // Test get_custom_preset() returns StringRef if (id(midea_unit).has_custom_preset()) { auto preset = id(midea_unit).get_custom_preset(); // Check if empty if (!preset.empty()) { - ESP_LOGD("test", "Custom preset: %.*s", (int) preset.size(), preset.data()); + ESP_LOGD("test", "Custom preset: %.*s", (int) preset.size(), preset.c_str()); } } transmitter_id: xmitr From 3fd31581d6bd5bb8faa72a624f8e2cb33fc75839 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 9 Jan 2026 21:59:19 -1000 Subject: [PATCH 8/8] cleanup --- esphome/components/web_server/web_server.cpp | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index f9f7c4142a..12115083f6 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -1548,15 +1548,13 @@ std::string WebServer::climate_json_(climate::Climate *obj, JsonDetail start_con root[ESPHOME_F("fan_mode")] = PSTR_LOCAL(climate_fan_mode_to_string(obj->fan_mode.value())); } if (!traits.get_supported_custom_fan_modes().empty() && obj->has_custom_fan_mode()) { - // get_custom_fan_mode() returns StringRef pointing to null-terminated string literals from codegen - root[ESPHOME_F("custom_fan_mode")] = obj->get_custom_fan_mode().c_str(); + root[ESPHOME_F("custom_fan_mode")] = obj->get_custom_fan_mode(); } if (traits.get_supports_presets() && obj->preset.has_value()) { root[ESPHOME_F("preset")] = PSTR_LOCAL(climate_preset_to_string(obj->preset.value())); } if (!traits.get_supported_custom_presets().empty() && obj->has_custom_preset()) { - // get_custom_preset() returns StringRef pointing to null-terminated string literals from codegen - root[ESPHOME_F("custom_preset")] = obj->get_custom_preset().c_str(); + root[ESPHOME_F("custom_preset")] = obj->get_custom_preset(); } if (traits.get_supports_swing_modes()) { root[ESPHOME_F("swing_mode")] = PSTR_LOCAL(climate_swing_mode_to_string(obj->swing_mode));