From 4810c361411cedc9e24c948de48ce86c3df5bc7c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 5 Nov 2025 18:49:58 -0600 Subject: [PATCH 1/7] [api] Store YAML service names in flash instead of heap MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reduces memory usage for YAML-defined API services by storing service names and argument names as pointers to string literals in flash instead of heap-allocated std::string objects. Implementation: - Created UserServiceBase for YAML services (const char* storage) - Created UserServiceDynamic for custom_api_device (std::string storage) - Updated CustomAPIDeviceService to inherit from UserServiceDynamic - UserServiceTrigger uses UserServiceBase (YAML-only) Memory savings per YAML service: - 0 args: 32 bytes (57% reduction) - 2 args: 48 bytes (60% reduction) - 5 args: 96 bytes (63% reduction) Custom API device services maintain same memory footprint (no regression). Typical ESPHome device (2-5 services): 100-240 bytes saved High-service device (10+ services): 400-800 bytes saved 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- esphome/components/api/custom_api_device.h | 4 +- esphome/components/api/user_services.h | 60 +++++++++++++++++-- .../fixtures/api_user_services_union.yaml | 59 ++++++++++++++++++ 3 files changed, 115 insertions(+), 8 deletions(-) create mode 100644 tests/integration/fixtures/api_user_services_union.yaml diff --git a/esphome/components/api/custom_api_device.h b/esphome/components/api/custom_api_device.h index d34ccfa0ce..43ea644f0c 100644 --- a/esphome/components/api/custom_api_device.h +++ b/esphome/components/api/custom_api_device.h @@ -9,11 +9,11 @@ namespace esphome::api { #ifdef USE_API_SERVICES -template class CustomAPIDeviceService : public UserServiceBase { +template class CustomAPIDeviceService : public UserServiceDynamic { public: CustomAPIDeviceService(const std::string &name, const std::array &arg_names, T *obj, void (T::*callback)(Ts...)) - : UserServiceBase(name, arg_names), obj_(obj), callback_(callback) {} + : UserServiceDynamic(name, arg_names), obj_(obj), callback_(callback) {} protected: void execute(Ts... x) override { (this->obj_->*this->callback_)(x...); } // NOLINT diff --git a/esphome/components/api/user_services.h b/esphome/components/api/user_services.h index 9ca5e1093e..4d980a148d 100644 --- a/esphome/components/api/user_services.h +++ b/esphome/components/api/user_services.h @@ -23,11 +23,13 @@ template T get_execute_arg_value(const ExecuteServiceArgument &arg); template enums::ServiceArgType to_service_arg_type(); +// Base class for YAML-defined services (most common case) +// Stores only pointers to string literals in flash - no heap allocation template class UserServiceBase : public UserServiceDescriptor { public: - UserServiceBase(std::string name, const std::array &arg_names) - : name_(std::move(name)), arg_names_(arg_names) { - this->key_ = fnv1_hash(this->name_); + UserServiceBase(const char *name, const std::array &arg_names) + : name_(name), arg_names_(arg_names) { + this->key_ = fnv1_hash(name); } ListEntitiesServicesResponse encode_list_service_response() override { @@ -47,7 +49,7 @@ template class UserServiceBase : public UserServiceDescriptor { bool execute_service(const ExecuteServiceRequest &req) override { if (req.key != this->key_) return false; - if (req.args.size() != this->arg_names_.size()) + if (req.args.size() != sizeof...(Ts)) return false; this->execute_(req.args, typename gens::type()); return true; @@ -59,14 +61,60 @@ template class UserServiceBase : public UserServiceDescriptor { this->execute((get_execute_arg_value(args[S]))...); } - std::string name_; + // Pointers to string literals in flash - no heap allocation + const char *name_; + std::array arg_names_; uint32_t key_{0}; +}; + +// Derived class for custom_api_device services (rare case) +// Stores copies of runtime-generated names +template class UserServiceDynamic : public UserServiceDescriptor { + public: + UserServiceDynamic(const std::string &name, const std::array &arg_names) + : name_(name), arg_names_(arg_names) { + this->key_ = fnv1_hash(this->name_.c_str()); + } + + ListEntitiesServicesResponse encode_list_service_response() override { + ListEntitiesServicesResponse msg; + msg.set_name(StringRef(this->name_)); + msg.key = this->key_; + std::array arg_types = {to_service_arg_type()...}; + msg.args.init(sizeof...(Ts)); + for (size_t i = 0; i < sizeof...(Ts); i++) { + auto &arg = msg.args.emplace_back(); + arg.type = arg_types[i]; + arg.set_name(StringRef(this->arg_names_[i])); + } + return msg; + } + + bool execute_service(const ExecuteServiceRequest &req) override { + if (req.key != this->key_) + return false; + if (req.args.size() != sizeof...(Ts)) + return false; + this->execute_(req.args, typename gens::type()); + return true; + } + + protected: + virtual void execute(Ts... x) = 0; + template void execute_(const ArgsContainer &args, seq type) { + this->execute((get_execute_arg_value(args[S]))...); + } + + // Heap-allocated strings for runtime-generated names + std::string name_; std::array arg_names_; + uint32_t key_{0}; }; template class UserServiceTrigger : public UserServiceBase, public Trigger { public: - UserServiceTrigger(const std::string &name, const std::array &arg_names) + // Constructor for static names (YAML-defined services - used by code generator) + UserServiceTrigger(const char *name, const std::array &arg_names) : UserServiceBase(name, arg_names) {} protected: diff --git a/tests/integration/fixtures/api_user_services_union.yaml b/tests/integration/fixtures/api_user_services_union.yaml new file mode 100644 index 0000000000..4fd9343772 --- /dev/null +++ b/tests/integration/fixtures/api_user_services_union.yaml @@ -0,0 +1,59 @@ +esphome: + name: test-user-services-union + friendly_name: Test User Services Union Storage + +esp32: + board: esp32dev + framework: + type: esp-idf + +logger: + level: DEBUG + +wifi: + ssid: "test" + password: "password" + +api: + actions: + # Test service with no arguments + - action: test_no_args + then: + - logger.log: "No args service called" + + # Test service with one argument + - action: test_one_arg + variables: + value: int + then: + - logger.log: + format: "One arg service: %d" + args: [value] + + # Test service with multiple arguments of different types + - action: test_multi_args + variables: + int_val: int + float_val: float + str_val: string + bool_val: bool + then: + - logger.log: + format: "Multi args: %d, %.2f, %s, %d" + args: [int_val, float_val, str_val.c_str(), bool_val] + + # Test service with max typical arguments + - action: test_many_args + variables: + arg1: int + arg2: int + arg3: int + arg4: string + arg5: float + then: + - logger.log: "Many args service called" + +binary_sensor: + - platform: template + name: "Test Binary Sensor" + id: test_sensor From fbc3413ed9cabb9325e0b401e4e8ac6a5d935f55 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 5 Nov 2025 23:00:40 -0600 Subject: [PATCH 2/7] free --- .../binary_sensor/binary_sensor.cpp | 2 +- esphome/components/button/button.cpp | 2 +- esphome/components/cover/cover.h | 2 +- esphome/components/datetime/date_entity.h | 2 +- esphome/components/datetime/datetime_entity.h | 2 +- esphome/components/datetime/time_entity.h | 2 +- esphome/components/event/event.h | 4 +-- esphome/components/lock/lock.h | 2 +- esphome/components/number/number.cpp | 4 +-- esphome/components/select/select.h | 2 +- esphome/components/sensor/sensor.cpp | 4 +-- esphome/components/switch/switch.cpp | 4 +-- esphome/components/text/text.h | 2 +- .../components/text_sensor/text_sensor.cpp | 4 +-- esphome/components/valve/valve.h | 2 +- esphome/core/entity_base.cpp | 29 ++++++++++--------- esphome/core/entity_base.h | 14 +++++---- 17 files changed, 43 insertions(+), 40 deletions(-) diff --git a/esphome/components/binary_sensor/binary_sensor.cpp b/esphome/components/binary_sensor/binary_sensor.cpp index 2fc8d9d01f..4659984418 100644 --- a/esphome/components/binary_sensor/binary_sensor.cpp +++ b/esphome/components/binary_sensor/binary_sensor.cpp @@ -11,7 +11,7 @@ static const char *const TAG = "binary_sensor"; void log_binary_sensor(const char *tag, const char *prefix, const char *type, BinarySensor *obj) { if (obj != nullptr) { ESP_LOGCONFIG(tag, "%s%s '%s'", prefix, type, obj->get_name().c_str()); - obj->log_device_class(tag, prefix); + log_entity_device_class(tag, prefix, obj); } } diff --git a/esphome/components/button/button.cpp b/esphome/components/button/button.cpp index 3a2624a6bf..4d8a0749e4 100644 --- a/esphome/components/button/button.cpp +++ b/esphome/components/button/button.cpp @@ -10,7 +10,7 @@ static const char *const TAG = "button"; void log_button(const char *tag, const char *prefix, const char *type, Button *obj) { if (obj != nullptr) { ESP_LOGCONFIG(tag, "%s%s '%s'", prefix, type, obj->get_name().c_str()); - obj->log_icon(tag, prefix); + log_entity_icon(tag, prefix, obj); } } diff --git a/esphome/components/cover/cover.h b/esphome/components/cover/cover.h index 3308255f9a..f412734789 100644 --- a/esphome/components/cover/cover.h +++ b/esphome/components/cover/cover.h @@ -20,7 +20,7 @@ const extern float COVER_CLOSED; if (traits_.get_is_assumed_state()) { \ ESP_LOGCONFIG(TAG, "%s Assumed State: YES", prefix); \ } \ - (obj)->log_device_class(TAG, prefix); \ + log_entity_device_class(TAG, prefix, (obj)); \ } class Cover; diff --git a/esphome/components/datetime/date_entity.h b/esphome/components/datetime/date_entity.h index ba2a64062d..6459ec6950 100644 --- a/esphome/components/datetime/date_entity.h +++ b/esphome/components/datetime/date_entity.h @@ -16,7 +16,7 @@ namespace datetime { #define LOG_DATETIME_DATE(prefix, type, obj) \ if ((obj) != nullptr) { \ ESP_LOGCONFIG(TAG, "%s%s '%s'", prefix, LOG_STR_LITERAL(type), (obj)->get_name().c_str()); \ - (obj)->log_icon(TAG, prefix); \ + log_entity_icon(TAG, prefix, (obj)); \ } class DateCall; diff --git a/esphome/components/datetime/datetime_entity.h b/esphome/components/datetime/datetime_entity.h index 9955686d8d..27d9807efe 100644 --- a/esphome/components/datetime/datetime_entity.h +++ b/esphome/components/datetime/datetime_entity.h @@ -16,7 +16,7 @@ namespace datetime { #define LOG_DATETIME_DATETIME(prefix, type, obj) \ if ((obj) != nullptr) { \ ESP_LOGCONFIG(TAG, "%s%s '%s'", prefix, LOG_STR_LITERAL(type), (obj)->get_name().c_str()); \ - (obj)->log_icon(TAG, prefix); \ + log_entity_icon(TAG, prefix, (obj)); \ } class DateTimeCall; diff --git a/esphome/components/datetime/time_entity.h b/esphome/components/datetime/time_entity.h index 30f73f3be2..887fd87df2 100644 --- a/esphome/components/datetime/time_entity.h +++ b/esphome/components/datetime/time_entity.h @@ -16,7 +16,7 @@ namespace datetime { #define LOG_DATETIME_TIME(prefix, type, obj) \ if ((obj) != nullptr) { \ ESP_LOGCONFIG(TAG, "%s%s '%s'", prefix, LOG_STR_LITERAL(type), (obj)->get_name().c_str()); \ - (obj)->log_icon(TAG, prefix); \ + log_entity_icon(TAG, prefix, (obj)); \ } class TimeCall; diff --git a/esphome/components/event/event.h b/esphome/components/event/event.h index f2a619eb38..bab46dd7c3 100644 --- a/esphome/components/event/event.h +++ b/esphome/components/event/event.h @@ -12,8 +12,8 @@ namespace event { #define LOG_EVENT(prefix, type, obj) \ if ((obj) != nullptr) { \ ESP_LOGCONFIG(TAG, "%s%s '%s'", prefix, LOG_STR_LITERAL(type), (obj)->get_name().c_str()); \ - (obj)->log_icon(TAG, prefix); \ - (obj)->log_device_class(TAG, prefix); \ + log_entity_icon(TAG, prefix, (obj)); \ + log_entity_device_class(TAG, prefix, (obj)); \ } class Event : public EntityBase, public EntityBase_DeviceClass { diff --git a/esphome/components/lock/lock.h b/esphome/components/lock/lock.h index 842c4e732b..c3e7ae9f6c 100644 --- a/esphome/components/lock/lock.h +++ b/esphome/components/lock/lock.h @@ -15,7 +15,7 @@ class Lock; #define LOG_LOCK(prefix, type, obj) \ if ((obj) != nullptr) { \ ESP_LOGCONFIG(TAG, "%s%s '%s'", prefix, LOG_STR_LITERAL(type), (obj)->get_name().c_str()); \ - (obj)->log_icon(TAG, prefix); \ + log_entity_icon(TAG, prefix, (obj)); \ if ((obj)->traits.get_assumed_state()) { \ ESP_LOGCONFIG(TAG, "%s Assumed State: YES", prefix); \ } \ diff --git a/esphome/components/number/number.cpp b/esphome/components/number/number.cpp index 85e0d41b9c..38a83cab8e 100644 --- a/esphome/components/number/number.cpp +++ b/esphome/components/number/number.cpp @@ -10,13 +10,13 @@ static const char *const TAG = "number"; void log_number(const char *tag, const char *prefix, const char *type, Number *obj) { if (obj != nullptr) { ESP_LOGCONFIG(tag, "%s%s '%s'", prefix, type, obj->get_name().c_str()); - obj->log_icon(tag, prefix); + log_entity_icon(tag, prefix, obj); if (!obj->traits.get_unit_of_measurement_ref().empty()) { ESP_LOGCONFIG(tag, "%s Unit of Measurement: '%s'", prefix, obj->traits.get_unit_of_measurement_ref().c_str()); } - obj->traits.log_device_class(tag, prefix); + log_entity_device_class(tag, prefix, &obj->traits); } } diff --git a/esphome/components/select/select.h b/esphome/components/select/select.h index f1e0db6c29..190f96c9d1 100644 --- a/esphome/components/select/select.h +++ b/esphome/components/select/select.h @@ -12,7 +12,7 @@ namespace select { #define LOG_SELECT(prefix, type, obj) \ if ((obj) != nullptr) { \ ESP_LOGCONFIG(TAG, "%s%s '%s'", prefix, LOG_STR_LITERAL(type), (obj)->get_name().c_str()); \ - (obj)->log_icon(TAG, prefix); \ + log_entity_icon(TAG, prefix, (obj)); \ } #define SUB_SELECT(name) \ diff --git a/esphome/components/sensor/sensor.cpp b/esphome/components/sensor/sensor.cpp index a472f9bec6..aaedb9ac0a 100644 --- a/esphome/components/sensor/sensor.cpp +++ b/esphome/components/sensor/sensor.cpp @@ -18,8 +18,8 @@ void log_sensor(const char *tag, const char *prefix, const char *type, Sensor *o LOG_STR_ARG(state_class_to_string(obj->get_state_class())), prefix, obj->get_unit_of_measurement_ref().c_str(), prefix, obj->get_accuracy_decimals()); - obj->log_device_class(tag, prefix); - obj->log_icon(tag, prefix); + log_entity_device_class(tag, prefix, obj); + log_entity_icon(tag, prefix, obj); if (obj->get_force_update()) { ESP_LOGV(tag, "%s Force Update: YES", prefix); diff --git a/esphome/components/switch/switch.cpp b/esphome/components/switch/switch.cpp index 659d15d883..e6d4076671 100644 --- a/esphome/components/switch/switch.cpp +++ b/esphome/components/switch/switch.cpp @@ -91,14 +91,14 @@ void log_switch(const char *tag, const char *prefix, const char *type, Switch *o LOG_STR_ARG(onoff)); // Add optional fields separately - obj->log_icon(tag, prefix); + log_entity_icon(tag, prefix, obj); if (obj->assumed_state()) { ESP_LOGCONFIG(tag, "%s Assumed State: YES", prefix); } if (obj->is_inverted()) { ESP_LOGCONFIG(tag, "%s Inverted: YES", prefix); } - obj->log_device_class(tag, prefix); + log_entity_device_class(tag, prefix, obj); } } diff --git a/esphome/components/text/text.h b/esphome/components/text/text.h index 18d5ba3f50..45c938c379 100644 --- a/esphome/components/text/text.h +++ b/esphome/components/text/text.h @@ -12,7 +12,7 @@ namespace text { #define LOG_TEXT(prefix, type, obj) \ if ((obj) != nullptr) { \ ESP_LOGCONFIG(TAG, "%s%s '%s'", prefix, LOG_STR_LITERAL(type), (obj)->get_name().c_str()); \ - (obj)->log_icon(TAG, prefix); \ + log_entity_icon(TAG, prefix, (obj)); \ } /** Base-class for all text inputs. diff --git a/esphome/components/text_sensor/text_sensor.cpp b/esphome/components/text_sensor/text_sensor.cpp index e5eea3c7af..e19ab4ae10 100644 --- a/esphome/components/text_sensor/text_sensor.cpp +++ b/esphome/components/text_sensor/text_sensor.cpp @@ -9,8 +9,8 @@ static const char *const TAG = "text_sensor"; void log_text_sensor(const char *tag, const char *prefix, const char *type, TextSensor *obj) { if (obj != nullptr) { ESP_LOGCONFIG(tag, "%s%s '%s'", prefix, type, obj->get_name().c_str()); - obj->log_device_class(tag, prefix); - obj->log_icon(tag, prefix); + log_entity_device_class(tag, prefix, obj); + log_entity_icon(tag, prefix, obj); } } diff --git a/esphome/components/valve/valve.h b/esphome/components/valve/valve.h index cd41c6d34a..48900bab21 100644 --- a/esphome/components/valve/valve.h +++ b/esphome/components/valve/valve.h @@ -19,7 +19,7 @@ const extern float VALVE_CLOSED; if (traits_.get_is_assumed_state()) { \ ESP_LOGCONFIG(TAG, "%s Assumed State: YES", prefix); \ } \ - (obj)->log_device_class(TAG, prefix); \ + log_entity_device_class(TAG, prefix, (obj)); \ } class Valve; diff --git a/esphome/core/entity_base.cpp b/esphome/core/entity_base.cpp index 404297cbfd..a4da2fce0c 100644 --- a/esphome/core/entity_base.cpp +++ b/esphome/core/entity_base.cpp @@ -45,14 +45,6 @@ void EntityBase::set_icon(const char *icon) { #endif } -void EntityBase::log_icon(const char *tag, const char *prefix) const { -#ifdef USE_ENTITY_ICON - if (!this->get_icon_ref().empty()) { - ESP_LOGCONFIG(tag, "%s Icon: '%s'", prefix, this->get_icon_ref().c_str()); - } -#endif -} - // Check if the object_id is dynamic (changes with MAC suffix) bool EntityBase::is_object_id_dynamic_() const { return !this->flags_.has_own_name && App.is_name_add_mac_suffix_enabled(); @@ -99,12 +91,6 @@ std::string EntityBase_DeviceClass::get_device_class() { void EntityBase_DeviceClass::set_device_class(const char *device_class) { this->device_class_ = device_class; } -void EntityBase_DeviceClass::log_device_class(const char *tag, const char *prefix) const { - if (!this->get_device_class_ref().empty()) { - ESP_LOGCONFIG(tag, "%s Device Class: '%s'", prefix, this->get_device_class_ref().c_str()); - } -} - std::string EntityBase_UnitOfMeasurement::get_unit_of_measurement() { if (this->unit_of_measurement_ == nullptr) return ""; @@ -114,4 +100,19 @@ void EntityBase_UnitOfMeasurement::set_unit_of_measurement(const char *unit_of_m this->unit_of_measurement_ = unit_of_measurement; } +// Helper functions for logging entity attributes +void log_entity_icon(const char *tag, const char *prefix, const EntityBase *obj) { +#ifdef USE_ENTITY_ICON + if (!obj->get_icon_ref().empty()) { + ESP_LOGCONFIG(tag, "%s Icon: '%s'", prefix, obj->get_icon_ref().c_str()); + } +#endif +} + +void log_entity_device_class(const char *tag, const char *prefix, const EntityBase_DeviceClass *obj) { + if (!obj->get_device_class_ref().empty()) { + ESP_LOGCONFIG(tag, "%s Device Class: '%s'", prefix, obj->get_device_class_ref().c_str()); + } +} + } // namespace esphome diff --git a/esphome/core/entity_base.h b/esphome/core/entity_base.h index 94436c0d47..d026f9b01b 100644 --- a/esphome/core/entity_base.h +++ b/esphome/core/entity_base.h @@ -74,9 +74,6 @@ class EntityBase { #endif } - /// Log entity icon if present (guarded by USE_ENTITY_ICON) - void log_icon(const char *tag, const char *prefix) const; - #ifdef USE_DEVICES // Get/set this entity's device id uint32_t get_device_id() const { @@ -174,9 +171,6 @@ class EntityBase_DeviceClass { // NOLINT(readability-identifier-naming) return this->device_class_ == nullptr ? EMPTY_STRING : StringRef(this->device_class_); } - /// Log entity device class if present - void log_device_class(const char *tag, const char *prefix) const; - protected: const char *device_class_{nullptr}; ///< Device class override }; @@ -254,4 +248,12 @@ template class StatefulEntityBase : public EntityBase { CallbackManager *state_callbacks_{}; }; +// Helper functions for logging entity attributes + +/// Log entity icon if present (guarded by USE_ENTITY_ICON) +void log_entity_icon(const char *tag, const char *prefix, const EntityBase *obj); + +/// Log entity device class if present +void log_entity_device_class(const char *tag, const char *prefix, const EntityBase_DeviceClass *obj); + } // namespace esphome From 1e58c400ea1d22df35d88ea3f8d3f39233a8bae8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 5 Nov 2025 23:47:28 -0600 Subject: [PATCH 3/7] Revert "free" This reverts commit fbc3413ed9cabb9325e0b401e4e8ac6a5d935f55. --- .../binary_sensor/binary_sensor.cpp | 2 +- esphome/components/button/button.cpp | 2 +- esphome/components/cover/cover.h | 2 +- esphome/components/datetime/date_entity.h | 2 +- esphome/components/datetime/datetime_entity.h | 2 +- esphome/components/datetime/time_entity.h | 2 +- esphome/components/event/event.h | 4 +-- esphome/components/lock/lock.h | 2 +- esphome/components/number/number.cpp | 4 +-- esphome/components/select/select.h | 2 +- esphome/components/sensor/sensor.cpp | 4 +-- esphome/components/switch/switch.cpp | 4 +-- esphome/components/text/text.h | 2 +- .../components/text_sensor/text_sensor.cpp | 4 +-- esphome/components/valve/valve.h | 2 +- esphome/core/entity_base.cpp | 29 +++++++++---------- esphome/core/entity_base.h | 14 ++++----- 17 files changed, 40 insertions(+), 43 deletions(-) diff --git a/esphome/components/binary_sensor/binary_sensor.cpp b/esphome/components/binary_sensor/binary_sensor.cpp index 4659984418..2fc8d9d01f 100644 --- a/esphome/components/binary_sensor/binary_sensor.cpp +++ b/esphome/components/binary_sensor/binary_sensor.cpp @@ -11,7 +11,7 @@ static const char *const TAG = "binary_sensor"; void log_binary_sensor(const char *tag, const char *prefix, const char *type, BinarySensor *obj) { if (obj != nullptr) { ESP_LOGCONFIG(tag, "%s%s '%s'", prefix, type, obj->get_name().c_str()); - log_entity_device_class(tag, prefix, obj); + obj->log_device_class(tag, prefix); } } diff --git a/esphome/components/button/button.cpp b/esphome/components/button/button.cpp index 4d8a0749e4..3a2624a6bf 100644 --- a/esphome/components/button/button.cpp +++ b/esphome/components/button/button.cpp @@ -10,7 +10,7 @@ static const char *const TAG = "button"; void log_button(const char *tag, const char *prefix, const char *type, Button *obj) { if (obj != nullptr) { ESP_LOGCONFIG(tag, "%s%s '%s'", prefix, type, obj->get_name().c_str()); - log_entity_icon(tag, prefix, obj); + obj->log_icon(tag, prefix); } } diff --git a/esphome/components/cover/cover.h b/esphome/components/cover/cover.h index f412734789..3308255f9a 100644 --- a/esphome/components/cover/cover.h +++ b/esphome/components/cover/cover.h @@ -20,7 +20,7 @@ const extern float COVER_CLOSED; if (traits_.get_is_assumed_state()) { \ ESP_LOGCONFIG(TAG, "%s Assumed State: YES", prefix); \ } \ - log_entity_device_class(TAG, prefix, (obj)); \ + (obj)->log_device_class(TAG, prefix); \ } class Cover; diff --git a/esphome/components/datetime/date_entity.h b/esphome/components/datetime/date_entity.h index 6459ec6950..ba2a64062d 100644 --- a/esphome/components/datetime/date_entity.h +++ b/esphome/components/datetime/date_entity.h @@ -16,7 +16,7 @@ namespace datetime { #define LOG_DATETIME_DATE(prefix, type, obj) \ if ((obj) != nullptr) { \ ESP_LOGCONFIG(TAG, "%s%s '%s'", prefix, LOG_STR_LITERAL(type), (obj)->get_name().c_str()); \ - log_entity_icon(TAG, prefix, (obj)); \ + (obj)->log_icon(TAG, prefix); \ } class DateCall; diff --git a/esphome/components/datetime/datetime_entity.h b/esphome/components/datetime/datetime_entity.h index 27d9807efe..9955686d8d 100644 --- a/esphome/components/datetime/datetime_entity.h +++ b/esphome/components/datetime/datetime_entity.h @@ -16,7 +16,7 @@ namespace datetime { #define LOG_DATETIME_DATETIME(prefix, type, obj) \ if ((obj) != nullptr) { \ ESP_LOGCONFIG(TAG, "%s%s '%s'", prefix, LOG_STR_LITERAL(type), (obj)->get_name().c_str()); \ - log_entity_icon(TAG, prefix, (obj)); \ + (obj)->log_icon(TAG, prefix); \ } class DateTimeCall; diff --git a/esphome/components/datetime/time_entity.h b/esphome/components/datetime/time_entity.h index 887fd87df2..30f73f3be2 100644 --- a/esphome/components/datetime/time_entity.h +++ b/esphome/components/datetime/time_entity.h @@ -16,7 +16,7 @@ namespace datetime { #define LOG_DATETIME_TIME(prefix, type, obj) \ if ((obj) != nullptr) { \ ESP_LOGCONFIG(TAG, "%s%s '%s'", prefix, LOG_STR_LITERAL(type), (obj)->get_name().c_str()); \ - log_entity_icon(TAG, prefix, (obj)); \ + (obj)->log_icon(TAG, prefix); \ } class TimeCall; diff --git a/esphome/components/event/event.h b/esphome/components/event/event.h index bab46dd7c3..f2a619eb38 100644 --- a/esphome/components/event/event.h +++ b/esphome/components/event/event.h @@ -12,8 +12,8 @@ namespace event { #define LOG_EVENT(prefix, type, obj) \ if ((obj) != nullptr) { \ ESP_LOGCONFIG(TAG, "%s%s '%s'", prefix, LOG_STR_LITERAL(type), (obj)->get_name().c_str()); \ - log_entity_icon(TAG, prefix, (obj)); \ - log_entity_device_class(TAG, prefix, (obj)); \ + (obj)->log_icon(TAG, prefix); \ + (obj)->log_device_class(TAG, prefix); \ } class Event : public EntityBase, public EntityBase_DeviceClass { diff --git a/esphome/components/lock/lock.h b/esphome/components/lock/lock.h index c3e7ae9f6c..842c4e732b 100644 --- a/esphome/components/lock/lock.h +++ b/esphome/components/lock/lock.h @@ -15,7 +15,7 @@ class Lock; #define LOG_LOCK(prefix, type, obj) \ if ((obj) != nullptr) { \ ESP_LOGCONFIG(TAG, "%s%s '%s'", prefix, LOG_STR_LITERAL(type), (obj)->get_name().c_str()); \ - log_entity_icon(TAG, prefix, (obj)); \ + (obj)->log_icon(TAG, prefix); \ if ((obj)->traits.get_assumed_state()) { \ ESP_LOGCONFIG(TAG, "%s Assumed State: YES", prefix); \ } \ diff --git a/esphome/components/number/number.cpp b/esphome/components/number/number.cpp index 38a83cab8e..85e0d41b9c 100644 --- a/esphome/components/number/number.cpp +++ b/esphome/components/number/number.cpp @@ -10,13 +10,13 @@ static const char *const TAG = "number"; void log_number(const char *tag, const char *prefix, const char *type, Number *obj) { if (obj != nullptr) { ESP_LOGCONFIG(tag, "%s%s '%s'", prefix, type, obj->get_name().c_str()); - log_entity_icon(tag, prefix, obj); + obj->log_icon(tag, prefix); if (!obj->traits.get_unit_of_measurement_ref().empty()) { ESP_LOGCONFIG(tag, "%s Unit of Measurement: '%s'", prefix, obj->traits.get_unit_of_measurement_ref().c_str()); } - log_entity_device_class(tag, prefix, &obj->traits); + obj->traits.log_device_class(tag, prefix); } } diff --git a/esphome/components/select/select.h b/esphome/components/select/select.h index 190f96c9d1..f1e0db6c29 100644 --- a/esphome/components/select/select.h +++ b/esphome/components/select/select.h @@ -12,7 +12,7 @@ namespace select { #define LOG_SELECT(prefix, type, obj) \ if ((obj) != nullptr) { \ ESP_LOGCONFIG(TAG, "%s%s '%s'", prefix, LOG_STR_LITERAL(type), (obj)->get_name().c_str()); \ - log_entity_icon(TAG, prefix, (obj)); \ + (obj)->log_icon(TAG, prefix); \ } #define SUB_SELECT(name) \ diff --git a/esphome/components/sensor/sensor.cpp b/esphome/components/sensor/sensor.cpp index aaedb9ac0a..a472f9bec6 100644 --- a/esphome/components/sensor/sensor.cpp +++ b/esphome/components/sensor/sensor.cpp @@ -18,8 +18,8 @@ void log_sensor(const char *tag, const char *prefix, const char *type, Sensor *o LOG_STR_ARG(state_class_to_string(obj->get_state_class())), prefix, obj->get_unit_of_measurement_ref().c_str(), prefix, obj->get_accuracy_decimals()); - log_entity_device_class(tag, prefix, obj); - log_entity_icon(tag, prefix, obj); + obj->log_device_class(tag, prefix); + obj->log_icon(tag, prefix); if (obj->get_force_update()) { ESP_LOGV(tag, "%s Force Update: YES", prefix); diff --git a/esphome/components/switch/switch.cpp b/esphome/components/switch/switch.cpp index e6d4076671..659d15d883 100644 --- a/esphome/components/switch/switch.cpp +++ b/esphome/components/switch/switch.cpp @@ -91,14 +91,14 @@ void log_switch(const char *tag, const char *prefix, const char *type, Switch *o LOG_STR_ARG(onoff)); // Add optional fields separately - log_entity_icon(tag, prefix, obj); + obj->log_icon(tag, prefix); if (obj->assumed_state()) { ESP_LOGCONFIG(tag, "%s Assumed State: YES", prefix); } if (obj->is_inverted()) { ESP_LOGCONFIG(tag, "%s Inverted: YES", prefix); } - log_entity_device_class(tag, prefix, obj); + obj->log_device_class(tag, prefix); } } diff --git a/esphome/components/text/text.h b/esphome/components/text/text.h index 45c938c379..18d5ba3f50 100644 --- a/esphome/components/text/text.h +++ b/esphome/components/text/text.h @@ -12,7 +12,7 @@ namespace text { #define LOG_TEXT(prefix, type, obj) \ if ((obj) != nullptr) { \ ESP_LOGCONFIG(TAG, "%s%s '%s'", prefix, LOG_STR_LITERAL(type), (obj)->get_name().c_str()); \ - log_entity_icon(TAG, prefix, (obj)); \ + (obj)->log_icon(TAG, prefix); \ } /** Base-class for all text inputs. diff --git a/esphome/components/text_sensor/text_sensor.cpp b/esphome/components/text_sensor/text_sensor.cpp index e19ab4ae10..e5eea3c7af 100644 --- a/esphome/components/text_sensor/text_sensor.cpp +++ b/esphome/components/text_sensor/text_sensor.cpp @@ -9,8 +9,8 @@ static const char *const TAG = "text_sensor"; void log_text_sensor(const char *tag, const char *prefix, const char *type, TextSensor *obj) { if (obj != nullptr) { ESP_LOGCONFIG(tag, "%s%s '%s'", prefix, type, obj->get_name().c_str()); - log_entity_device_class(tag, prefix, obj); - log_entity_icon(tag, prefix, obj); + obj->log_device_class(tag, prefix); + obj->log_icon(tag, prefix); } } diff --git a/esphome/components/valve/valve.h b/esphome/components/valve/valve.h index 48900bab21..cd41c6d34a 100644 --- a/esphome/components/valve/valve.h +++ b/esphome/components/valve/valve.h @@ -19,7 +19,7 @@ const extern float VALVE_CLOSED; if (traits_.get_is_assumed_state()) { \ ESP_LOGCONFIG(TAG, "%s Assumed State: YES", prefix); \ } \ - log_entity_device_class(TAG, prefix, (obj)); \ + (obj)->log_device_class(TAG, prefix); \ } class Valve; diff --git a/esphome/core/entity_base.cpp b/esphome/core/entity_base.cpp index a4da2fce0c..404297cbfd 100644 --- a/esphome/core/entity_base.cpp +++ b/esphome/core/entity_base.cpp @@ -45,6 +45,14 @@ void EntityBase::set_icon(const char *icon) { #endif } +void EntityBase::log_icon(const char *tag, const char *prefix) const { +#ifdef USE_ENTITY_ICON + if (!this->get_icon_ref().empty()) { + ESP_LOGCONFIG(tag, "%s Icon: '%s'", prefix, this->get_icon_ref().c_str()); + } +#endif +} + // Check if the object_id is dynamic (changes with MAC suffix) bool EntityBase::is_object_id_dynamic_() const { return !this->flags_.has_own_name && App.is_name_add_mac_suffix_enabled(); @@ -91,6 +99,12 @@ std::string EntityBase_DeviceClass::get_device_class() { void EntityBase_DeviceClass::set_device_class(const char *device_class) { this->device_class_ = device_class; } +void EntityBase_DeviceClass::log_device_class(const char *tag, const char *prefix) const { + if (!this->get_device_class_ref().empty()) { + ESP_LOGCONFIG(tag, "%s Device Class: '%s'", prefix, this->get_device_class_ref().c_str()); + } +} + std::string EntityBase_UnitOfMeasurement::get_unit_of_measurement() { if (this->unit_of_measurement_ == nullptr) return ""; @@ -100,19 +114,4 @@ void EntityBase_UnitOfMeasurement::set_unit_of_measurement(const char *unit_of_m this->unit_of_measurement_ = unit_of_measurement; } -// Helper functions for logging entity attributes -void log_entity_icon(const char *tag, const char *prefix, const EntityBase *obj) { -#ifdef USE_ENTITY_ICON - if (!obj->get_icon_ref().empty()) { - ESP_LOGCONFIG(tag, "%s Icon: '%s'", prefix, obj->get_icon_ref().c_str()); - } -#endif -} - -void log_entity_device_class(const char *tag, const char *prefix, const EntityBase_DeviceClass *obj) { - if (!obj->get_device_class_ref().empty()) { - ESP_LOGCONFIG(tag, "%s Device Class: '%s'", prefix, obj->get_device_class_ref().c_str()); - } -} - } // namespace esphome diff --git a/esphome/core/entity_base.h b/esphome/core/entity_base.h index d026f9b01b..94436c0d47 100644 --- a/esphome/core/entity_base.h +++ b/esphome/core/entity_base.h @@ -74,6 +74,9 @@ class EntityBase { #endif } + /// Log entity icon if present (guarded by USE_ENTITY_ICON) + void log_icon(const char *tag, const char *prefix) const; + #ifdef USE_DEVICES // Get/set this entity's device id uint32_t get_device_id() const { @@ -171,6 +174,9 @@ class EntityBase_DeviceClass { // NOLINT(readability-identifier-naming) return this->device_class_ == nullptr ? EMPTY_STRING : StringRef(this->device_class_); } + /// Log entity device class if present + void log_device_class(const char *tag, const char *prefix) const; + protected: const char *device_class_{nullptr}; ///< Device class override }; @@ -248,12 +254,4 @@ template class StatefulEntityBase : public EntityBase { CallbackManager *state_callbacks_{}; }; -// Helper functions for logging entity attributes - -/// Log entity icon if present (guarded by USE_ENTITY_ICON) -void log_entity_icon(const char *tag, const char *prefix, const EntityBase *obj); - -/// Log entity device class if present -void log_entity_device_class(const char *tag, const char *prefix, const EntityBase_DeviceClass *obj); - } // namespace esphome From b8e4efc1cdb4929dbb58a698bf31cea3d8e9c38e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 10 Nov 2025 22:23:02 -0600 Subject: [PATCH 4/7] manual_ip test --- tests/components/wifi/test.esp32-idf.yaml | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/tests/components/wifi/test.esp32-idf.yaml b/tests/components/wifi/test.esp32-idf.yaml index 827e4b17f7..6b3ef20963 100644 --- a/tests/components/wifi/test.esp32-idf.yaml +++ b/tests/components/wifi/test.esp32-idf.yaml @@ -3,6 +3,21 @@ psram: wifi: use_psram: true min_auth_mode: WPA + manual_ip: + static_ip: 192.168.1.100 + gateway: 192.168.1.1 + subnet: 255.255.255.0 + dns1: 1.1.1.1 + dns2: 8.8.8.8 + ap: + ssid: Fallback AP + password: fallback_password + manual_ip: + static_ip: 192.168.4.1 + gateway: 192.168.4.1 + subnet: 255.255.255.0 + +captive_portal: packages: - !include common.yaml From b017e034ee87112f930acb4975a51a760cd949f3 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 12 Nov 2025 15:00:52 -0600 Subject: [PATCH 5/7] tweaks --- esphome/components/thermostat/thermostat_climate.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/thermostat/thermostat_climate.h b/esphome/components/thermostat/thermostat_climate.h index 3fe6ef0f7c..76391f800c 100644 --- a/esphome/components/thermostat/thermostat_climate.h +++ b/esphome/components/thermostat/thermostat_climate.h @@ -89,7 +89,6 @@ class ThermostatClimate : public climate::Climate, public Component { using PresetEntry = ThermostatPresetEntry; using CustomPresetEntry = ThermostatCustomPresetEntry; - public: ThermostatClimate(); void setup() override; void dump_config() override; @@ -551,6 +550,7 @@ class ThermostatClimate : public climate::Climate, public Component { /// The set of custom preset configurations this thermostat supports (eg. "My Custom Preset") FixedVector custom_preset_config_{}; /// Default custom preset to use on start up (pointer to entry in custom_preset_config_) + private: const char *default_custom_preset_{nullptr}; }; From 86833cbc3ce6103d93053aa8109f0e577f159fc1 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 15 Nov 2025 10:20:40 -0600 Subject: [PATCH 6/7] rpeen --- esphome/cpp_generator.py | 6 +- tests/unit_tests/test_lambda_dedup.py | 183 ++++++++++++++++++++++++++ 2 files changed, 186 insertions(+), 3 deletions(-) create mode 100644 tests/unit_tests/test_lambda_dedup.py diff --git a/esphome/cpp_generator.py b/esphome/cpp_generator.py index 5a8685dd0a..fcc0ca2e43 100644 --- a/esphome/cpp_generator.py +++ b/esphome/cpp_generator.py @@ -202,7 +202,7 @@ class LambdaExpression(Expression): self.capture = capture self.return_type = safe_exp(return_type) if return_type is not None else None - def _format_body(self) -> str: + def format_body(self) -> str: """Format the lambda body with source directive and content.""" body = "" if self.source is not None: @@ -216,7 +216,7 @@ class LambdaExpression(Expression): cpp = f"[{self.capture}]({self.parameters})" if self.return_type is not None: cpp += f" -> {self.return_type}" - cpp += f" {{\n{self._format_body()}\n}}" + cpp += f" {{\n{self.format_body()}\n}}" return indent_all_but_first_and_last(cpp) @property @@ -759,7 +759,7 @@ def _try_deduplicate_lambda(lambda_expr: LambdaExpression) -> str | None: # Build the function declaration using lambda's body formatting func_declaration = ( - f"{return_str} {func_name}({param_str}) {{\n{lambda_expr._format_body()}\n}}" + f"{return_str} {func_name}({param_str}) {{\n{lambda_expr.format_body()}\n}}" ) # Store the declaration to be added later (after all variable declarations) diff --git a/tests/unit_tests/test_lambda_dedup.py b/tests/unit_tests/test_lambda_dedup.py new file mode 100644 index 0000000000..e25e8dff00 --- /dev/null +++ b/tests/unit_tests/test_lambda_dedup.py @@ -0,0 +1,183 @@ +"""Tests for lambda deduplication in cpp_generator.""" + +import pytest + +from esphome import cpp_generator as cg +from esphome.core import CORE + + +@pytest.fixture(autouse=True) +def reset_core(): + """Reset CORE.data before each test.""" + CORE.reset() + yield + CORE.reset() + + +def test_deduplicate_identical_lambdas(): + """Test that identical stateless lambdas are deduplicated.""" + # Create two identical lambda expressions + lambda1 = cg.LambdaExpression( + parts=["return 42;"], + parameters=[], + capture="", + return_type=cg.RawExpression("int"), + ) + + lambda2 = cg.LambdaExpression( + parts=["return 42;"], + parameters=[], + capture="", + return_type=cg.RawExpression("int"), + ) + + # Try to deduplicate them + func_name1 = cg._try_deduplicate_lambda(lambda1) + func_name2 = cg._try_deduplicate_lambda(lambda2) + + # Both should get the same function name (deduplication happened) + assert func_name1 == func_name2 + assert func_name1 == "shared_lambda_0" + + +def test_different_lambdas_not_deduplicated(): + """Test that different lambdas get different function names.""" + lambda1 = cg.LambdaExpression( + parts=["return 42;"], + parameters=[], + capture="", + return_type=cg.RawExpression("int"), + ) + + lambda2 = cg.LambdaExpression( + parts=["return 24;"], # Different content + parameters=[], + capture="", + return_type=cg.RawExpression("int"), + ) + + func_name1 = cg._try_deduplicate_lambda(lambda1) + func_name2 = cg._try_deduplicate_lambda(lambda2) + + # Different lambdas should get different function names + assert func_name1 != func_name2 + assert func_name1 == "shared_lambda_0" + assert func_name2 == "shared_lambda_1" + + +def test_different_return_types_not_deduplicated(): + """Test that lambdas with different return types are not deduplicated.""" + lambda1 = cg.LambdaExpression( + parts=["return 42;"], + parameters=[], + capture="", + return_type=cg.RawExpression("int"), + ) + + lambda2 = cg.LambdaExpression( + parts=["return 42;"], # Same content + parameters=[], + capture="", + return_type=cg.RawExpression("float"), # Different return type + ) + + func_name1 = cg._try_deduplicate_lambda(lambda1) + func_name2 = cg._try_deduplicate_lambda(lambda2) + + # Different return types = different functions + assert func_name1 != func_name2 + + +def test_different_parameters_not_deduplicated(): + """Test that lambdas with different parameters are not deduplicated.""" + lambda1 = cg.LambdaExpression( + parts=["return x;"], + parameters=[("int", "x")], + capture="", + return_type=cg.RawExpression("int"), + ) + + lambda2 = cg.LambdaExpression( + parts=["return x;"], # Same content + parameters=[("float", "x")], # Different parameter type + capture="", + return_type=cg.RawExpression("int"), + ) + + func_name1 = cg._try_deduplicate_lambda(lambda1) + func_name2 = cg._try_deduplicate_lambda(lambda2) + + # Different parameters = different functions + assert func_name1 != func_name2 + + +def test_flush_lambda_dedup_declarations(): + """Test that deferred declarations are properly stored for later flushing.""" + # Create a lambda which will create a deferred declaration + lambda1 = cg.LambdaExpression( + parts=["return 42;"], + parameters=[], + capture="", + return_type=cg.RawExpression("int"), + ) + + cg._try_deduplicate_lambda(lambda1) + + # Check that declaration was stored + assert cg._KEY_LAMBDA_DEDUP_DECLARATIONS in CORE.data + assert len(CORE.data[cg._KEY_LAMBDA_DEDUP_DECLARATIONS]) == 1 + + # Verify the declaration content is correct + declaration = CORE.data[cg._KEY_LAMBDA_DEDUP_DECLARATIONS][0] + assert "shared_lambda_0" in declaration + assert "return 42;" in declaration + + # Note: The actual flushing happens via CORE.add_job with FINAL priority + # during real code generation, so we don't test that here + + +def test_shared_function_lambda_expression(): + """Test SharedFunctionLambdaExpression behaves correctly.""" + shared_lambda = cg.SharedFunctionLambdaExpression( + func_name="shared_lambda_0", + parameters=[], + return_type=cg.RawExpression("int"), + ) + + # Should output just the function name + assert str(shared_lambda) == "shared_lambda_0" + + # Should have empty capture (stateless) + assert shared_lambda.capture == "" + + # Should have empty content (just a reference) + assert shared_lambda.content == "" + + +def test_lambda_deduplication_counter(): + """Test that lambda counter increments correctly.""" + # Create 3 different lambdas + for i in range(3): + lambda_expr = cg.LambdaExpression( + parts=[f"return {i};"], + parameters=[], + capture="", + return_type=cg.RawExpression("int"), + ) + func_name = cg._try_deduplicate_lambda(lambda_expr) + assert func_name == f"shared_lambda_{i}" + + +def test_lambda_format_body(): + """Test that format_body correctly formats lambda body with source.""" + # Without source + lambda1 = cg.LambdaExpression( + parts=["return 42;"], + parameters=[], + capture="", + return_type=None, + source=None, + ) + assert lambda1.format_body() == "return 42;" + + # With source would need a proper source object, skip for now From 3dd570fdd0f335854a5f9da3a82f73bf2241ecf8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 15 Nov 2025 10:42:44 -0600 Subject: [PATCH 7/7] address bot review --- esphome/cpp_generator.py | 20 ++++++------ tests/component_tests/text/test_text.py | 4 +-- tests/unit_tests/test_lambda_dedup.py | 41 +++++++++++++++++++------ 3 files changed, 42 insertions(+), 23 deletions(-) diff --git a/esphome/cpp_generator.py b/esphome/cpp_generator.py index fcc0ca2e43..2904cae796 100644 --- a/esphome/cpp_generator.py +++ b/esphome/cpp_generator.py @@ -716,18 +716,17 @@ async def get_variable_with_full_id(id_: ID) -> tuple[ID, "MockObj"]: return await CORE.get_variable_with_full_id(id_) -def _try_deduplicate_lambda(lambda_expr: LambdaExpression) -> str | None: - """Try to deduplicate a lambda expression. +def _get_shared_lambda_name(lambda_expr: LambdaExpression) -> str: + """Get the shared function name for a lambda expression. - If an identical lambda was already generated, returns the name of the - shared function. Otherwise, creates a new shared function and stores it. + If an identical lambda was already generated, returns the existing shared + function name. Otherwise, creates a new shared function and returns its name. Args: - lambda_expr: The lambda expression to potentially deduplicate + lambda_expr: The lambda expression to deduplicate Returns: - The name of the shared function if this lambda should be deduplicated, - None if this is the first occurrence (caller should use original lambda) + The name of the shared function for this lambda (either existing or newly created) """ # Create a unique key from the lambda content, parameters, and return type content = lambda_expr.content @@ -837,10 +836,9 @@ async def process_lambda( lambda_expr = LambdaExpression( parts, parameters, capture, return_type, location ) - func_name = _try_deduplicate_lambda(lambda_expr) - if func_name is not None: - # Return a shared function reference instead of inline lambda - return SharedFunctionLambdaExpression(func_name, parameters, return_type) + func_name = _get_shared_lambda_name(lambda_expr) + # Return a shared function reference instead of inline lambda + return SharedFunctionLambdaExpression(func_name, parameters, return_type) return LambdaExpression(parts, parameters, capture, return_type, location) diff --git a/tests/component_tests/text/test_text.py b/tests/component_tests/text/test_text.py index b2318bd416..ffc0fd780a 100644 --- a/tests/component_tests/text/test_text.py +++ b/tests/component_tests/text/test_text.py @@ -1,4 +1,4 @@ -"""Tests for the binary sensor component.""" +"""Tests for the text component.""" from esphome.core import CORE @@ -58,7 +58,7 @@ def test_text_config_value_mode_set(generate_main): assert "it_3->traits.set_mode(text::TEXT_MODE_PASSWORD);" in main_cpp -def test_text_config_lamda_is_set(generate_main): +def test_text_config_lambda_is_set(generate_main) -> None: """ Test if lambda is set for lambda mode (optimized with stateless lambda and deduplication) """ diff --git a/tests/unit_tests/test_lambda_dedup.py b/tests/unit_tests/test_lambda_dedup.py index ec4c0d29c9..ebc217f308 100644 --- a/tests/unit_tests/test_lambda_dedup.py +++ b/tests/unit_tests/test_lambda_dedup.py @@ -22,8 +22,8 @@ def test_deduplicate_identical_lambdas() -> None: ) # Try to deduplicate them - func_name1 = cg._try_deduplicate_lambda(lambda1) - func_name2 = cg._try_deduplicate_lambda(lambda2) + func_name1 = cg._get_shared_lambda_name(lambda1) + func_name2 = cg._get_shared_lambda_name(lambda2) # Both should get the same function name (deduplication happened) assert func_name1 == func_name2 @@ -46,8 +46,8 @@ def test_different_lambdas_not_deduplicated() -> None: return_type=cg.RawExpression("int"), ) - func_name1 = cg._try_deduplicate_lambda(lambda1) - func_name2 = cg._try_deduplicate_lambda(lambda2) + func_name1 = cg._get_shared_lambda_name(lambda1) + func_name2 = cg._get_shared_lambda_name(lambda2) # Different lambdas should get different function names assert func_name1 != func_name2 @@ -71,8 +71,8 @@ def test_different_return_types_not_deduplicated() -> None: return_type=cg.RawExpression("float"), # Different return type ) - func_name1 = cg._try_deduplicate_lambda(lambda1) - func_name2 = cg._try_deduplicate_lambda(lambda2) + func_name1 = cg._get_shared_lambda_name(lambda1) + func_name2 = cg._get_shared_lambda_name(lambda2) # Different return types = different functions assert func_name1 != func_name2 @@ -94,8 +94,8 @@ def test_different_parameters_not_deduplicated() -> None: return_type=cg.RawExpression("int"), ) - func_name1 = cg._try_deduplicate_lambda(lambda1) - func_name2 = cg._try_deduplicate_lambda(lambda2) + func_name1 = cg._get_shared_lambda_name(lambda1) + func_name2 = cg._get_shared_lambda_name(lambda2) # Different parameters = different functions assert func_name1 != func_name2 @@ -111,7 +111,7 @@ def test_flush_lambda_dedup_declarations() -> None: return_type=cg.RawExpression("int"), ) - cg._try_deduplicate_lambda(lambda1) + cg._get_shared_lambda_name(lambda1) # Check that declaration was stored assert cg._KEY_LAMBDA_DEDUP_DECLARATIONS in CORE.data @@ -154,7 +154,7 @@ def test_lambda_deduplication_counter() -> None: capture="", return_type=cg.RawExpression("int"), ) - func_name = cg._try_deduplicate_lambda(lambda_expr) + func_name = cg._get_shared_lambda_name(lambda_expr) assert func_name == f"shared_lambda_{i}" @@ -171,3 +171,24 @@ def test_lambda_format_body() -> None: assert lambda1.format_body() == "return 42;" # With source would need a proper source object, skip for now + + +def test_stateful_lambdas_not_deduplicated() -> None: + """Test that stateful lambdas (non-empty capture) are not deduplicated.""" + # _get_shared_lambda_name is only called for stateless lambdas (capture == "") + # Stateful lambdas bypass deduplication entirely in process_lambda + + # Verify that a stateful lambda would NOT get deduplicated + # by checking it's not in the stateless dedup cache + stateful_lambda = cg.LambdaExpression( + parts=["return x + y;"], + parameters=[], + capture="=", # Non-empty capture means stateful + return_type=cg.RawExpression("int"), + ) + + # Stateful lambdas should NOT be passed to _get_shared_lambda_name + # This is enforced by the `if capture == ""` check in process_lambda + # We verify the lambda has a non-empty capture + assert stateful_lambda.capture != "" + assert stateful_lambda.capture == "="