mirror of
				https://github.com/esphome/esphome.git
				synced 2025-10-30 22:53:59 +00:00 
			
		
		
		
	Merge branch 'dev' of https://github.com/esphome/esphome into usb-uart
This commit is contained in:
		| @@ -16,7 +16,12 @@ from esphome.const import ( | ||||
|     CONF_UPDATE_INTERVAL, | ||||
| ) | ||||
| from esphome.core import ID | ||||
| from esphome.cpp_generator import MockObj, MockObjClass, TemplateArgsType | ||||
| from esphome.cpp_generator import ( | ||||
|     LambdaExpression, | ||||
|     MockObj, | ||||
|     MockObjClass, | ||||
|     TemplateArgsType, | ||||
| ) | ||||
| from esphome.schema_extractors import SCHEMA_EXTRACT, schema_extractor | ||||
| from esphome.types import ConfigType | ||||
| from esphome.util import Registry | ||||
| @@ -87,6 +92,7 @@ def validate_potentially_or_condition(value): | ||||
|  | ||||
| DelayAction = cg.esphome_ns.class_("DelayAction", Action, cg.Component) | ||||
| LambdaAction = cg.esphome_ns.class_("LambdaAction", Action) | ||||
| StatelessLambdaAction = cg.esphome_ns.class_("StatelessLambdaAction", Action) | ||||
| IfAction = cg.esphome_ns.class_("IfAction", Action) | ||||
| WhileAction = cg.esphome_ns.class_("WhileAction", Action) | ||||
| RepeatAction = cg.esphome_ns.class_("RepeatAction", Action) | ||||
| @@ -97,9 +103,40 @@ ResumeComponentAction = cg.esphome_ns.class_("ResumeComponentAction", Action) | ||||
| Automation = cg.esphome_ns.class_("Automation") | ||||
|  | ||||
| LambdaCondition = cg.esphome_ns.class_("LambdaCondition", Condition) | ||||
| StatelessLambdaCondition = cg.esphome_ns.class_("StatelessLambdaCondition", Condition) | ||||
| ForCondition = cg.esphome_ns.class_("ForCondition", Condition, cg.Component) | ||||
|  | ||||
|  | ||||
| def new_lambda_pvariable( | ||||
|     id_obj: ID, | ||||
|     lambda_expr: LambdaExpression, | ||||
|     stateless_class: MockObjClass, | ||||
|     template_arg: cg.TemplateArguments | None = None, | ||||
| ) -> MockObj: | ||||
|     """Create Pvariable for lambda, using stateless class if applicable. | ||||
|  | ||||
|     Combines ID selection and Pvariable creation in one call. For stateless | ||||
|     lambdas (empty capture), uses function pointer instead of std::function. | ||||
|  | ||||
|     Args: | ||||
|         id_obj: The ID object (action_id, condition_id, or filter_id) | ||||
|         lambda_expr: The lambda expression object | ||||
|         stateless_class: The stateless class to use for stateless lambdas | ||||
|         template_arg: Optional template arguments (for actions/conditions) | ||||
|  | ||||
|     Returns: | ||||
|         The created Pvariable | ||||
|     """ | ||||
|     # For stateless lambdas, use function pointer instead of std::function | ||||
|     if lambda_expr.capture == "": | ||||
|         id_obj = id_obj.copy() | ||||
|         id_obj.type = stateless_class | ||||
|  | ||||
|     if template_arg is not None: | ||||
|         return cg.new_Pvariable(id_obj, template_arg, lambda_expr) | ||||
|     return cg.new_Pvariable(id_obj, lambda_expr) | ||||
|  | ||||
|  | ||||
| def validate_automation(extra_schema=None, extra_validators=None, single=False): | ||||
|     if extra_schema is None: | ||||
|         extra_schema = {} | ||||
| @@ -240,7 +277,9 @@ async def lambda_condition_to_code( | ||||
|     args: TemplateArgsType, | ||||
| ) -> MockObj: | ||||
|     lambda_ = await cg.process_lambda(config, args, return_type=bool) | ||||
|     return cg.new_Pvariable(condition_id, template_arg, lambda_) | ||||
|     return new_lambda_pvariable( | ||||
|         condition_id, lambda_, StatelessLambdaCondition, template_arg | ||||
|     ) | ||||
|  | ||||
|  | ||||
| @register_condition( | ||||
| @@ -406,7 +445,7 @@ async def lambda_action_to_code( | ||||
|     args: TemplateArgsType, | ||||
| ) -> MockObj: | ||||
|     lambda_ = await cg.process_lambda(config, args, return_type=cg.void) | ||||
|     return cg.new_Pvariable(action_id, template_arg, lambda_) | ||||
|     return new_lambda_pvariable(action_id, lambda_, StatelessLambdaAction, template_arg) | ||||
|  | ||||
|  | ||||
| @register_action( | ||||
|   | ||||
| @@ -62,6 +62,7 @@ from esphome.cpp_types import (  # noqa: F401 | ||||
|     EntityBase, | ||||
|     EntityCategory, | ||||
|     ESPTime, | ||||
|     FixedVector, | ||||
|     GPIOPin, | ||||
|     InternalGPIOPin, | ||||
|     JsonObject, | ||||
|   | ||||
| @@ -71,10 +71,12 @@ SERVICE_ARG_NATIVE_TYPES = { | ||||
|     "int": cg.int32, | ||||
|     "float": float, | ||||
|     "string": cg.std_string, | ||||
|     "bool[]": cg.std_vector.template(bool), | ||||
|     "int[]": cg.std_vector.template(cg.int32), | ||||
|     "float[]": cg.std_vector.template(float), | ||||
|     "string[]": cg.std_vector.template(cg.std_string), | ||||
|     "bool[]": cg.FixedVector.template(bool).operator("const").operator("ref"), | ||||
|     "int[]": cg.FixedVector.template(cg.int32).operator("const").operator("ref"), | ||||
|     "float[]": cg.FixedVector.template(float).operator("const").operator("ref"), | ||||
|     "string[]": cg.FixedVector.template(cg.std_string) | ||||
|     .operator("const") | ||||
|     .operator("ref"), | ||||
| } | ||||
| CONF_ENCRYPTION = "encryption" | ||||
| CONF_BATCH_DELAY = "batch_delay" | ||||
| @@ -258,6 +260,10 @@ async def to_code(config): | ||||
|     if config.get(CONF_ACTIONS) or config[CONF_CUSTOM_SERVICES]: | ||||
|         cg.add_define("USE_API_SERVICES") | ||||
|  | ||||
|     # Set USE_API_CUSTOM_SERVICES if external components need dynamic service registration | ||||
|     if config[CONF_CUSTOM_SERVICES]: | ||||
|         cg.add_define("USE_API_CUSTOM_SERVICES") | ||||
|  | ||||
|     if config[CONF_HOMEASSISTANT_SERVICES]: | ||||
|         cg.add_define("USE_API_HOMEASSISTANT_SERVICES") | ||||
|  | ||||
| @@ -265,6 +271,8 @@ async def to_code(config): | ||||
|         cg.add_define("USE_API_HOMEASSISTANT_STATES") | ||||
|  | ||||
|     if actions := config.get(CONF_ACTIONS, []): | ||||
|         # Collect all triggers first, then register all at once with initializer_list | ||||
|         triggers: list[cg.Pvariable] = [] | ||||
|         for conf in actions: | ||||
|             template_args = [] | ||||
|             func_args = [] | ||||
| @@ -278,8 +286,10 @@ async def to_code(config): | ||||
|             trigger = cg.new_Pvariable( | ||||
|                 conf[CONF_TRIGGER_ID], templ, conf[CONF_ACTION], service_arg_names | ||||
|             ) | ||||
|             cg.add(var.register_user_service(trigger)) | ||||
|             triggers.append(trigger) | ||||
|             await automation.build_automation(trigger, func_args, conf) | ||||
|         # Register all services at once - single allocation, no reallocations | ||||
|         cg.add(var.initialize_user_services(triggers)) | ||||
|  | ||||
|     if CONF_ON_CLIENT_CONNECTED in config: | ||||
|         cg.add_define("USE_API_CLIENT_CONNECTED_TRIGGER") | ||||
|   | ||||
| @@ -125,8 +125,14 @@ class APIServer : public Component, public Controller { | ||||
| #endif  // USE_API_HOMEASSISTANT_ACTION_RESPONSES | ||||
| #endif  // USE_API_HOMEASSISTANT_SERVICES | ||||
| #ifdef USE_API_SERVICES | ||||
|   void initialize_user_services(std::initializer_list<UserServiceDescriptor *> services) { | ||||
|     this->user_services_.assign(services); | ||||
|   } | ||||
| #ifdef USE_API_CUSTOM_SERVICES | ||||
|   // Only compile push_back method when custom_services: true (external components) | ||||
|   void register_user_service(UserServiceDescriptor *descriptor) { this->user_services_.push_back(descriptor); } | ||||
| #endif | ||||
| #endif | ||||
| #ifdef USE_HOMEASSISTANT_TIME | ||||
|   void request_time(); | ||||
| #endif | ||||
|   | ||||
| @@ -53,8 +53,14 @@ class CustomAPIDevice { | ||||
|   template<typename T, typename... Ts> | ||||
|   void register_service(void (T::*callback)(Ts...), const std::string &name, | ||||
|                         const std::array<std::string, sizeof...(Ts)> &arg_names) { | ||||
| #ifdef USE_API_CUSTOM_SERVICES | ||||
|     auto *service = new CustomAPIDeviceService<T, Ts...>(name, arg_names, (T *) this, callback);  // NOLINT | ||||
|     global_api_server->register_user_service(service); | ||||
| #else | ||||
|     static_assert( | ||||
|         sizeof(T) == 0, | ||||
|         "register_service() requires 'custom_services: true' in the 'api:' section of your YAML configuration"); | ||||
| #endif | ||||
|   } | ||||
| #else | ||||
|   template<typename T, typename... Ts> | ||||
| @@ -86,8 +92,14 @@ class CustomAPIDevice { | ||||
|    */ | ||||
| #ifdef USE_API_SERVICES | ||||
|   template<typename T> void register_service(void (T::*callback)(), const std::string &name) { | ||||
| #ifdef USE_API_CUSTOM_SERVICES | ||||
|     auto *service = new CustomAPIDeviceService<T>(name, {}, (T *) this, callback);  // NOLINT | ||||
|     global_api_server->register_user_service(service); | ||||
| #else | ||||
|     static_assert( | ||||
|         sizeof(T) == 0, | ||||
|         "register_service() requires 'custom_services: true' in the 'api:' section of your YAML configuration"); | ||||
| #endif | ||||
|   } | ||||
| #else | ||||
|   template<typename T> void register_service(void (T::*callback)(), const std::string &name) { | ||||
|   | ||||
| @@ -11,23 +11,58 @@ template<> int32_t get_execute_arg_value<int32_t>(const ExecuteServiceArgument & | ||||
| } | ||||
| template<> float get_execute_arg_value<float>(const ExecuteServiceArgument &arg) { return arg.float_; } | ||||
| template<> std::string get_execute_arg_value<std::string>(const ExecuteServiceArgument &arg) { return arg.string_; } | ||||
|  | ||||
| // Legacy std::vector versions for external components using custom_api_device.h - optimized with reserve | ||||
| template<> std::vector<bool> get_execute_arg_value<std::vector<bool>>(const ExecuteServiceArgument &arg) { | ||||
|   return std::vector<bool>(arg.bool_array.begin(), arg.bool_array.end()); | ||||
|   std::vector<bool> result; | ||||
|   result.reserve(arg.bool_array.size()); | ||||
|   result.insert(result.end(), arg.bool_array.begin(), arg.bool_array.end()); | ||||
|   return result; | ||||
| } | ||||
| template<> std::vector<int32_t> get_execute_arg_value<std::vector<int32_t>>(const ExecuteServiceArgument &arg) { | ||||
|   return std::vector<int32_t>(arg.int_array.begin(), arg.int_array.end()); | ||||
|   std::vector<int32_t> result; | ||||
|   result.reserve(arg.int_array.size()); | ||||
|   result.insert(result.end(), arg.int_array.begin(), arg.int_array.end()); | ||||
|   return result; | ||||
| } | ||||
| template<> std::vector<float> get_execute_arg_value<std::vector<float>>(const ExecuteServiceArgument &arg) { | ||||
|   return std::vector<float>(arg.float_array.begin(), arg.float_array.end()); | ||||
|   std::vector<float> result; | ||||
|   result.reserve(arg.float_array.size()); | ||||
|   result.insert(result.end(), arg.float_array.begin(), arg.float_array.end()); | ||||
|   return result; | ||||
| } | ||||
| template<> std::vector<std::string> get_execute_arg_value<std::vector<std::string>>(const ExecuteServiceArgument &arg) { | ||||
|   return std::vector<std::string>(arg.string_array.begin(), arg.string_array.end()); | ||||
|   std::vector<std::string> result; | ||||
|   result.reserve(arg.string_array.size()); | ||||
|   result.insert(result.end(), arg.string_array.begin(), arg.string_array.end()); | ||||
|   return result; | ||||
| } | ||||
|  | ||||
| // New FixedVector const reference versions for YAML-generated services - zero-copy | ||||
| template<> | ||||
| const FixedVector<bool> &get_execute_arg_value<const FixedVector<bool> &>(const ExecuteServiceArgument &arg) { | ||||
|   return arg.bool_array; | ||||
| } | ||||
| template<> | ||||
| const FixedVector<int32_t> &get_execute_arg_value<const FixedVector<int32_t> &>(const ExecuteServiceArgument &arg) { | ||||
|   return arg.int_array; | ||||
| } | ||||
| template<> | ||||
| const FixedVector<float> &get_execute_arg_value<const FixedVector<float> &>(const ExecuteServiceArgument &arg) { | ||||
|   return arg.float_array; | ||||
| } | ||||
| template<> | ||||
| const FixedVector<std::string> &get_execute_arg_value<const FixedVector<std::string> &>( | ||||
|     const ExecuteServiceArgument &arg) { | ||||
|   return arg.string_array; | ||||
| } | ||||
|  | ||||
| template<> enums::ServiceArgType to_service_arg_type<bool>() { return enums::SERVICE_ARG_TYPE_BOOL; } | ||||
| template<> enums::ServiceArgType to_service_arg_type<int32_t>() { return enums::SERVICE_ARG_TYPE_INT; } | ||||
| template<> enums::ServiceArgType to_service_arg_type<float>() { return enums::SERVICE_ARG_TYPE_FLOAT; } | ||||
| template<> enums::ServiceArgType to_service_arg_type<std::string>() { return enums::SERVICE_ARG_TYPE_STRING; } | ||||
|  | ||||
| // Legacy std::vector versions for external components using custom_api_device.h | ||||
| template<> enums::ServiceArgType to_service_arg_type<std::vector<bool>>() { return enums::SERVICE_ARG_TYPE_BOOL_ARRAY; } | ||||
| template<> enums::ServiceArgType to_service_arg_type<std::vector<int32_t>>() { | ||||
|   return enums::SERVICE_ARG_TYPE_INT_ARRAY; | ||||
| @@ -39,4 +74,18 @@ template<> enums::ServiceArgType to_service_arg_type<std::vector<std::string>>() | ||||
|   return enums::SERVICE_ARG_TYPE_STRING_ARRAY; | ||||
| } | ||||
|  | ||||
| // New FixedVector const reference versions for YAML-generated services | ||||
| template<> enums::ServiceArgType to_service_arg_type<const FixedVector<bool> &>() { | ||||
|   return enums::SERVICE_ARG_TYPE_BOOL_ARRAY; | ||||
| } | ||||
| template<> enums::ServiceArgType to_service_arg_type<const FixedVector<int32_t> &>() { | ||||
|   return enums::SERVICE_ARG_TYPE_INT_ARRAY; | ||||
| } | ||||
| template<> enums::ServiceArgType to_service_arg_type<const FixedVector<float> &>() { | ||||
|   return enums::SERVICE_ARG_TYPE_FLOAT_ARRAY; | ||||
| } | ||||
| template<> enums::ServiceArgType to_service_arg_type<const FixedVector<std::string> &>() { | ||||
|   return enums::SERVICE_ARG_TYPE_STRING_ARRAY; | ||||
| } | ||||
|  | ||||
| }  // namespace esphome::api | ||||
|   | ||||
| @@ -155,6 +155,7 @@ DelayedOffFilter = binary_sensor_ns.class_("DelayedOffFilter", Filter, cg.Compon | ||||
| InvertFilter = binary_sensor_ns.class_("InvertFilter", Filter) | ||||
| AutorepeatFilter = binary_sensor_ns.class_("AutorepeatFilter", Filter, cg.Component) | ||||
| LambdaFilter = binary_sensor_ns.class_("LambdaFilter", Filter) | ||||
| StatelessLambdaFilter = binary_sensor_ns.class_("StatelessLambdaFilter", Filter) | ||||
| SettleFilter = binary_sensor_ns.class_("SettleFilter", Filter, cg.Component) | ||||
|  | ||||
| _LOGGER = getLogger(__name__) | ||||
| @@ -299,7 +300,7 @@ async def lambda_filter_to_code(config, filter_id): | ||||
|     lambda_ = await cg.process_lambda( | ||||
|         config, [(bool, "x")], return_type=cg.optional.template(bool) | ||||
|     ) | ||||
|     return cg.new_Pvariable(filter_id, lambda_) | ||||
|     return automation.new_lambda_pvariable(filter_id, lambda_, StatelessLambdaFilter) | ||||
|  | ||||
|  | ||||
| @register_filter( | ||||
|   | ||||
| @@ -111,6 +111,21 @@ class LambdaFilter : public Filter { | ||||
|   std::function<optional<bool>(bool)> f_; | ||||
| }; | ||||
|  | ||||
| /** Optimized lambda filter for stateless lambdas (no capture). | ||||
|  * | ||||
|  * Uses function pointer instead of std::function to reduce memory overhead. | ||||
|  * Memory: 4 bytes (function pointer on 32-bit) vs 32 bytes (std::function). | ||||
|  */ | ||||
| class StatelessLambdaFilter : public Filter { | ||||
|  public: | ||||
|   explicit StatelessLambdaFilter(optional<bool> (*f)(bool)) : f_(f) {} | ||||
|  | ||||
|   optional<bool> new_value(bool value) override { return this->f_(value); } | ||||
|  | ||||
|  protected: | ||||
|   optional<bool> (*f_)(bool); | ||||
| }; | ||||
|  | ||||
| class SettleFilter : public Filter, public Component { | ||||
|  public: | ||||
|   optional<bool> new_value(bool value) override; | ||||
|   | ||||
| @@ -40,13 +40,13 @@ class ESP32InternalGPIOPin : public InternalGPIOPin { | ||||
|   // - 3 bytes for members below | ||||
|   // - 1 byte padding for alignment | ||||
|   // - 4 bytes for vtable pointer | ||||
|   uint8_t pin_;        // GPIO pin number (0-255, actual max ~54 on ESP32) | ||||
|   gpio::Flags flags_;  // GPIO flags (1 byte) | ||||
|   uint8_t pin_;          // GPIO pin number (0-255, actual max ~54 on ESP32) | ||||
|   gpio::Flags flags_{};  // GPIO flags (1 byte) | ||||
|   struct PinFlags { | ||||
|     uint8_t inverted : 1;        // Invert pin logic (1 bit) | ||||
|     uint8_t drive_strength : 2;  // Drive strength 0-3 (2 bits) | ||||
|     uint8_t reserved : 5;        // Reserved for future use (5 bits) | ||||
|   } pin_flags_;                  // Total: 1 byte | ||||
|   } pin_flags_{};                // Total: 1 byte | ||||
|   // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) | ||||
|   static bool isr_service_installed; | ||||
| }; | ||||
|   | ||||
| @@ -223,7 +223,10 @@ async def esp32_pin_to_code(config): | ||||
|     var = cg.new_Pvariable(config[CONF_ID]) | ||||
|     num = config[CONF_NUMBER] | ||||
|     cg.add(var.set_pin(getattr(gpio_num_t, f"GPIO_NUM_{num}"))) | ||||
|     cg.add(var.set_inverted(config[CONF_INVERTED])) | ||||
|     # Only set if true to avoid bloating setup() function | ||||
|     # (inverted bit in pin_flags_ bitfield is zero-initialized to false) | ||||
|     if config[CONF_INVERTED]: | ||||
|         cg.add(var.set_inverted(True)) | ||||
|     if CONF_DRIVE_STRENGTH in config: | ||||
|         cg.add(var.set_drive_strength(config[CONF_DRIVE_STRENGTH])) | ||||
|     cg.add(var.set_flags(pins.gpio_flags_expr(config[CONF_MODE]))) | ||||
|   | ||||
| @@ -29,8 +29,8 @@ class ESP8266GPIOPin : public InternalGPIOPin { | ||||
|   void attach_interrupt(void (*func)(void *), void *arg, gpio::InterruptType type) const override; | ||||
|  | ||||
|   uint8_t pin_; | ||||
|   bool inverted_; | ||||
|   gpio::Flags flags_; | ||||
|   bool inverted_{}; | ||||
|   gpio::Flags flags_{}; | ||||
| }; | ||||
|  | ||||
| }  // namespace esp8266 | ||||
|   | ||||
| @@ -165,7 +165,10 @@ async def esp8266_pin_to_code(config): | ||||
|     num = config[CONF_NUMBER] | ||||
|     mode = config[CONF_MODE] | ||||
|     cg.add(var.set_pin(num)) | ||||
|     cg.add(var.set_inverted(config[CONF_INVERTED])) | ||||
|     # Only set if true to avoid bloating setup() function | ||||
|     # (inverted bit in pin_flags_ bitfield is zero-initialized to false) | ||||
|     if config[CONF_INVERTED]: | ||||
|         cg.add(var.set_inverted(True)) | ||||
|     cg.add(var.set_flags(pins.gpio_flags_expr(mode))) | ||||
|     if num < 16: | ||||
|         initial_state: PinInitialState = CORE.data[KEY_ESP8266][KEY_PIN_INITIAL_STATES][ | ||||
|   | ||||
| @@ -14,7 +14,7 @@ from esphome.components.esp32.const import ( | ||||
|     VARIANT_ESP32S2, | ||||
|     VARIANT_ESP32S3, | ||||
| ) | ||||
| from esphome.components.network import IPAddress | ||||
| from esphome.components.network import ip_address_literal | ||||
| from esphome.components.spi import CONF_INTERFACE_INDEX, get_spi_interface | ||||
| import esphome.config_validation as cv | ||||
| from esphome.const import ( | ||||
| @@ -320,11 +320,11 @@ def _final_validate_spi(config): | ||||
| def manual_ip(config): | ||||
|     return cg.StructInitializer( | ||||
|         ManualIP, | ||||
|         ("static_ip", IPAddress(str(config[CONF_STATIC_IP]))), | ||||
|         ("gateway", IPAddress(str(config[CONF_GATEWAY]))), | ||||
|         ("subnet", IPAddress(str(config[CONF_SUBNET]))), | ||||
|         ("dns1", IPAddress(str(config[CONF_DNS1]))), | ||||
|         ("dns2", IPAddress(str(config[CONF_DNS2]))), | ||||
|         ("static_ip", ip_address_literal(config[CONF_STATIC_IP])), | ||||
|         ("gateway", ip_address_literal(config[CONF_GATEWAY])), | ||||
|         ("subnet", ip_address_literal(config[CONF_SUBNET])), | ||||
|         ("dns1", ip_address_literal(config[CONF_DNS1])), | ||||
|         ("dns2", ip_address_literal(config[CONF_DNS2])), | ||||
|     ) | ||||
|  | ||||
|  | ||||
|   | ||||
| @@ -28,8 +28,8 @@ class HostGPIOPin : public InternalGPIOPin { | ||||
|   void attach_interrupt(void (*func)(void *), void *arg, gpio::InterruptType type) const override; | ||||
|  | ||||
|   uint8_t pin_; | ||||
|   bool inverted_; | ||||
|   gpio::Flags flags_; | ||||
|   bool inverted_{}; | ||||
|   gpio::Flags flags_{}; | ||||
| }; | ||||
|  | ||||
| }  // namespace host | ||||
|   | ||||
| @@ -57,6 +57,9 @@ async def host_pin_to_code(config): | ||||
|     var = cg.new_Pvariable(config[CONF_ID]) | ||||
|     num = config[CONF_NUMBER] | ||||
|     cg.add(var.set_pin(num)) | ||||
|     cg.add(var.set_inverted(config[CONF_INVERTED])) | ||||
|     # Only set if true to avoid bloating setup() function | ||||
|     # (inverted bit in pin_flags_ bitfield is zero-initialized to false) | ||||
|     if config[CONF_INVERTED]: | ||||
|         cg.add(var.set_inverted(True)) | ||||
|     cg.add(var.set_flags(pins.gpio_flags_expr(config[CONF_MODE]))) | ||||
|     return var | ||||
|   | ||||
| @@ -199,6 +199,9 @@ async def component_pin_to_code(config): | ||||
|     var = cg.new_Pvariable(config[CONF_ID]) | ||||
|     num = config[CONF_NUMBER] | ||||
|     cg.add(var.set_pin(num)) | ||||
|     cg.add(var.set_inverted(config[CONF_INVERTED])) | ||||
|     # Only set if true to avoid bloating setup() function | ||||
|     # (inverted bit in pin_flags_ bitfield is zero-initialized to false) | ||||
|     if config[CONF_INVERTED]: | ||||
|         cg.add(var.set_inverted(True)) | ||||
|     cg.add(var.set_flags(pins.gpio_flags_expr(config[CONF_MODE]))) | ||||
|     return var | ||||
|   | ||||
| @@ -27,8 +27,8 @@ class ArduinoInternalGPIOPin : public InternalGPIOPin { | ||||
|   void attach_interrupt(void (*func)(void *), void *arg, gpio::InterruptType type) const override; | ||||
|  | ||||
|   uint8_t pin_; | ||||
|   bool inverted_; | ||||
|   gpio::Flags flags_; | ||||
|   bool inverted_{}; | ||||
|   gpio::Flags flags_{}; | ||||
| }; | ||||
|  | ||||
| }  // namespace libretiny | ||||
|   | ||||
| @@ -57,9 +57,9 @@ class AddressableLightEffect : public LightEffect { | ||||
|  | ||||
| class AddressableLambdaLightEffect : public AddressableLightEffect { | ||||
|  public: | ||||
|   AddressableLambdaLightEffect(const char *name, std::function<void(AddressableLight &, Color, bool initial_run)> f, | ||||
|   AddressableLambdaLightEffect(const char *name, void (*f)(AddressableLight &, Color, bool initial_run), | ||||
|                                uint32_t update_interval) | ||||
|       : AddressableLightEffect(name), f_(std::move(f)), update_interval_(update_interval) {} | ||||
|       : AddressableLightEffect(name), f_(f), update_interval_(update_interval) {} | ||||
|   void start() override { this->initial_run_ = true; } | ||||
|   void apply(AddressableLight &it, const Color ¤t_color) override { | ||||
|     const uint32_t now = millis(); | ||||
| @@ -72,7 +72,7 @@ class AddressableLambdaLightEffect : public AddressableLightEffect { | ||||
|   } | ||||
|  | ||||
|  protected: | ||||
|   std::function<void(AddressableLight &, Color, bool initial_run)> f_; | ||||
|   void (*f_)(AddressableLight &, Color, bool initial_run); | ||||
|   uint32_t update_interval_; | ||||
|   uint32_t last_run_{0}; | ||||
|   bool initial_run_; | ||||
|   | ||||
| @@ -112,8 +112,8 @@ class RandomLightEffect : public LightEffect { | ||||
|  | ||||
| class LambdaLightEffect : public LightEffect { | ||||
|  public: | ||||
|   LambdaLightEffect(const char *name, std::function<void(bool initial_run)> f, uint32_t update_interval) | ||||
|       : LightEffect(name), f_(std::move(f)), update_interval_(update_interval) {} | ||||
|   LambdaLightEffect(const char *name, void (*f)(bool initial_run), uint32_t update_interval) | ||||
|       : LightEffect(name), f_(f), update_interval_(update_interval) {} | ||||
|  | ||||
|   void start() override { this->initial_run_ = true; } | ||||
|   void apply() override { | ||||
| @@ -130,7 +130,7 @@ class LambdaLightEffect : public LightEffect { | ||||
|   uint32_t get_current_index() const { return this->get_index(); } | ||||
|  | ||||
|  protected: | ||||
|   std::function<void(bool initial_run)> f_; | ||||
|   void (*f_)(bool initial_run); | ||||
|   uint32_t update_interval_; | ||||
|   uint32_t last_run_{0}; | ||||
|   bool initial_run_; | ||||
|   | ||||
| @@ -1,7 +1,7 @@ | ||||
| import re | ||||
|  | ||||
| from esphome import automation | ||||
| from esphome.automation import LambdaAction | ||||
| from esphome.automation import LambdaAction, StatelessLambdaAction | ||||
| import esphome.codegen as cg | ||||
| from esphome.components.esp32 import add_idf_sdkconfig_option, get_esp32_variant | ||||
| from esphome.components.esp32.const import ( | ||||
| @@ -430,7 +430,9 @@ async def logger_log_action_to_code(config, action_id, template_arg, args): | ||||
|     text = str(cg.statement(esp_log(config[CONF_TAG], config[CONF_FORMAT], *args_))) | ||||
|  | ||||
|     lambda_ = await cg.process_lambda(Lambda(text), args, return_type=cg.void) | ||||
|     return cg.new_Pvariable(action_id, template_arg, lambda_) | ||||
|     return automation.new_lambda_pvariable( | ||||
|         action_id, lambda_, StatelessLambdaAction, template_arg | ||||
|     ) | ||||
|  | ||||
|  | ||||
| @automation.register_action( | ||||
| @@ -455,7 +457,9 @@ async def logger_set_level_to_code(config, action_id, template_arg, args): | ||||
|         text = str(cg.statement(logger.set_log_level(level))) | ||||
|  | ||||
|     lambda_ = await cg.process_lambda(Lambda(text), args, return_type=cg.void) | ||||
|     return cg.new_Pvariable(action_id, template_arg, lambda_) | ||||
|     return automation.new_lambda_pvariable( | ||||
|         action_id, lambda_, StatelessLambdaAction, template_arg | ||||
|     ) | ||||
|  | ||||
|  | ||||
| FILTER_SOURCE_FILES = filter_source_files_from_platform( | ||||
|   | ||||
| @@ -1,3 +1,5 @@ | ||||
| import ipaddress | ||||
|  | ||||
| import esphome.codegen as cg | ||||
| from esphome.components.esp32 import add_idf_sdkconfig_option | ||||
| import esphome.config_validation as cv | ||||
| @@ -10,6 +12,41 @@ AUTO_LOAD = ["mdns"] | ||||
| network_ns = cg.esphome_ns.namespace("network") | ||||
| IPAddress = network_ns.class_("IPAddress") | ||||
|  | ||||
|  | ||||
| def ip_address_literal(ip: str | int | None) -> cg.MockObj: | ||||
|     """Generate an IPAddress with compile-time initialization instead of runtime parsing. | ||||
|  | ||||
|     This function parses the IP address in Python during code generation and generates | ||||
|     a call to the 4-octet constructor (IPAddress(192, 168, 1, 1)) instead of the | ||||
|     string constructor (IPAddress("192.168.1.1")). This eliminates runtime string | ||||
|     parsing overhead and reduces flash usage on embedded systems. | ||||
|  | ||||
|     Args: | ||||
|         ip: IP address as string (e.g., "192.168.1.1"), ipaddress.IPv4Address, or None | ||||
|  | ||||
|     Returns: | ||||
|         IPAddress expression that uses 4-octet constructor for efficiency | ||||
|     """ | ||||
|     if ip is None: | ||||
|         return IPAddress(0, 0, 0, 0) | ||||
|  | ||||
|     try: | ||||
|         # Parse using Python's ipaddress module | ||||
|         ip_obj = ipaddress.ip_address(ip) | ||||
|     except (ValueError, TypeError): | ||||
|         pass | ||||
|     else: | ||||
|         # Only support IPv4 for now | ||||
|         if isinstance(ip_obj, ipaddress.IPv4Address): | ||||
|             # Extract octets from the packed bytes representation | ||||
|             octets = ip_obj.packed | ||||
|             # Generate call to 4-octet constructor: IPAddress(192, 168, 1, 1) | ||||
|             return IPAddress(octets[0], octets[1], octets[2], octets[3]) | ||||
|  | ||||
|     # Fallback to string constructor if parsing fails | ||||
|     return IPAddress(str(ip)) | ||||
|  | ||||
|  | ||||
| CONFIG_SCHEMA = cv.Schema( | ||||
|     { | ||||
|         cv.SplitDefault( | ||||
|   | ||||
| @@ -74,6 +74,9 @@ async def nrf52_pin_to_code(config): | ||||
|     var = cg.new_Pvariable(config[CONF_ID]) | ||||
|     num = config[CONF_NUMBER] | ||||
|     cg.add(var.set_pin(num)) | ||||
|     cg.add(var.set_inverted(config[CONF_INVERTED])) | ||||
|     # Only set if true to avoid bloating setup() function | ||||
|     # (inverted bit in pin_flags_ bitfield is zero-initialized to false) | ||||
|     if config[CONF_INVERTED]: | ||||
|         cg.add(var.set_inverted(True)) | ||||
|     cg.add(var.set_flags(pins.gpio_flags_expr(config[CONF_MODE]))) | ||||
|     return var | ||||
|   | ||||
| @@ -29,8 +29,8 @@ class RP2040GPIOPin : public InternalGPIOPin { | ||||
|   void attach_interrupt(void (*func)(void *), void *arg, gpio::InterruptType type) const override; | ||||
|  | ||||
|   uint8_t pin_; | ||||
|   bool inverted_; | ||||
|   gpio::Flags flags_; | ||||
|   bool inverted_{}; | ||||
|   gpio::Flags flags_{}; | ||||
| }; | ||||
|  | ||||
| }  // namespace rp2040 | ||||
|   | ||||
| @@ -94,6 +94,9 @@ async def rp2040_pin_to_code(config): | ||||
|     var = cg.new_Pvariable(config[CONF_ID]) | ||||
|     num = config[CONF_NUMBER] | ||||
|     cg.add(var.set_pin(num)) | ||||
|     cg.add(var.set_inverted(config[CONF_INVERTED])) | ||||
|     # Only set if true to avoid bloating setup() function | ||||
|     # (inverted bit in pin_flags_ bitfield is zero-initialized to false) | ||||
|     if config[CONF_INVERTED]: | ||||
|         cg.add(var.set_inverted(True)) | ||||
|     cg.add(var.set_flags(pins.gpio_flags_expr(config[CONF_MODE]))) | ||||
|     return var | ||||
|   | ||||
| @@ -261,6 +261,7 @@ ExponentialMovingAverageFilter = sensor_ns.class_( | ||||
| ) | ||||
| ThrottleAverageFilter = sensor_ns.class_("ThrottleAverageFilter", Filter, cg.Component) | ||||
| LambdaFilter = sensor_ns.class_("LambdaFilter", Filter) | ||||
| StatelessLambdaFilter = sensor_ns.class_("StatelessLambdaFilter", Filter) | ||||
| OffsetFilter = sensor_ns.class_("OffsetFilter", Filter) | ||||
| MultiplyFilter = sensor_ns.class_("MultiplyFilter", Filter) | ||||
| ValueListFilter = sensor_ns.class_("ValueListFilter", Filter) | ||||
| @@ -573,7 +574,7 @@ async def lambda_filter_to_code(config, filter_id): | ||||
|     lambda_ = await cg.process_lambda( | ||||
|         config, [(float, "x")], return_type=cg.optional.template(float) | ||||
|     ) | ||||
|     return cg.new_Pvariable(filter_id, lambda_) | ||||
|     return automation.new_lambda_pvariable(filter_id, lambda_, StatelessLambdaFilter) | ||||
|  | ||||
|  | ||||
| DELTA_SCHEMA = cv.Schema( | ||||
|   | ||||
| @@ -296,6 +296,21 @@ class LambdaFilter : public Filter { | ||||
|   lambda_filter_t lambda_filter_; | ||||
| }; | ||||
|  | ||||
| /** Optimized lambda filter for stateless lambdas (no capture). | ||||
|  * | ||||
|  * Uses function pointer instead of std::function to reduce memory overhead. | ||||
|  * Memory: 4 bytes (function pointer on 32-bit) vs 32 bytes (std::function). | ||||
|  */ | ||||
| class StatelessLambdaFilter : public Filter { | ||||
|  public: | ||||
|   explicit StatelessLambdaFilter(optional<float> (*lambda_filter)(float)) : lambda_filter_(lambda_filter) {} | ||||
|  | ||||
|   optional<float> new_value(float value) override { return this->lambda_filter_(value); } | ||||
|  | ||||
|  protected: | ||||
|   optional<float> (*lambda_filter_)(float); | ||||
| }; | ||||
|  | ||||
| /// A simple filter that adds `offset` to each value it receives. | ||||
| class OffsetFilter : public Filter { | ||||
|  public: | ||||
|   | ||||
| @@ -1,4 +1,6 @@ | ||||
| import logging | ||||
| from re import Match | ||||
| from typing import Any | ||||
|  | ||||
| from esphome import core | ||||
| from esphome.config_helpers import Extend, Remove, merge_config, merge_dicts_ordered | ||||
| @@ -39,7 +41,34 @@ async def to_code(config): | ||||
|     pass | ||||
|  | ||||
|  | ||||
| def _expand_jinja(value, orig_value, path, jinja, ignore_missing): | ||||
| def _restore_data_base(value: Any, orig_value: ESPHomeDataBase) -> ESPHomeDataBase: | ||||
|     """This function restores ESPHomeDataBase metadata held by the original string. | ||||
|     This is needed because during jinja evaluation, strings can be replaced by other types, | ||||
|     but we want to keep the original metadata for error reporting and source mapping. | ||||
|     For example, if a substitution replaces a string with a dictionary, we want that items | ||||
|     in the dictionary to still point to the original document location | ||||
|     """ | ||||
|     if isinstance(value, ESPHomeDataBase): | ||||
|         return value | ||||
|     if isinstance(value, dict): | ||||
|         return { | ||||
|             _restore_data_base(k, orig_value): _restore_data_base(v, orig_value) | ||||
|             for k, v in value.items() | ||||
|         } | ||||
|     if isinstance(value, list): | ||||
|         return [_restore_data_base(v, orig_value) for v in value] | ||||
|     if isinstance(value, str): | ||||
|         return make_data_base(value, orig_value) | ||||
|     return value | ||||
|  | ||||
|  | ||||
| def _expand_jinja( | ||||
|     value: str | JinjaStr, | ||||
|     orig_value: str | JinjaStr, | ||||
|     path, | ||||
|     jinja: Jinja, | ||||
|     ignore_missing: bool, | ||||
| ) -> Any: | ||||
|     if has_jinja(value): | ||||
|         # If the original value passed in to this function is a JinjaStr, it means it contains an unresolved | ||||
|         # Jinja expression from a previous pass. | ||||
| @@ -65,10 +94,17 @@ def _expand_jinja(value, orig_value, path, jinja, ignore_missing): | ||||
|                 f"\nSee {'->'.join(str(x) for x in path)}", | ||||
|                 path, | ||||
|             ) | ||||
|         # If the original, unexpanded string, contained document metadata (ESPHomeDatabase), | ||||
|         # assign this same document metadata to the resulting value. | ||||
|         if isinstance(orig_value, ESPHomeDataBase): | ||||
|             value = _restore_data_base(value, orig_value) | ||||
|  | ||||
|     return value | ||||
|  | ||||
|  | ||||
| def _expand_substitutions(substitutions, value, path, jinja, ignore_missing): | ||||
| def _expand_substitutions( | ||||
|     substitutions: dict, value: str, path, jinja: Jinja, ignore_missing: bool | ||||
| ) -> Any: | ||||
|     if "$" not in value: | ||||
|         return value | ||||
|  | ||||
| @@ -76,14 +112,14 @@ def _expand_substitutions(substitutions, value, path, jinja, ignore_missing): | ||||
|  | ||||
|     i = 0 | ||||
|     while True: | ||||
|         m = cv.VARIABLE_PROG.search(value, i) | ||||
|         m: Match[str] = cv.VARIABLE_PROG.search(value, i) | ||||
|         if not m: | ||||
|             # No more variable substitutions found. See if the remainder looks like a jinja template | ||||
|             value = _expand_jinja(value, orig_value, path, jinja, ignore_missing) | ||||
|             break | ||||
|  | ||||
|         i, j = m.span(0) | ||||
|         name = m.group(1) | ||||
|         name: str = m.group(1) | ||||
|         if name.startswith("{") and name.endswith("}"): | ||||
|             name = name[1:-1] | ||||
|         if name not in substitutions: | ||||
| @@ -98,7 +134,7 @@ def _expand_substitutions(substitutions, value, path, jinja, ignore_missing): | ||||
|             i = j | ||||
|             continue | ||||
|  | ||||
|         sub = substitutions[name] | ||||
|         sub: Any = substitutions[name] | ||||
|  | ||||
|         if i == 0 and j == len(value): | ||||
|             # The variable spans the whole expression, e.g., "${varName}". Return its resolved value directly | ||||
| @@ -121,7 +157,13 @@ def _expand_substitutions(substitutions, value, path, jinja, ignore_missing): | ||||
|     return value | ||||
|  | ||||
|  | ||||
| def _substitute_item(substitutions, item, path, jinja, ignore_missing): | ||||
| def _substitute_item( | ||||
|     substitutions: dict, | ||||
|     item: Any, | ||||
|     path: list[int | str], | ||||
|     jinja: Jinja, | ||||
|     ignore_missing: bool, | ||||
| ) -> Any | None: | ||||
|     if isinstance(item, ESPLiteralValue): | ||||
|         return None  # do not substitute inside literal blocks | ||||
|     if isinstance(item, list): | ||||
| @@ -160,7 +202,9 @@ def _substitute_item(substitutions, item, path, jinja, ignore_missing): | ||||
|     return None | ||||
|  | ||||
|  | ||||
| def do_substitution_pass(config, command_line_substitutions, ignore_missing=False): | ||||
| def do_substitution_pass( | ||||
|     config: dict, command_line_substitutions: dict, ignore_missing: bool = False | ||||
| ) -> None: | ||||
|     if CONF_SUBSTITUTIONS not in config and not command_line_substitutions: | ||||
|         return | ||||
|  | ||||
|   | ||||
| @@ -1,10 +1,14 @@ | ||||
| from ast import literal_eval | ||||
| from collections.abc import Iterator | ||||
| from itertools import chain, islice | ||||
| import logging | ||||
| import math | ||||
| import re | ||||
| from types import GeneratorType | ||||
| from typing import Any | ||||
|  | ||||
| import jinja2 as jinja | ||||
| from jinja2.sandbox import SandboxedEnvironment | ||||
| from jinja2.nativetypes import NativeCodeGenerator, NativeTemplate | ||||
|  | ||||
| from esphome.yaml_util import ESPLiteralValue | ||||
|  | ||||
| @@ -24,7 +28,7 @@ detect_jinja_re = re.compile( | ||||
| ) | ||||
|  | ||||
|  | ||||
| def has_jinja(st): | ||||
| def has_jinja(st: str) -> bool: | ||||
|     return detect_jinja_re.search(st) is not None | ||||
|  | ||||
|  | ||||
| @@ -109,12 +113,56 @@ class TrackerContext(jinja.runtime.Context): | ||||
|         return val | ||||
|  | ||||
|  | ||||
| class Jinja(SandboxedEnvironment): | ||||
| def _concat_nodes_override(values: Iterator[Any]) -> Any: | ||||
|     """ | ||||
|     This function customizes how Jinja preserves native types when concatenating | ||||
|     multiple result nodes together. If the result is a single node, its value | ||||
|     is returned. Otherwise, the nodes are concatenated as strings. If | ||||
|     the result can be parsed with `ast.literal_eval`, the parsed | ||||
|     value is returned. Otherwise, the string is returned. | ||||
|     This helps preserve metadata such as ESPHomeDataBase from original values | ||||
|     and mimicks how HomeAssistant deals with template evaluation and preserving | ||||
|     the original datatype. | ||||
|     """ | ||||
|     head: list[Any] = list(islice(values, 2)) | ||||
|  | ||||
|     if not head: | ||||
|         return None | ||||
|  | ||||
|     if len(head) == 1: | ||||
|         raw = head[0] | ||||
|         if not isinstance(raw, str): | ||||
|             return raw | ||||
|     else: | ||||
|         if isinstance(values, GeneratorType): | ||||
|             values = chain(head, values) | ||||
|         raw = "".join([str(v) for v in values]) | ||||
|  | ||||
|     try: | ||||
|         # Attempt to parse the concatenated string into a Python literal. | ||||
|         # This allows expressions like "1 + 2" to be evaluated to the integer 3. | ||||
|         # If the result is also a string or there is a parsing error, | ||||
|         # fall back to returning the raw string. This is consistent with | ||||
|         #  Home Assistant's behavior when evaluating templates | ||||
|         result = literal_eval(raw) | ||||
|         if not isinstance(result, str): | ||||
|             return result | ||||
|  | ||||
|     except (ValueError, SyntaxError, MemoryError, TypeError): | ||||
|         pass | ||||
|     return raw | ||||
|  | ||||
|  | ||||
| class Jinja(jinja.Environment): | ||||
|     """ | ||||
|     Wraps a Jinja environment | ||||
|     """ | ||||
|  | ||||
|     def __init__(self, context_vars): | ||||
|     # jinja environment customization overrides | ||||
|     code_generator_class = NativeCodeGenerator | ||||
|     concat = staticmethod(_concat_nodes_override) | ||||
|  | ||||
|     def __init__(self, context_vars: dict): | ||||
|         super().__init__( | ||||
|             trim_blocks=True, | ||||
|             lstrip_blocks=True, | ||||
| @@ -142,19 +190,10 @@ class Jinja(SandboxedEnvironment): | ||||
|             **SAFE_GLOBALS, | ||||
|         } | ||||
|  | ||||
|     def safe_eval(self, expr): | ||||
|         try: | ||||
|             result = literal_eval(expr) | ||||
|             if not isinstance(result, str): | ||||
|                 return result | ||||
|         except (ValueError, SyntaxError, MemoryError, TypeError): | ||||
|             pass | ||||
|         return expr | ||||
|  | ||||
|     def expand(self, content_str): | ||||
|     def expand(self, content_str: str | JinjaStr) -> Any: | ||||
|         """ | ||||
|         Renders a string that may contain Jinja expressions or statements | ||||
|         Returns the resulting processed string if all values could be resolved. | ||||
|         Returns the resulting value if all variables and expressions could be resolved. | ||||
|         Otherwise, it returns a tagged (JinjaStr) string that captures variables | ||||
|         in scope (upvalues), like a closure for later evaluation. | ||||
|         """ | ||||
| @@ -172,7 +211,7 @@ class Jinja(SandboxedEnvironment): | ||||
|         self.context_trace = {} | ||||
|         try: | ||||
|             template = self.from_string(content_str) | ||||
|             result = self.safe_eval(template.render(override_vars)) | ||||
|             result = template.render(override_vars) | ||||
|             if isinstance(result, Undefined): | ||||
|                 print("" + result)  # force a UndefinedError exception | ||||
|         except (TemplateSyntaxError, UndefinedError) as err: | ||||
| @@ -201,3 +240,10 @@ class Jinja(SandboxedEnvironment): | ||||
|             content_str.result = result | ||||
|  | ||||
|         return result, None | ||||
|  | ||||
|  | ||||
| class JinjaTemplate(NativeTemplate): | ||||
|     environment_class = Jinja | ||||
|  | ||||
|  | ||||
| Jinja.template_class = JinjaTemplate | ||||
|   | ||||
| @@ -38,8 +38,14 @@ async def to_code(config): | ||||
|         condition = await automation.build_condition( | ||||
|             condition, cg.TemplateArguments(), [] | ||||
|         ) | ||||
|         # Generate a stateless lambda that calls condition.check() | ||||
|         # capture="" is safe because condition is a global variable in generated C++ code | ||||
|         # and doesn't need to be captured. This allows implicit conversion to function pointer. | ||||
|         template_ = LambdaExpression( | ||||
|             f"return {condition.check()};", [], return_type=cg.optional.template(bool) | ||||
|             f"return {condition.check()};", | ||||
|             [], | ||||
|             return_type=cg.optional.template(bool), | ||||
|             capture="", | ||||
|         ) | ||||
|         cg.add(var.set_template(template_)) | ||||
|  | ||||
|   | ||||
| @@ -9,10 +9,10 @@ static const char *const TAG = "template.binary_sensor"; | ||||
| void TemplateBinarySensor::setup() { this->loop(); } | ||||
|  | ||||
| void TemplateBinarySensor::loop() { | ||||
|   if (this->f_ == nullptr) | ||||
|   if (!this->f_.has_value()) | ||||
|     return; | ||||
|  | ||||
|   auto s = this->f_(); | ||||
|   auto s = (*this->f_)(); | ||||
|   if (s.has_value()) { | ||||
|     this->publish_state(*s); | ||||
|   } | ||||
|   | ||||
| @@ -8,7 +8,7 @@ namespace template_ { | ||||
|  | ||||
| class TemplateBinarySensor : public Component, public binary_sensor::BinarySensor { | ||||
|  public: | ||||
|   void set_template(std::function<optional<bool>()> &&f) { this->f_ = f; } | ||||
|   void set_template(optional<bool> (*f)()) { this->f_ = f; } | ||||
|  | ||||
|   void setup() override; | ||||
|   void loop() override; | ||||
| @@ -17,7 +17,7 @@ class TemplateBinarySensor : public Component, public binary_sensor::BinarySenso | ||||
|   float get_setup_priority() const override { return setup_priority::HARDWARE; } | ||||
|  | ||||
|  protected: | ||||
|   std::function<optional<bool>()> f_{nullptr}; | ||||
|   optional<optional<bool> (*)()> f_; | ||||
| }; | ||||
|  | ||||
| }  // namespace template_ | ||||
|   | ||||
| @@ -63,7 +63,7 @@ void TemplateCover::loop() { | ||||
| } | ||||
| void TemplateCover::set_optimistic(bool optimistic) { this->optimistic_ = optimistic; } | ||||
| void TemplateCover::set_assumed_state(bool assumed_state) { this->assumed_state_ = assumed_state; } | ||||
| void TemplateCover::set_state_lambda(std::function<optional<float>()> &&f) { this->state_f_ = f; } | ||||
| void TemplateCover::set_state_lambda(optional<float> (*f)()) { this->state_f_ = f; } | ||||
| float TemplateCover::get_setup_priority() const { return setup_priority::HARDWARE; } | ||||
| Trigger<> *TemplateCover::get_open_trigger() const { return this->open_trigger_; } | ||||
| Trigger<> *TemplateCover::get_close_trigger() const { return this->close_trigger_; } | ||||
| @@ -124,7 +124,7 @@ CoverTraits TemplateCover::get_traits() { | ||||
| } | ||||
| Trigger<float> *TemplateCover::get_position_trigger() const { return this->position_trigger_; } | ||||
| Trigger<float> *TemplateCover::get_tilt_trigger() const { return this->tilt_trigger_; } | ||||
| void TemplateCover::set_tilt_lambda(std::function<optional<float>()> &&tilt_f) { this->tilt_f_ = tilt_f; } | ||||
| void TemplateCover::set_tilt_lambda(optional<float> (*tilt_f)()) { this->tilt_f_ = tilt_f; } | ||||
| void TemplateCover::set_has_stop(bool has_stop) { this->has_stop_ = has_stop; } | ||||
| void TemplateCover::set_has_toggle(bool has_toggle) { this->has_toggle_ = has_toggle; } | ||||
| void TemplateCover::set_has_position(bool has_position) { this->has_position_ = has_position; } | ||||
|   | ||||
| @@ -17,7 +17,7 @@ class TemplateCover : public cover::Cover, public Component { | ||||
|  public: | ||||
|   TemplateCover(); | ||||
|  | ||||
|   void set_state_lambda(std::function<optional<float>()> &&f); | ||||
|   void set_state_lambda(optional<float> (*f)()); | ||||
|   Trigger<> *get_open_trigger() const; | ||||
|   Trigger<> *get_close_trigger() const; | ||||
|   Trigger<> *get_stop_trigger() const; | ||||
| @@ -26,7 +26,7 @@ class TemplateCover : public cover::Cover, public Component { | ||||
|   Trigger<float> *get_tilt_trigger() const; | ||||
|   void set_optimistic(bool optimistic); | ||||
|   void set_assumed_state(bool assumed_state); | ||||
|   void set_tilt_lambda(std::function<optional<float>()> &&tilt_f); | ||||
|   void set_tilt_lambda(optional<float> (*tilt_f)()); | ||||
|   void set_has_stop(bool has_stop); | ||||
|   void set_has_position(bool has_position); | ||||
|   void set_has_tilt(bool has_tilt); | ||||
| @@ -45,8 +45,8 @@ class TemplateCover : public cover::Cover, public Component { | ||||
|   void stop_prev_trigger_(); | ||||
|  | ||||
|   TemplateCoverRestoreMode restore_mode_{COVER_RESTORE}; | ||||
|   optional<std::function<optional<float>()>> state_f_; | ||||
|   optional<std::function<optional<float>()>> tilt_f_; | ||||
|   optional<optional<float> (*)()> state_f_; | ||||
|   optional<optional<float> (*)()> tilt_f_; | ||||
|   bool assumed_state_{false}; | ||||
|   bool optimistic_{false}; | ||||
|   Trigger<> *open_trigger_; | ||||
|   | ||||
| @@ -15,7 +15,7 @@ namespace template_ { | ||||
|  | ||||
| class TemplateDate : public datetime::DateEntity, public PollingComponent { | ||||
|  public: | ||||
|   void set_template(std::function<optional<ESPTime>()> &&f) { this->f_ = f; } | ||||
|   void set_template(optional<ESPTime> (*f)()) { this->f_ = f; } | ||||
|  | ||||
|   void setup() override; | ||||
|   void update() override; | ||||
| @@ -35,7 +35,7 @@ class TemplateDate : public datetime::DateEntity, public PollingComponent { | ||||
|   ESPTime initial_value_{}; | ||||
|   bool restore_value_{false}; | ||||
|   Trigger<ESPTime> *set_trigger_ = new Trigger<ESPTime>(); | ||||
|   optional<std::function<optional<ESPTime>()>> f_; | ||||
|   optional<optional<ESPTime> (*)()> f_; | ||||
|  | ||||
|   ESPPreferenceObject pref_; | ||||
| }; | ||||
|   | ||||
| @@ -15,7 +15,7 @@ namespace template_ { | ||||
|  | ||||
| class TemplateDateTime : public datetime::DateTimeEntity, public PollingComponent { | ||||
|  public: | ||||
|   void set_template(std::function<optional<ESPTime>()> &&f) { this->f_ = f; } | ||||
|   void set_template(optional<ESPTime> (*f)()) { this->f_ = f; } | ||||
|  | ||||
|   void setup() override; | ||||
|   void update() override; | ||||
| @@ -35,7 +35,7 @@ class TemplateDateTime : public datetime::DateTimeEntity, public PollingComponen | ||||
|   ESPTime initial_value_{}; | ||||
|   bool restore_value_{false}; | ||||
|   Trigger<ESPTime> *set_trigger_ = new Trigger<ESPTime>(); | ||||
|   optional<std::function<optional<ESPTime>()>> f_; | ||||
|   optional<optional<ESPTime> (*)()> f_; | ||||
|  | ||||
|   ESPPreferenceObject pref_; | ||||
| }; | ||||
|   | ||||
| @@ -15,7 +15,7 @@ namespace template_ { | ||||
|  | ||||
| class TemplateTime : public datetime::TimeEntity, public PollingComponent { | ||||
|  public: | ||||
|   void set_template(std::function<optional<ESPTime>()> &&f) { this->f_ = f; } | ||||
|   void set_template(optional<ESPTime> (*f)()) { this->f_ = f; } | ||||
|  | ||||
|   void setup() override; | ||||
|   void update() override; | ||||
| @@ -35,7 +35,7 @@ class TemplateTime : public datetime::TimeEntity, public PollingComponent { | ||||
|   ESPTime initial_value_{}; | ||||
|   bool restore_value_{false}; | ||||
|   Trigger<ESPTime> *set_trigger_ = new Trigger<ESPTime>(); | ||||
|   optional<std::function<optional<ESPTime>()>> f_; | ||||
|   optional<optional<ESPTime> (*)()> f_; | ||||
|  | ||||
|   ESPPreferenceObject pref_; | ||||
| }; | ||||
|   | ||||
| @@ -45,7 +45,7 @@ void TemplateLock::open_latch() { | ||||
|   this->open_trigger_->trigger(); | ||||
| } | ||||
| void TemplateLock::set_optimistic(bool optimistic) { this->optimistic_ = optimistic; } | ||||
| void TemplateLock::set_state_lambda(std::function<optional<lock::LockState>()> &&f) { this->f_ = f; } | ||||
| void TemplateLock::set_state_lambda(optional<lock::LockState> (*f)()) { this->f_ = f; } | ||||
| float TemplateLock::get_setup_priority() const { return setup_priority::HARDWARE; } | ||||
| Trigger<> *TemplateLock::get_lock_trigger() const { return this->lock_trigger_; } | ||||
| Trigger<> *TemplateLock::get_unlock_trigger() const { return this->unlock_trigger_; } | ||||
|   | ||||
| @@ -13,7 +13,7 @@ class TemplateLock : public lock::Lock, public Component { | ||||
|  | ||||
|   void dump_config() override; | ||||
|  | ||||
|   void set_state_lambda(std::function<optional<lock::LockState>()> &&f); | ||||
|   void set_state_lambda(optional<lock::LockState> (*f)()); | ||||
|   Trigger<> *get_lock_trigger() const; | ||||
|   Trigger<> *get_unlock_trigger() const; | ||||
|   Trigger<> *get_open_trigger() const; | ||||
| @@ -26,7 +26,7 @@ class TemplateLock : public lock::Lock, public Component { | ||||
|   void control(const lock::LockCall &call) override; | ||||
|   void open_latch() override; | ||||
|  | ||||
|   optional<std::function<optional<lock::LockState>()>> f_; | ||||
|   optional<optional<lock::LockState> (*)()> f_; | ||||
|   bool optimistic_{false}; | ||||
|   Trigger<> *lock_trigger_; | ||||
|   Trigger<> *unlock_trigger_; | ||||
|   | ||||
| @@ -10,7 +10,7 @@ namespace template_ { | ||||
|  | ||||
| class TemplateNumber : public number::Number, public PollingComponent { | ||||
|  public: | ||||
|   void set_template(std::function<optional<float>()> &&f) { this->f_ = f; } | ||||
|   void set_template(optional<float> (*f)()) { this->f_ = f; } | ||||
|  | ||||
|   void setup() override; | ||||
|   void update() override; | ||||
| @@ -28,7 +28,7 @@ class TemplateNumber : public number::Number, public PollingComponent { | ||||
|   float initial_value_{NAN}; | ||||
|   bool restore_value_{false}; | ||||
|   Trigger<float> *set_trigger_ = new Trigger<float>(); | ||||
|   optional<std::function<optional<float>()>> f_; | ||||
|   optional<optional<float> (*)()> f_; | ||||
|  | ||||
|   ESPPreferenceObject pref_; | ||||
| }; | ||||
|   | ||||
| @@ -10,7 +10,7 @@ namespace template_ { | ||||
|  | ||||
| class TemplateSelect : public select::Select, public PollingComponent { | ||||
|  public: | ||||
|   void set_template(std::function<optional<std::string>()> &&f) { this->f_ = f; } | ||||
|   void set_template(optional<std::string> (*f)()) { this->f_ = f; } | ||||
|  | ||||
|   void setup() override; | ||||
|   void update() override; | ||||
| @@ -28,7 +28,7 @@ class TemplateSelect : public select::Select, public PollingComponent { | ||||
|   std::string initial_option_; | ||||
|   bool restore_value_ = false; | ||||
|   Trigger<std::string> *set_trigger_ = new Trigger<std::string>(); | ||||
|   optional<std::function<optional<std::string>()>> f_; | ||||
|   optional<optional<std::string> (*)()> f_; | ||||
|  | ||||
|   ESPPreferenceObject pref_; | ||||
| }; | ||||
|   | ||||
| @@ -17,7 +17,7 @@ void TemplateSensor::update() { | ||||
|   } | ||||
| } | ||||
| float TemplateSensor::get_setup_priority() const { return setup_priority::HARDWARE; } | ||||
| void TemplateSensor::set_template(std::function<optional<float>()> &&f) { this->f_ = f; } | ||||
| void TemplateSensor::set_template(optional<float> (*f)()) { this->f_ = f; } | ||||
| void TemplateSensor::dump_config() { | ||||
|   LOG_SENSOR("", "Template Sensor", this); | ||||
|   LOG_UPDATE_INTERVAL(this); | ||||
|   | ||||
| @@ -8,7 +8,7 @@ namespace template_ { | ||||
|  | ||||
| class TemplateSensor : public sensor::Sensor, public PollingComponent { | ||||
|  public: | ||||
|   void set_template(std::function<optional<float>()> &&f); | ||||
|   void set_template(optional<float> (*f)()); | ||||
|  | ||||
|   void update() override; | ||||
|  | ||||
| @@ -17,7 +17,7 @@ class TemplateSensor : public sensor::Sensor, public PollingComponent { | ||||
|   float get_setup_priority() const override; | ||||
|  | ||||
|  protected: | ||||
|   optional<std::function<optional<float>()>> f_; | ||||
|   optional<optional<float> (*)()> f_; | ||||
| }; | ||||
|  | ||||
| }  // namespace template_ | ||||
|   | ||||
| @@ -35,7 +35,7 @@ void TemplateSwitch::write_state(bool state) { | ||||
| } | ||||
| void TemplateSwitch::set_optimistic(bool optimistic) { this->optimistic_ = optimistic; } | ||||
| bool TemplateSwitch::assumed_state() { return this->assumed_state_; } | ||||
| void TemplateSwitch::set_state_lambda(std::function<optional<bool>()> &&f) { this->f_ = f; } | ||||
| void TemplateSwitch::set_state_lambda(optional<bool> (*f)()) { this->f_ = f; } | ||||
| float TemplateSwitch::get_setup_priority() const { return setup_priority::HARDWARE - 2.0f; } | ||||
| Trigger<> *TemplateSwitch::get_turn_on_trigger() const { return this->turn_on_trigger_; } | ||||
| Trigger<> *TemplateSwitch::get_turn_off_trigger() const { return this->turn_off_trigger_; } | ||||
|   | ||||
| @@ -14,7 +14,7 @@ class TemplateSwitch : public switch_::Switch, public Component { | ||||
|   void setup() override; | ||||
|   void dump_config() override; | ||||
|  | ||||
|   void set_state_lambda(std::function<optional<bool>()> &&f); | ||||
|   void set_state_lambda(optional<bool> (*f)()); | ||||
|   Trigger<> *get_turn_on_trigger() const; | ||||
|   Trigger<> *get_turn_off_trigger() const; | ||||
|   void set_optimistic(bool optimistic); | ||||
| @@ -28,7 +28,7 @@ class TemplateSwitch : public switch_::Switch, public Component { | ||||
|  | ||||
|   void write_state(bool state) override; | ||||
|  | ||||
|   optional<std::function<optional<bool>()>> f_; | ||||
|   optional<optional<bool> (*)()> f_; | ||||
|   bool optimistic_{false}; | ||||
|   bool assumed_state_{false}; | ||||
|   Trigger<> *turn_on_trigger_; | ||||
|   | ||||
| @@ -61,7 +61,7 @@ template<uint8_t SZ> class TextSaver : public TemplateTextSaverBase { | ||||
|  | ||||
| class TemplateText : public text::Text, public PollingComponent { | ||||
|  public: | ||||
|   void set_template(std::function<optional<std::string>()> &&f) { this->f_ = f; } | ||||
|   void set_template(optional<std::string> (*f)()) { this->f_ = f; } | ||||
|  | ||||
|   void setup() override; | ||||
|   void update() override; | ||||
| @@ -78,7 +78,7 @@ class TemplateText : public text::Text, public PollingComponent { | ||||
|   bool optimistic_ = false; | ||||
|   std::string initial_value_; | ||||
|   Trigger<std::string> *set_trigger_ = new Trigger<std::string>(); | ||||
|   optional<std::function<optional<std::string>()>> f_{nullptr}; | ||||
|   optional<optional<std::string> (*)()> f_{nullptr}; | ||||
|  | ||||
|   TemplateTextSaverBase *pref_ = nullptr; | ||||
| }; | ||||
|   | ||||
| @@ -16,7 +16,7 @@ void TemplateTextSensor::update() { | ||||
|   } | ||||
| } | ||||
| float TemplateTextSensor::get_setup_priority() const { return setup_priority::HARDWARE; } | ||||
| void TemplateTextSensor::set_template(std::function<optional<std::string>()> &&f) { this->f_ = f; } | ||||
| void TemplateTextSensor::set_template(optional<std::string> (*f)()) { this->f_ = f; } | ||||
| void TemplateTextSensor::dump_config() { LOG_TEXT_SENSOR("", "Template Sensor", this); } | ||||
|  | ||||
| }  // namespace template_ | ||||
|   | ||||
| @@ -9,7 +9,7 @@ namespace template_ { | ||||
|  | ||||
| class TemplateTextSensor : public text_sensor::TextSensor, public PollingComponent { | ||||
|  public: | ||||
|   void set_template(std::function<optional<std::string>()> &&f); | ||||
|   void set_template(optional<std::string> (*f)()); | ||||
|  | ||||
|   void update() override; | ||||
|  | ||||
| @@ -18,7 +18,7 @@ class TemplateTextSensor : public text_sensor::TextSensor, public PollingCompone | ||||
|   void dump_config() override; | ||||
|  | ||||
|  protected: | ||||
|   optional<std::function<optional<std::string>()>> f_{}; | ||||
|   optional<optional<std::string> (*)()> f_{}; | ||||
| }; | ||||
|  | ||||
| }  // namespace template_ | ||||
|   | ||||
| @@ -55,7 +55,7 @@ void TemplateValve::loop() { | ||||
|  | ||||
| void TemplateValve::set_optimistic(bool optimistic) { this->optimistic_ = optimistic; } | ||||
| void TemplateValve::set_assumed_state(bool assumed_state) { this->assumed_state_ = assumed_state; } | ||||
| void TemplateValve::set_state_lambda(std::function<optional<float>()> &&f) { this->state_f_ = f; } | ||||
| void TemplateValve::set_state_lambda(optional<float> (*f)()) { this->state_f_ = f; } | ||||
| float TemplateValve::get_setup_priority() const { return setup_priority::HARDWARE; } | ||||
|  | ||||
| Trigger<> *TemplateValve::get_open_trigger() const { return this->open_trigger_; } | ||||
|   | ||||
| @@ -17,7 +17,7 @@ class TemplateValve : public valve::Valve, public Component { | ||||
|  public: | ||||
|   TemplateValve(); | ||||
|  | ||||
|   void set_state_lambda(std::function<optional<float>()> &&f); | ||||
|   void set_state_lambda(optional<float> (*f)()); | ||||
|   Trigger<> *get_open_trigger() const; | ||||
|   Trigger<> *get_close_trigger() const; | ||||
|   Trigger<> *get_stop_trigger() const; | ||||
| @@ -42,7 +42,7 @@ class TemplateValve : public valve::Valve, public Component { | ||||
|   void stop_prev_trigger_(); | ||||
|  | ||||
|   TemplateValveRestoreMode restore_mode_{VALVE_NO_RESTORE}; | ||||
|   optional<std::function<optional<float>()>> state_f_; | ||||
|   optional<optional<float> (*)()> state_f_; | ||||
|   bool assumed_state_{false}; | ||||
|   bool optimistic_{false}; | ||||
|   Trigger<> *open_trigger_; | ||||
|   | ||||
| @@ -57,6 +57,7 @@ validate_filters = cv.validate_registry("filter", FILTER_REGISTRY) | ||||
| # Filters | ||||
| Filter = text_sensor_ns.class_("Filter") | ||||
| LambdaFilter = text_sensor_ns.class_("LambdaFilter", Filter) | ||||
| StatelessLambdaFilter = text_sensor_ns.class_("StatelessLambdaFilter", Filter) | ||||
| ToUpperFilter = text_sensor_ns.class_("ToUpperFilter", Filter) | ||||
| ToLowerFilter = text_sensor_ns.class_("ToLowerFilter", Filter) | ||||
| AppendFilter = text_sensor_ns.class_("AppendFilter", Filter) | ||||
| @@ -70,7 +71,7 @@ async def lambda_filter_to_code(config, filter_id): | ||||
|     lambda_ = await cg.process_lambda( | ||||
|         config, [(cg.std_string, "x")], return_type=cg.optional.template(cg.std_string) | ||||
|     ) | ||||
|     return cg.new_Pvariable(filter_id, lambda_) | ||||
|     return automation.new_lambda_pvariable(filter_id, lambda_, StatelessLambdaFilter) | ||||
|  | ||||
|  | ||||
| @FILTER_REGISTRY.register("to_upper", ToUpperFilter, {}) | ||||
|   | ||||
| @@ -62,6 +62,21 @@ class LambdaFilter : public Filter { | ||||
|   lambda_filter_t lambda_filter_; | ||||
| }; | ||||
|  | ||||
| /** Optimized lambda filter for stateless lambdas (no capture). | ||||
|  * | ||||
|  * Uses function pointer instead of std::function to reduce memory overhead. | ||||
|  * Memory: 4 bytes (function pointer on 32-bit) vs 32 bytes (std::function). | ||||
|  */ | ||||
| class StatelessLambdaFilter : public Filter { | ||||
|  public: | ||||
|   explicit StatelessLambdaFilter(optional<std::string> (*lambda_filter)(std::string)) : lambda_filter_(lambda_filter) {} | ||||
|  | ||||
|   optional<std::string> new_value(std::string value) override { return this->lambda_filter_(value); } | ||||
|  | ||||
|  protected: | ||||
|   optional<std::string> (*lambda_filter_)(std::string); | ||||
| }; | ||||
|  | ||||
| /// A simple filter that converts all text to uppercase | ||||
| class ToUpperFilter : public Filter { | ||||
|  public: | ||||
|   | ||||
| @@ -99,10 +99,26 @@ void IDFUARTComponent::setup() { | ||||
| } | ||||
|  | ||||
| void IDFUARTComponent::load_settings(bool dump_config) { | ||||
|   uart_config_t uart_config = this->get_config_(); | ||||
|   esp_err_t err = uart_param_config(this->uart_num_, &uart_config); | ||||
|   esp_err_t err; | ||||
|  | ||||
|   if (uart_is_driver_installed(this->uart_num_)) { | ||||
|     err = uart_driver_delete(this->uart_num_); | ||||
|     if (err != ESP_OK) { | ||||
|       ESP_LOGW(TAG, "uart_driver_delete failed: %s", esp_err_to_name(err)); | ||||
|       this->mark_failed(); | ||||
|       return; | ||||
|     } | ||||
|   } | ||||
|   err = uart_driver_install(this->uart_num_,        // UART number | ||||
|                             this->rx_buffer_size_,  // RX ring buffer size | ||||
|                             0,   // TX ring buffer size. If zero, driver will not use a TX buffer and TX function will | ||||
|                                  // block task until all data has been sent out | ||||
|                             20,  // event queue size/depth | ||||
|                             &this->uart_event_queue_,  // event queue | ||||
|                             0                          // Flags used to allocate the interrupt | ||||
|   ); | ||||
|   if (err != ESP_OK) { | ||||
|     ESP_LOGW(TAG, "uart_param_config failed: %s", esp_err_to_name(err)); | ||||
|     ESP_LOGW(TAG, "uart_driver_install failed: %s", esp_err_to_name(err)); | ||||
|     this->mark_failed(); | ||||
|     return; | ||||
|   } | ||||
| @@ -119,10 +135,12 @@ void IDFUARTComponent::load_settings(bool dump_config) { | ||||
|   int8_t flow_control = this->flow_control_pin_ != nullptr ? this->flow_control_pin_->get_pin() : -1; | ||||
|  | ||||
|   uint32_t invert = 0; | ||||
|   if (this->tx_pin_ != nullptr && this->tx_pin_->is_inverted()) | ||||
|   if (this->tx_pin_ != nullptr && this->tx_pin_->is_inverted()) { | ||||
|     invert |= UART_SIGNAL_TXD_INV; | ||||
|   if (this->rx_pin_ != nullptr && this->rx_pin_->is_inverted()) | ||||
|   } | ||||
|   if (this->rx_pin_ != nullptr && this->rx_pin_->is_inverted()) { | ||||
|     invert |= UART_SIGNAL_RXD_INV; | ||||
|   } | ||||
|  | ||||
|   err = uart_set_line_inverse(this->uart_num_, invert); | ||||
|   if (err != ESP_OK) { | ||||
| @@ -138,26 +156,6 @@ void IDFUARTComponent::load_settings(bool dump_config) { | ||||
|     return; | ||||
|   } | ||||
|  | ||||
|   if (uart_is_driver_installed(this->uart_num_)) { | ||||
|     uart_driver_delete(this->uart_num_); | ||||
|     if (err != ESP_OK) { | ||||
|       ESP_LOGW(TAG, "uart_driver_delete failed: %s", esp_err_to_name(err)); | ||||
|       this->mark_failed(); | ||||
|       return; | ||||
|     } | ||||
|   } | ||||
|   err = uart_driver_install(this->uart_num_, /* UART RX ring buffer size. */ this->rx_buffer_size_, | ||||
|                             /* UART TX ring buffer size. If set to zero, driver will not use TX buffer, TX function will | ||||
|                                block task until all data have been sent out.*/ | ||||
|                             0, | ||||
|                             /* UART event queue size/depth. */ 20, &(this->uart_event_queue_), | ||||
|                             /* Flags used to allocate the interrupt. */ 0); | ||||
|   if (err != ESP_OK) { | ||||
|     ESP_LOGW(TAG, "uart_driver_install failed: %s", esp_err_to_name(err)); | ||||
|     this->mark_failed(); | ||||
|     return; | ||||
|   } | ||||
|  | ||||
|   err = uart_set_rx_full_threshold(this->uart_num_, this->rx_full_threshold_); | ||||
|   if (err != ESP_OK) { | ||||
|     ESP_LOGW(TAG, "uart_set_rx_full_threshold failed: %s", esp_err_to_name(err)); | ||||
| @@ -173,24 +171,32 @@ void IDFUARTComponent::load_settings(bool dump_config) { | ||||
|   } | ||||
|  | ||||
|   auto mode = this->flow_control_pin_ != nullptr ? UART_MODE_RS485_HALF_DUPLEX : UART_MODE_UART; | ||||
|   err = uart_set_mode(this->uart_num_, mode); | ||||
|   err = uart_set_mode(this->uart_num_, mode);  // per docs, must be called only after uart_driver_install() | ||||
|   if (err != ESP_OK) { | ||||
|     ESP_LOGW(TAG, "uart_set_mode failed: %s", esp_err_to_name(err)); | ||||
|     this->mark_failed(); | ||||
|     return; | ||||
|   } | ||||
|  | ||||
|   uart_config_t uart_config = this->get_config_(); | ||||
|   err = uart_param_config(this->uart_num_, &uart_config); | ||||
|   if (err != ESP_OK) { | ||||
|     ESP_LOGW(TAG, "uart_param_config failed: %s", esp_err_to_name(err)); | ||||
|     this->mark_failed(); | ||||
|     return; | ||||
|   } | ||||
|  | ||||
|   if (dump_config) { | ||||
|     ESP_LOGCONFIG(TAG, "UART %u was reloaded.", this->uart_num_); | ||||
|     ESP_LOGCONFIG(TAG, "Reloaded UART %u", this->uart_num_); | ||||
|     this->dump_config(); | ||||
|   } | ||||
| } | ||||
|  | ||||
| void IDFUARTComponent::dump_config() { | ||||
|   ESP_LOGCONFIG(TAG, "UART Bus %u:", this->uart_num_); | ||||
|   LOG_PIN("  TX Pin: ", tx_pin_); | ||||
|   LOG_PIN("  RX Pin: ", rx_pin_); | ||||
|   LOG_PIN("  Flow Control Pin: ", flow_control_pin_); | ||||
|   LOG_PIN("  TX Pin: ", this->tx_pin_); | ||||
|   LOG_PIN("  RX Pin: ", this->rx_pin_); | ||||
|   LOG_PIN("  Flow Control Pin: ", this->flow_control_pin_); | ||||
|   if (this->rx_pin_ != nullptr) { | ||||
|     ESP_LOGCONFIG(TAG, | ||||
|                   "  RX Buffer Size: %u\n" | ||||
|   | ||||
| @@ -3,7 +3,7 @@ from esphome.automation import Condition | ||||
| import esphome.codegen as cg | ||||
| from esphome.components.const import CONF_USE_PSRAM | ||||
| from esphome.components.esp32 import add_idf_sdkconfig_option, const, get_esp32_variant | ||||
| from esphome.components.network import IPAddress | ||||
| from esphome.components.network import ip_address_literal | ||||
| from esphome.config_helpers import filter_source_files_from_platform | ||||
| import esphome.config_validation as cv | ||||
| from esphome.config_validation import only_with_esp_idf | ||||
| @@ -334,9 +334,7 @@ def eap_auth(config): | ||||
|  | ||||
|  | ||||
| def safe_ip(ip): | ||||
|     if ip is None: | ||||
|         return IPAddress(0, 0, 0, 0) | ||||
|     return IPAddress(str(ip)) | ||||
|     return ip_address_literal(ip) | ||||
|  | ||||
|  | ||||
| def manual_ip(config): | ||||
|   | ||||
| @@ -26,10 +26,10 @@ class ZephyrGPIOPin : public InternalGPIOPin { | ||||
|  protected: | ||||
|   void attach_interrupt(void (*func)(void *), void *arg, gpio::InterruptType type) const override; | ||||
|   uint8_t pin_; | ||||
|   bool inverted_; | ||||
|   gpio::Flags flags_; | ||||
|   const device *gpio_ = nullptr; | ||||
|   bool value_ = false; | ||||
|   bool inverted_{}; | ||||
|   gpio::Flags flags_{}; | ||||
|   const device *gpio_{nullptr}; | ||||
|   bool value_{false}; | ||||
| }; | ||||
|  | ||||
| }  // namespace zephyr | ||||
|   | ||||
| @@ -4,6 +4,8 @@ | ||||
| #include "esphome/core/defines.h" | ||||
| #include "esphome/core/helpers.h" | ||||
| #include "esphome/core/preferences.h" | ||||
| #include <concepts> | ||||
| #include <functional> | ||||
| #include <utility> | ||||
| #include <vector> | ||||
|  | ||||
| @@ -27,11 +29,20 @@ template<typename T, typename... X> class TemplatableValue { | ||||
|  public: | ||||
|   TemplatableValue() : type_(NONE) {} | ||||
|  | ||||
|   template<typename F, enable_if_t<!is_invocable<F, X...>::value, int> = 0> TemplatableValue(F value) : type_(VALUE) { | ||||
|   template<typename F> TemplatableValue(F value) requires(!std::invocable<F, X...>) : type_(VALUE) { | ||||
|     new (&this->value_) T(std::move(value)); | ||||
|   } | ||||
|  | ||||
|   template<typename F, enable_if_t<is_invocable<F, X...>::value, int> = 0> TemplatableValue(F f) : type_(LAMBDA) { | ||||
|   // For stateless lambdas (convertible to function pointer): use function pointer | ||||
|   template<typename F> | ||||
|   TemplatableValue(F f) requires std::invocable<F, X...> && std::convertible_to<F, T (*)(X...)> | ||||
|       : type_(STATELESS_LAMBDA) { | ||||
|     this->stateless_f_ = f;  // Implicit conversion to function pointer | ||||
|   } | ||||
|  | ||||
|   // For stateful lambdas (not convertible to function pointer): use std::function | ||||
|   template<typename F> | ||||
|   TemplatableValue(F f) requires std::invocable<F, X...> &&(!std::convertible_to<F, T (*)(X...)>) : type_(LAMBDA) { | ||||
|     this->f_ = new std::function<T(X...)>(std::move(f)); | ||||
|   } | ||||
|  | ||||
| @@ -41,6 +52,8 @@ template<typename T, typename... X> class TemplatableValue { | ||||
|       new (&this->value_) T(other.value_); | ||||
|     } else if (type_ == LAMBDA) { | ||||
|       this->f_ = new std::function<T(X...)>(*other.f_); | ||||
|     } else if (type_ == STATELESS_LAMBDA) { | ||||
|       this->stateless_f_ = other.stateless_f_; | ||||
|     } | ||||
|   } | ||||
|  | ||||
| @@ -51,6 +64,8 @@ template<typename T, typename... X> class TemplatableValue { | ||||
|     } else if (type_ == LAMBDA) { | ||||
|       this->f_ = other.f_; | ||||
|       other.f_ = nullptr; | ||||
|     } else if (type_ == STATELESS_LAMBDA) { | ||||
|       this->stateless_f_ = other.stateless_f_; | ||||
|     } | ||||
|     other.type_ = NONE; | ||||
|   } | ||||
| @@ -78,16 +93,23 @@ template<typename T, typename... X> class TemplatableValue { | ||||
|     } else if (type_ == LAMBDA) { | ||||
|       delete this->f_; | ||||
|     } | ||||
|     // STATELESS_LAMBDA/NONE: no cleanup needed (function pointer or empty, not heap-allocated) | ||||
|   } | ||||
|  | ||||
|   bool has_value() { return this->type_ != NONE; } | ||||
|  | ||||
|   T value(X... x) { | ||||
|     if (this->type_ == LAMBDA) { | ||||
|       return (*this->f_)(x...); | ||||
|     switch (this->type_) { | ||||
|       case STATELESS_LAMBDA: | ||||
|         return this->stateless_f_(x...);  // Direct function pointer call | ||||
|       case LAMBDA: | ||||
|         return (*this->f_)(x...);  // std::function call | ||||
|       case VALUE: | ||||
|         return this->value_; | ||||
|       case NONE: | ||||
|       default: | ||||
|         return T{}; | ||||
|     } | ||||
|     // return value also when none | ||||
|     return this->type_ == VALUE ? this->value_ : T{}; | ||||
|   } | ||||
|  | ||||
|   optional<T> optional_value(X... x) { | ||||
| @@ -109,11 +131,13 @@ template<typename T, typename... X> class TemplatableValue { | ||||
|     NONE, | ||||
|     VALUE, | ||||
|     LAMBDA, | ||||
|     STATELESS_LAMBDA, | ||||
|   } type_; | ||||
|  | ||||
|   union { | ||||
|     T value_; | ||||
|     std::function<T(X...)> *f_; | ||||
|     T (*stateless_f_)(X...); | ||||
|   }; | ||||
| }; | ||||
|  | ||||
|   | ||||
| @@ -79,6 +79,18 @@ template<typename... Ts> class LambdaCondition : public Condition<Ts...> { | ||||
|   std::function<bool(Ts...)> f_; | ||||
| }; | ||||
|  | ||||
| /// Optimized lambda condition for stateless lambdas (no capture). | ||||
| /// Uses function pointer instead of std::function to reduce memory overhead. | ||||
| /// Memory: 4 bytes (function pointer on 32-bit) vs 32 bytes (std::function). | ||||
| template<typename... Ts> class StatelessLambdaCondition : public Condition<Ts...> { | ||||
|  public: | ||||
|   explicit StatelessLambdaCondition(bool (*f)(Ts...)) : f_(f) {} | ||||
|   bool check(Ts... x) override { return this->f_(x...); } | ||||
|  | ||||
|  protected: | ||||
|   bool (*f_)(Ts...); | ||||
| }; | ||||
|  | ||||
| template<typename... Ts> class ForCondition : public Condition<Ts...>, public Component { | ||||
|  public: | ||||
|   explicit ForCondition(Condition<> *condition) : condition_(condition) {} | ||||
| @@ -190,6 +202,19 @@ template<typename... Ts> class LambdaAction : public Action<Ts...> { | ||||
|   std::function<void(Ts...)> f_; | ||||
| }; | ||||
|  | ||||
| /// Optimized lambda action for stateless lambdas (no capture). | ||||
| /// Uses function pointer instead of std::function to reduce memory overhead. | ||||
| /// Memory: 4 bytes (function pointer on 32-bit) vs 32 bytes (std::function). | ||||
| template<typename... Ts> class StatelessLambdaAction : public Action<Ts...> { | ||||
|  public: | ||||
|   explicit StatelessLambdaAction(void (*f)(Ts...)) : f_(f) {} | ||||
|  | ||||
|   void play(Ts... x) override { this->f_(x...); } | ||||
|  | ||||
|  protected: | ||||
|   void (*f_)(Ts...); | ||||
| }; | ||||
|  | ||||
| template<typename... Ts> class IfAction : public Action<Ts...> { | ||||
|  public: | ||||
|   explicit IfAction(Condition<Ts...> *condition) : condition_(condition) {} | ||||
|   | ||||
| @@ -123,6 +123,7 @@ | ||||
| #define USE_API_NOISE | ||||
| #define USE_API_PLAINTEXT | ||||
| #define USE_API_SERVICES | ||||
| #define USE_API_CUSTOM_SERVICES | ||||
| #define API_MAX_SEND_QUEUE 8 | ||||
| #define USE_MD5 | ||||
| #define USE_SHA256 | ||||
|   | ||||
| @@ -198,6 +198,8 @@ class LambdaExpression(Expression): | ||||
|         self.return_type = safe_exp(return_type) if return_type is not None else None | ||||
|  | ||||
|     def __str__(self): | ||||
|         # Stateless lambdas (empty capture) implicitly convert to function pointers | ||||
|         # when assigned to function pointer types - no unary + needed | ||||
|         cpp = f"[{self.capture}]({self.parameters})" | ||||
|         if self.return_type is not None: | ||||
|             cpp += f" -> {self.return_type}" | ||||
| @@ -700,6 +702,12 @@ async def process_lambda( | ||||
|             parts[i * 3 + 1] = var | ||||
|         parts[i * 3 + 2] = "" | ||||
|  | ||||
|     # All id() references are global variables in generated C++ code. | ||||
|     # Global variables should not be captured - they're accessible everywhere. | ||||
|     # Use empty capture instead of capture-by-value. | ||||
|     if capture == "=": | ||||
|         capture = "" | ||||
|  | ||||
|     if isinstance(value, ESPHomeDataBase) and value.esp_range is not None: | ||||
|         location = value.esp_range.start_mark | ||||
|         location.line += value.content_offset | ||||
|   | ||||
| @@ -23,6 +23,7 @@ size_t = global_ns.namespace("size_t") | ||||
| const_char_ptr = global_ns.namespace("const char *") | ||||
| NAN = global_ns.namespace("NAN") | ||||
| esphome_ns = global_ns  # using namespace esphome; | ||||
| FixedVector = esphome_ns.class_("FixedVector") | ||||
| App = esphome_ns.App | ||||
| EntityBase = esphome_ns.class_("EntityBase") | ||||
| Component = esphome_ns.class_("Component") | ||||
|   | ||||
| @@ -19,7 +19,9 @@ classifiers = [ | ||||
|     "Programming Language :: Python :: 3", | ||||
|     "Topic :: Home Automation", | ||||
| ] | ||||
| requires-python = ">=3.11.0" | ||||
|  | ||||
| # Python 3.14 is currently not supported by IDF <= 5.5.1, see https://github.com/esphome/esphome/issues/11502 | ||||
| requires-python = ">=3.11.0,<3.14" | ||||
|  | ||||
| dynamic = ["dependencies", "optional-dependencies", "version"] | ||||
|  | ||||
|   | ||||
| @@ -58,7 +58,7 @@ def test_text_config_value_mode_set(generate_main): | ||||
|  | ||||
| def test_text_config_lamda_is_set(generate_main): | ||||
|     """ | ||||
|     Test if lambda is set for lambda mode | ||||
|     Test if lambda is set for lambda mode (optimized with stateless lambda) | ||||
|     """ | ||||
|     # Given | ||||
|  | ||||
| @@ -66,5 +66,5 @@ def test_text_config_lamda_is_set(generate_main): | ||||
|     main_cpp = generate_main("tests/component_tests/text/test_text.yaml") | ||||
|  | ||||
|     # Then | ||||
|     assert "it_4->set_template([=]() -> esphome::optional<std::string> {" in main_cpp | ||||
|     assert "it_4->set_template([]() -> esphome::optional<std::string> {" in main_cpp | ||||
|     assert 'return std::string{"Hello"};' in main_cpp | ||||
|   | ||||
| @@ -173,6 +173,61 @@ class TestLambdaExpression: | ||||
|             "}" | ||||
|         ) | ||||
|  | ||||
|     def test_str__stateless_no_return(self): | ||||
|         """Test stateless lambda (empty capture) generates correctly""" | ||||
|         target = cg.LambdaExpression( | ||||
|             ('ESP_LOGD("main", "Test message");',), | ||||
|             (),  # No parameters | ||||
|             "",  # Empty capture (stateless) | ||||
|         ) | ||||
|  | ||||
|         actual = str(target) | ||||
|  | ||||
|         assert actual == ('[]() {\n  ESP_LOGD("main", "Test message");\n}') | ||||
|  | ||||
|     def test_str__stateless_with_return(self): | ||||
|         """Test stateless lambda with return type generates correctly""" | ||||
|         target = cg.LambdaExpression( | ||||
|             ("return global_value > 0;",), | ||||
|             (),  # No parameters | ||||
|             "",  # Empty capture (stateless) | ||||
|             bool,  # Return type | ||||
|         ) | ||||
|  | ||||
|         actual = str(target) | ||||
|  | ||||
|         assert actual == ("[]() -> bool {\n  return global_value > 0;\n}") | ||||
|  | ||||
|     def test_str__stateless_with_params(self): | ||||
|         """Test stateless lambda with parameters generates correctly""" | ||||
|         target = cg.LambdaExpression( | ||||
|             ("return foo + bar;",), | ||||
|             ((int, "foo"), (float, "bar")), | ||||
|             "",  # Empty capture (stateless) | ||||
|             float, | ||||
|         ) | ||||
|  | ||||
|         actual = str(target) | ||||
|  | ||||
|         assert actual == ( | ||||
|             "[](int32_t foo, float bar) -> float {\n  return foo + bar;\n}" | ||||
|         ) | ||||
|  | ||||
|     def test_str__with_capture(self): | ||||
|         """Test lambda with capture generates correctly""" | ||||
|         target = cg.LambdaExpression( | ||||
|             ("return captured_var + x;",), | ||||
|             ((int, "x"),), | ||||
|             "captured_var",  # Has capture (not stateless) | ||||
|             int, | ||||
|         ) | ||||
|  | ||||
|         actual = str(target) | ||||
|  | ||||
|         assert actual == ( | ||||
|             "[captured_var](int32_t x) -> int32_t {\n  return captured_var + x;\n}" | ||||
|         ) | ||||
|  | ||||
|  | ||||
| class TestLiterals: | ||||
|     @pytest.mark.parametrize( | ||||
|   | ||||
| @@ -1,6 +1,7 @@ | ||||
| import glob | ||||
| import logging | ||||
| from pathlib import Path | ||||
| from typing import Any | ||||
|  | ||||
| from esphome import config as config_module, yaml_util | ||||
| from esphome.components import substitutions | ||||
| @@ -60,6 +61,29 @@ def write_yaml(path: Path, data: dict) -> None: | ||||
|     path.write_text(yaml_util.dump(data), encoding="utf-8") | ||||
|  | ||||
|  | ||||
| def verify_database(value: Any, path: str = "") -> str | None: | ||||
|     if isinstance(value, list): | ||||
|         for i, v in enumerate(value): | ||||
|             result = verify_database(v, f"{path}[{i}]") | ||||
|             if result is not None: | ||||
|                 return result | ||||
|         return None | ||||
|     if isinstance(value, dict): | ||||
|         for k, v in value.items(): | ||||
|             key_result = verify_database(k, f"{path}/{k}") | ||||
|             if key_result is not None: | ||||
|                 return key_result | ||||
|             value_result = verify_database(v, f"{path}/{k}") | ||||
|             if value_result is not None: | ||||
|                 return value_result | ||||
|         return None | ||||
|     if isinstance(value, str): | ||||
|         if not isinstance(value, yaml_util.ESPHomeDataBase): | ||||
|             return f"{path}: {value!r} is not ESPHomeDataBase" | ||||
|         return None | ||||
|     return None | ||||
|  | ||||
|  | ||||
| def test_substitutions_fixtures(fixture_path): | ||||
|     base_dir = fixture_path / "substitutions" | ||||
|     sources = sorted(glob.glob(str(base_dir / "*.input.yaml"))) | ||||
| @@ -83,6 +107,9 @@ def test_substitutions_fixtures(fixture_path): | ||||
|             substitutions.do_substitution_pass(config, None) | ||||
|  | ||||
|             resolve_extend_remove(config) | ||||
|             verify_database_result = verify_database(config) | ||||
|             if verify_database_result is not None: | ||||
|                 raise AssertionError(verify_database_result) | ||||
|  | ||||
|             # Also load expected using ESPHome's loader, or use {} if missing and DEV_MODE | ||||
|             if expected_path.is_file(): | ||||
|   | ||||
		Reference in New Issue
	
	Block a user