mirror of
				https://github.com/esphome/esphome.git
				synced 2025-10-31 07:03:55 +00:00 
			
		
		
		
	Merge remote-tracking branch 'upstream/dev' into select_options_in_flash
# Conflicts: # esphome/components/template/select/template_select.cpp
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; | ||||
|   | ||||
| @@ -304,9 +304,13 @@ def _format_framework_arduino_version(ver: cv.Version) -> str: | ||||
| def _format_framework_espidf_version(ver: cv.Version, release: str) -> str: | ||||
|     # format the given espidf (https://github.com/pioarduino/esp-idf/releases) version to | ||||
|     # a PIO platformio/framework-espidf value | ||||
|     if ver == cv.Version(5, 4, 3) or ver >= cv.Version(5, 5, 1): | ||||
|         ext = "tar.xz" | ||||
|     else: | ||||
|         ext = "zip" | ||||
|     if release: | ||||
|         return f"pioarduino/framework-espidf@https://github.com/pioarduino/esp-idf/releases/download/v{str(ver)}.{release}/esp-idf-v{str(ver)}.zip" | ||||
|     return f"pioarduino/framework-espidf@https://github.com/pioarduino/esp-idf/releases/download/v{str(ver)}/esp-idf-v{str(ver)}.zip" | ||||
|         return f"pioarduino/framework-espidf@https://github.com/pioarduino/esp-idf/releases/download/v{str(ver)}.{release}/esp-idf-v{str(ver)}.{ext}" | ||||
|     return f"pioarduino/framework-espidf@https://github.com/pioarduino/esp-idf/releases/download/v{str(ver)}/esp-idf-v{str(ver)}.{ext}" | ||||
|  | ||||
|  | ||||
| def _is_framework_url(source: str) -> str: | ||||
| @@ -355,6 +359,7 @@ ESP_IDF_FRAMEWORK_VERSION_LOOKUP = { | ||||
| ESP_IDF_PLATFORM_VERSION_LOOKUP = { | ||||
|     cv.Version(5, 5, 1): cv.Version(55, 3, 31, "1"), | ||||
|     cv.Version(5, 5, 0): cv.Version(55, 3, 31, "1"), | ||||
|     cv.Version(5, 4, 3): cv.Version(55, 3, 32), | ||||
|     cv.Version(5, 4, 2): cv.Version(54, 3, 21, "2"), | ||||
|     cv.Version(5, 4, 1): cv.Version(54, 3, 21, "2"), | ||||
|     cv.Version(5, 4, 0): cv.Version(54, 3, 21, "2"), | ||||
|   | ||||
| @@ -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( | ||||
|   | ||||
| @@ -540,6 +540,23 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe | ||||
|    */ | ||||
|   void goto_page(uint8_t page); | ||||
|  | ||||
|   /** | ||||
|    * Set the visibility of a component. | ||||
|    * | ||||
|    * @param component The component name. | ||||
|    * @param show True to show the component, false to hide it. | ||||
|    * | ||||
|    * @see show_component() | ||||
|    * @see hide_component() | ||||
|    * | ||||
|    * Example: | ||||
|    * ```cpp | ||||
|    * it.set_component_visibility("textview", true);   // Equivalent to show_component("textview") | ||||
|    * it.set_component_visibility("textview", false);  // Equivalent to hide_component("textview") | ||||
|    * ``` | ||||
|    */ | ||||
|   void set_component_visibility(const char *component, bool show) override; | ||||
|  | ||||
|   /** | ||||
|    * Hide a component. | ||||
|    * @param component The component name. | ||||
|   | ||||
| @@ -45,6 +45,7 @@ class NextionBase { | ||||
|   virtual void set_component_pressed_font_color(const char *component, Color color) = 0; | ||||
|   virtual void set_component_font(const char *component, uint8_t font_id) = 0; | ||||
|  | ||||
|   virtual void set_component_visibility(const char *component, bool show) = 0; | ||||
|   virtual void show_component(const char *component) = 0; | ||||
|   virtual void hide_component(const char *component) = 0; | ||||
|  | ||||
|   | ||||
| @@ -201,13 +201,13 @@ void Nextion::set_component_font(const char *component, uint8_t font_id) { | ||||
|   this->add_no_result_to_queue_with_printf_("set_component_font", "%s.font=%" PRIu8, component, font_id); | ||||
| } | ||||
|  | ||||
| void Nextion::hide_component(const char *component) { | ||||
|   this->add_no_result_to_queue_with_printf_("hide_component", "vis %s,0", component); | ||||
| void Nextion::set_component_visibility(const char *component, bool show) { | ||||
|   this->add_no_result_to_queue_with_printf_("set_component_visibility", "vis %s,%d", component, show ? 1 : 0); | ||||
| } | ||||
|  | ||||
| void Nextion::show_component(const char *component) { | ||||
|   this->add_no_result_to_queue_with_printf_("show_component", "vis %s,1", component); | ||||
| } | ||||
| void Nextion::hide_component(const char *component) { this->set_component_visibility(component, false); } | ||||
|  | ||||
| void Nextion::show_component(const char *component) { this->set_component_visibility(component, true); } | ||||
|  | ||||
| void Nextion::enable_component_touch(const char *component) { | ||||
|   this->add_no_result_to_queue_with_printf_("enable_component_touch", "tsw %s,1", component); | ||||
|   | ||||
| @@ -81,13 +81,11 @@ void NextionComponent::update_component_settings(bool force_update) { | ||||
|  | ||||
|     this->component_flags_.visible_needs_update = false; | ||||
|  | ||||
|     if (this->component_flags_.visible) { | ||||
|       this->nextion_->show_component(name_to_send.c_str()); | ||||
|       this->send_state_to_nextion(); | ||||
|     } else { | ||||
|       this->nextion_->hide_component(name_to_send.c_str()); | ||||
|     this->nextion_->set_component_visibility(name_to_send.c_str(), this->component_flags_.visible); | ||||
|     if (!this->component_flags_.visible) { | ||||
|       return; | ||||
|     } | ||||
|     this->send_state_to_nextion(); | ||||
|   } | ||||
|  | ||||
|   if (this->component_flags_.bco_needs_update || (force_update && this->component_flags_.bco2_is_set)) { | ||||
|   | ||||
| @@ -174,11 +174,6 @@ bool Nextion::upload_tft(uint32_t baud_rate, bool exit_reparse) { | ||||
|  | ||||
|   // Check if baud rate is supported | ||||
|   this->original_baud_rate_ = this->parent_->get_baud_rate(); | ||||
|   static const std::vector<uint32_t> SUPPORTED_BAUD_RATES = {2400,   4800,   9600,   19200,  31250,  38400, 57600, | ||||
|                                                              115200, 230400, 250000, 256000, 512000, 921600}; | ||||
|   if (std::find(SUPPORTED_BAUD_RATES.begin(), SUPPORTED_BAUD_RATES.end(), baud_rate) == SUPPORTED_BAUD_RATES.end()) { | ||||
|     baud_rate = this->original_baud_rate_; | ||||
|   } | ||||
|   ESP_LOGD(TAG, "Baud rate: %" PRIu32, baud_rate); | ||||
|  | ||||
|   // Define the configuration for the HTTP client | ||||
|   | ||||
| @@ -177,11 +177,6 @@ bool Nextion::upload_tft(uint32_t baud_rate, bool exit_reparse) { | ||||
|  | ||||
|   // Check if baud rate is supported | ||||
|   this->original_baud_rate_ = this->parent_->get_baud_rate(); | ||||
|   static const std::vector<uint32_t> SUPPORTED_BAUD_RATES = {2400,   4800,   9600,   19200,  31250,  38400, 57600, | ||||
|                                                              115200, 230400, 250000, 256000, 512000, 921600}; | ||||
|   if (std::find(SUPPORTED_BAUD_RATES.begin(), SUPPORTED_BAUD_RATES.end(), baud_rate) == SUPPORTED_BAUD_RATES.end()) { | ||||
|     baud_rate = this->original_baud_rate_; | ||||
|   } | ||||
|   ESP_LOGD(TAG, "Baud rate: %" PRIu32, baud_rate); | ||||
|  | ||||
|   // Define the configuration for the HTTP client | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -252,7 +252,10 @@ async def setup_number_core_( | ||||
|     cg.add(var.traits.set_max_value(max_value)) | ||||
|     cg.add(var.traits.set_step(step)) | ||||
|  | ||||
|     cg.add(var.traits.set_mode(config[CONF_MODE])) | ||||
|     # Only set if non-default to avoid bloating setup() function | ||||
|     # (mode_ is initialized to NUMBER_MODE_AUTO in the header) | ||||
|     if config[CONF_MODE] != NumberMode.NUMBER_MODE_AUTO: | ||||
|         cg.add(var.traits.set_mode(config[CONF_MODE])) | ||||
|  | ||||
|     for conf in config.get(CONF_ON_VALUE, []): | ||||
|         trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) | ||||
|   | ||||
| @@ -12,6 +12,25 @@ | ||||
| namespace esphome { | ||||
| namespace remote_transmitter { | ||||
|  | ||||
| #ifdef USE_ESP32 | ||||
| #if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 5, 1) | ||||
| // IDF version 5.5.1 and above is required because of a bug in | ||||
| // the RMT encoder: https://github.com/espressif/esp-idf/issues/17244 | ||||
| typedef union {  // NOLINT(modernize-use-using) | ||||
|   struct { | ||||
|     uint16_t duration : 15; | ||||
|     uint16_t level : 1; | ||||
|   }; | ||||
|   uint16_t val; | ||||
| } rmt_symbol_half_t; | ||||
|  | ||||
| struct RemoteTransmitterComponentStore { | ||||
|   uint32_t times{0}; | ||||
|   uint32_t index{0}; | ||||
| }; | ||||
| #endif | ||||
| #endif | ||||
|  | ||||
| class RemoteTransmitterComponent : public remote_base::RemoteTransmitterBase, | ||||
|                                    public Component | ||||
| #ifdef USE_ESP32 | ||||
| @@ -56,9 +75,14 @@ class RemoteTransmitterComponent : public remote_base::RemoteTransmitterBase, | ||||
| #ifdef USE_ESP32 | ||||
|   void configure_rmt_(); | ||||
|  | ||||
| #if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 5, 1) | ||||
|   RemoteTransmitterComponentStore store_{}; | ||||
|   std::vector<rmt_symbol_half_t> rmt_temp_; | ||||
| #else | ||||
|   std::vector<rmt_symbol_word_t> rmt_temp_; | ||||
| #endif | ||||
|   uint32_t current_carrier_frequency_{38000}; | ||||
|   bool initialized_{false}; | ||||
|   std::vector<rmt_symbol_word_t> rmt_temp_; | ||||
|   bool with_dma_{false}; | ||||
|   bool eot_level_{false}; | ||||
|   rmt_channel_handle_t channel_{NULL}; | ||||
|   | ||||
| @@ -10,6 +10,46 @@ namespace remote_transmitter { | ||||
|  | ||||
| static const char *const TAG = "remote_transmitter"; | ||||
|  | ||||
| // Maximum RMT symbol duration (15-bit field) | ||||
| static constexpr uint32_t RMT_SYMBOL_DURATION_MAX = 0x7FFF; | ||||
|  | ||||
| #if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 5, 1) | ||||
| static size_t IRAM_ATTR HOT encoder_callback(const void *data, size_t size, size_t written, size_t free, | ||||
|                                              rmt_symbol_word_t *symbols, bool *done, void *arg) { | ||||
|   auto *store = static_cast<RemoteTransmitterComponentStore *>(arg); | ||||
|   const auto *encoded = static_cast<const rmt_symbol_half_t *>(data); | ||||
|   size_t length = size / sizeof(rmt_symbol_half_t); | ||||
|   size_t count = 0; | ||||
|  | ||||
|   // copy symbols | ||||
|   for (size_t i = 0; i < free; i++) { | ||||
|     uint16_t sym_0 = encoded[store->index++].val; | ||||
|     if (store->index >= length) { | ||||
|       store->index = 0; | ||||
|       store->times--; | ||||
|       if (store->times == 0) { | ||||
|         *done = true; | ||||
|         symbols[count++].val = sym_0; | ||||
|         return count; | ||||
|       } | ||||
|     } | ||||
|     uint16_t sym_1 = encoded[store->index++].val; | ||||
|     if (store->index >= length) { | ||||
|       store->index = 0; | ||||
|       store->times--; | ||||
|       if (store->times == 0) { | ||||
|         *done = true; | ||||
|         symbols[count++].val = sym_0 | (sym_1 << 16); | ||||
|         return count; | ||||
|       } | ||||
|     } | ||||
|     symbols[count++].val = sym_0 | (sym_1 << 16); | ||||
|   } | ||||
|   *done = false; | ||||
|   return count; | ||||
| } | ||||
| #endif | ||||
|  | ||||
| void RemoteTransmitterComponent::setup() { | ||||
|   this->inverted_ = this->pin_->is_inverted(); | ||||
|   this->configure_rmt_(); | ||||
| @@ -34,6 +74,17 @@ void RemoteTransmitterComponent::dump_config() { | ||||
| } | ||||
|  | ||||
| void RemoteTransmitterComponent::digital_write(bool value) { | ||||
| #if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 5, 1) | ||||
|   rmt_symbol_half_t symbol = { | ||||
|       .duration = 1, | ||||
|       .level = value, | ||||
|   }; | ||||
|   rmt_transmit_config_t config; | ||||
|   memset(&config, 0, sizeof(config)); | ||||
|   config.flags.eot_level = value; | ||||
|   this->store_.times = 1; | ||||
|   this->store_.index = 0; | ||||
| #else | ||||
|   rmt_symbol_word_t symbol = { | ||||
|       .duration0 = 1, | ||||
|       .level0 = value, | ||||
| @@ -42,8 +93,8 @@ void RemoteTransmitterComponent::digital_write(bool value) { | ||||
|   }; | ||||
|   rmt_transmit_config_t config; | ||||
|   memset(&config, 0, sizeof(config)); | ||||
|   config.loop_count = 0; | ||||
|   config.flags.eot_level = value; | ||||
| #endif | ||||
|   esp_err_t error = rmt_transmit(this->channel_, this->encoder_, &symbol, sizeof(symbol), &config); | ||||
|   if (error != ESP_OK) { | ||||
|     ESP_LOGW(TAG, "rmt_transmit failed: %s", esp_err_to_name(error)); | ||||
| @@ -90,6 +141,20 @@ void RemoteTransmitterComponent::configure_rmt_() { | ||||
|       gpio_pullup_dis(gpio_num_t(this->pin_->get_pin())); | ||||
|     } | ||||
|  | ||||
| #if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 5, 1) | ||||
|     rmt_simple_encoder_config_t encoder; | ||||
|     memset(&encoder, 0, sizeof(encoder)); | ||||
|     encoder.callback = encoder_callback; | ||||
|     encoder.arg = &this->store_; | ||||
|     encoder.min_chunk_size = 1; | ||||
|     error = rmt_new_simple_encoder(&encoder, &this->encoder_); | ||||
|     if (error != ESP_OK) { | ||||
|       this->error_code_ = error; | ||||
|       this->error_string_ = "in rmt_new_simple_encoder"; | ||||
|       this->mark_failed(); | ||||
|       return; | ||||
|     } | ||||
| #else | ||||
|     rmt_copy_encoder_config_t encoder; | ||||
|     memset(&encoder, 0, sizeof(encoder)); | ||||
|     error = rmt_new_copy_encoder(&encoder, &this->encoder_); | ||||
| @@ -99,6 +164,7 @@ void RemoteTransmitterComponent::configure_rmt_() { | ||||
|       this->mark_failed(); | ||||
|       return; | ||||
|     } | ||||
| #endif | ||||
|  | ||||
|     error = rmt_enable(this->channel_); | ||||
|     if (error != ESP_OK) { | ||||
| @@ -130,6 +196,79 @@ void RemoteTransmitterComponent::configure_rmt_() { | ||||
|   } | ||||
| } | ||||
|  | ||||
| #if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 5, 1) | ||||
| void RemoteTransmitterComponent::send_internal(uint32_t send_times, uint32_t send_wait) { | ||||
|   if (this->is_failed()) { | ||||
|     return; | ||||
|   } | ||||
|  | ||||
|   if (this->current_carrier_frequency_ != this->temp_.get_carrier_frequency()) { | ||||
|     this->current_carrier_frequency_ = this->temp_.get_carrier_frequency(); | ||||
|     this->configure_rmt_(); | ||||
|   } | ||||
|  | ||||
|   this->rmt_temp_.clear(); | ||||
|   this->rmt_temp_.reserve(this->temp_.get_data().size() + 1); | ||||
|  | ||||
|   // encode any delay at the start of the buffer to simplify the encoder callback | ||||
|   // this will be skipped the first time around | ||||
|   send_wait = this->from_microseconds_(static_cast<uint32_t>(send_wait)); | ||||
|   while (send_wait > 0) { | ||||
|     int32_t duration = std::min(send_wait, uint32_t(RMT_SYMBOL_DURATION_MAX)); | ||||
|     this->rmt_temp_.push_back({ | ||||
|         .duration = static_cast<uint16_t>(duration), | ||||
|         .level = static_cast<uint16_t>(this->eot_level_), | ||||
|     }); | ||||
|     send_wait -= duration; | ||||
|   } | ||||
|  | ||||
|   // encode data | ||||
|   size_t offset = this->rmt_temp_.size(); | ||||
|   for (int32_t value : this->temp_.get_data()) { | ||||
|     bool level = value >= 0; | ||||
|     if (!level) { | ||||
|       value = -value; | ||||
|     } | ||||
|     value = this->from_microseconds_(static_cast<uint32_t>(value)); | ||||
|     while (value > 0) { | ||||
|       int32_t duration = std::min(value, int32_t(RMT_SYMBOL_DURATION_MAX)); | ||||
|       this->rmt_temp_.push_back({ | ||||
|           .duration = static_cast<uint16_t>(duration), | ||||
|           .level = static_cast<uint16_t>(level ^ this->inverted_), | ||||
|       }); | ||||
|       value -= duration; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   if ((this->rmt_temp_.data() == nullptr) || this->rmt_temp_.size() <= offset) { | ||||
|     ESP_LOGE(TAG, "Empty data"); | ||||
|     return; | ||||
|   } | ||||
|  | ||||
|   this->transmit_trigger_->trigger(); | ||||
|  | ||||
|   rmt_transmit_config_t config; | ||||
|   memset(&config, 0, sizeof(config)); | ||||
|   config.flags.eot_level = this->eot_level_; | ||||
|   this->store_.times = send_times; | ||||
|   this->store_.index = offset; | ||||
|   esp_err_t error = rmt_transmit(this->channel_, this->encoder_, this->rmt_temp_.data(), | ||||
|                                  this->rmt_temp_.size() * sizeof(rmt_symbol_half_t), &config); | ||||
|   if (error != ESP_OK) { | ||||
|     ESP_LOGW(TAG, "rmt_transmit failed: %s", esp_err_to_name(error)); | ||||
|     this->status_set_warning(); | ||||
|   } else { | ||||
|     this->status_clear_warning(); | ||||
|   } | ||||
|   error = rmt_tx_wait_all_done(this->channel_, -1); | ||||
|   if (error != ESP_OK) { | ||||
|     ESP_LOGW(TAG, "rmt_tx_wait_all_done failed: %s", esp_err_to_name(error)); | ||||
|     this->status_set_warning(); | ||||
|   } | ||||
|  | ||||
|   this->complete_trigger_->trigger(); | ||||
| } | ||||
| #else | ||||
| void RemoteTransmitterComponent::send_internal(uint32_t send_times, uint32_t send_wait) { | ||||
|   if (this->is_failed()) | ||||
|     return; | ||||
| @@ -151,7 +290,7 @@ void RemoteTransmitterComponent::send_internal(uint32_t send_times, uint32_t sen | ||||
|     val = this->from_microseconds_(static_cast<uint32_t>(val)); | ||||
|  | ||||
|     do { | ||||
|       int32_t item = std::min(val, int32_t(32767)); | ||||
|       int32_t item = std::min(val, int32_t(RMT_SYMBOL_DURATION_MAX)); | ||||
|       val -= item; | ||||
|  | ||||
|       if (rmt_i % 2 == 0) { | ||||
| @@ -180,7 +319,6 @@ void RemoteTransmitterComponent::send_internal(uint32_t send_times, uint32_t sen | ||||
|   for (uint32_t i = 0; i < send_times; i++) { | ||||
|     rmt_transmit_config_t config; | ||||
|     memset(&config, 0, sizeof(config)); | ||||
|     config.loop_count = 0; | ||||
|     config.flags.eot_level = this->eot_level_; | ||||
|     esp_err_t error = rmt_transmit(this->channel_, this->encoder_, this->rmt_temp_.data(), | ||||
|                                    this->rmt_temp_.size() * sizeof(rmt_symbol_word_t), &config); | ||||
| @@ -200,6 +338,7 @@ void RemoteTransmitterComponent::send_internal(uint32_t send_times, uint32_t sen | ||||
|   } | ||||
|   this->complete_trigger_->trigger(); | ||||
| } | ||||
| #endif | ||||
|  | ||||
| }  // namespace remote_transmitter | ||||
| }  // namespace esphome | ||||
|   | ||||
| @@ -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: | ||||
|   | ||||
| @@ -27,7 +27,7 @@ void SNTPComponent::setup() { | ||||
|   esp_sntp_setoperatingmode(ESP_SNTP_OPMODE_POLL); | ||||
|   size_t i = 0; | ||||
|   for (auto &server : this->servers_) { | ||||
|     esp_sntp_setservername(i++, server.c_str()); | ||||
|     esp_sntp_setservername(i++, server); | ||||
|   } | ||||
|   esp_sntp_set_sync_interval(this->get_update_interval()); | ||||
|   esp_sntp_set_time_sync_notification_cb([](struct timeval *tv) { | ||||
| @@ -42,7 +42,7 @@ void SNTPComponent::setup() { | ||||
|  | ||||
|   size_t i = 0; | ||||
|   for (auto &server : this->servers_) { | ||||
|     sntp_setservername(i++, server.c_str()); | ||||
|     sntp_setservername(i++, server); | ||||
|   } | ||||
|  | ||||
| #if defined(USE_ESP8266) | ||||
| @@ -59,7 +59,7 @@ void SNTPComponent::dump_config() { | ||||
|   ESP_LOGCONFIG(TAG, "SNTP Time:"); | ||||
|   size_t i = 0; | ||||
|   for (auto &server : this->servers_) { | ||||
|     ESP_LOGCONFIG(TAG, "  Server %zu: '%s'", i++, server.c_str()); | ||||
|     ESP_LOGCONFIG(TAG, "  Server %zu: '%s'", i++, server); | ||||
|   } | ||||
| } | ||||
| void SNTPComponent::update() { | ||||
|   | ||||
| @@ -2,10 +2,14 @@ | ||||
|  | ||||
| #include "esphome/core/component.h" | ||||
| #include "esphome/components/time/real_time_clock.h" | ||||
| #include <array> | ||||
|  | ||||
| namespace esphome { | ||||
| namespace sntp { | ||||
|  | ||||
| // Server count is calculated at compile time by Python codegen | ||||
| // SNTP_SERVER_COUNT will always be defined | ||||
|  | ||||
| /// The SNTP component allows you to configure local timekeeping via Simple Network Time Protocol. | ||||
| /// | ||||
| /// \note | ||||
| @@ -14,10 +18,7 @@ namespace sntp { | ||||
| /// \see https://www.gnu.org/software/libc/manual/html_node/TZ-Variable.html | ||||
| class SNTPComponent : public time::RealTimeClock { | ||||
|  public: | ||||
|   SNTPComponent(const std::vector<std::string> &servers) : servers_(servers) {} | ||||
|  | ||||
|   // Note: set_servers() has been removed and replaced by a constructor - calling set_servers after setup would | ||||
|   // have had no effect anyway, and making the strings immutable avoids the need to strdup their contents. | ||||
|   SNTPComponent(const std::array<const char *, SNTP_SERVER_COUNT> &servers) : servers_(servers) {} | ||||
|  | ||||
|   void setup() override; | ||||
|   void dump_config() override; | ||||
| @@ -29,7 +30,10 @@ class SNTPComponent : public time::RealTimeClock { | ||||
|   void time_synced(); | ||||
|  | ||||
|  protected: | ||||
|   std::vector<std::string> servers_; | ||||
|   // Store const char pointers to string literals | ||||
|   // ESP8266: strings in rodata (RAM), but avoids std::string overhead (~24 bytes each) | ||||
|   // Other platforms: strings in flash | ||||
|   std::array<const char *, SNTP_SERVER_COUNT> servers_; | ||||
|   bool has_time_{false}; | ||||
|  | ||||
| #if defined(USE_ESP32) | ||||
|   | ||||
| @@ -43,6 +43,11 @@ CONFIG_SCHEMA = cv.All( | ||||
|  | ||||
| async def to_code(config): | ||||
|     servers = config[CONF_SERVERS] | ||||
|  | ||||
|     # Define server count at compile time | ||||
|     cg.add_define("SNTP_SERVER_COUNT", len(servers)) | ||||
|  | ||||
|     # Pass string literals to constructor - stored in flash/rodata by compiler | ||||
|     var = cg.new_Pvariable(config[CONF_ID], servers) | ||||
|  | ||||
|     await cg.register_component(var, config) | ||||
|   | ||||
| @@ -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_; | ||||
| }; | ||||
|   | ||||
| @@ -73,11 +73,18 @@ async def to_code(config): | ||||
|         cg.add(var.set_template(template_)) | ||||
|  | ||||
|     else: | ||||
|         cg.add(var.set_optimistic(config[CONF_OPTIMISTIC])) | ||||
|         cg.add(var.set_initial_option(config[CONF_INITIAL_OPTION])) | ||||
|         # Only set if non-default to avoid bloating setup() function | ||||
|         if config[CONF_OPTIMISTIC]: | ||||
|             cg.add(var.set_optimistic(True)) | ||||
|         initial_option_index = config[CONF_OPTIONS].index(config[CONF_INITIAL_OPTION]) | ||||
|         # Only set if non-zero to avoid bloating setup() function | ||||
|         # (initial_option_index_ is zero-initialized in the header) | ||||
|         if initial_option_index != 0: | ||||
|             cg.add(var.set_initial_option_index(initial_option_index)) | ||||
|  | ||||
|         if CONF_RESTORE_VALUE in config: | ||||
|             cg.add(var.set_restore_value(config[CONF_RESTORE_VALUE])) | ||||
|         # Only set if True (default is False) | ||||
|         if config.get(CONF_RESTORE_VALUE): | ||||
|             cg.add(var.set_restore_value(True)) | ||||
|  | ||||
|     if CONF_SET_ACTION in config: | ||||
|         await automation.build_automation( | ||||
|   | ||||
| @@ -10,26 +10,21 @@ void TemplateSelect::setup() { | ||||
|   if (this->f_.has_value()) | ||||
|     return; | ||||
|  | ||||
|   std::string value; | ||||
|   if (!this->restore_value_) { | ||||
|     value = this->initial_option_; | ||||
|     ESP_LOGD(TAG, "State from initial: %s", value.c_str()); | ||||
|   } else { | ||||
|     size_t index; | ||||
|   size_t index = this->initial_option_index_; | ||||
|   if (this->restore_value_) { | ||||
|     this->pref_ = global_preferences->make_preference<size_t>(this->get_preference_hash()); | ||||
|     if (!this->pref_.load(&index)) { | ||||
|       value = this->initial_option_; | ||||
|       ESP_LOGD(TAG, "State from initial (could not load stored index): %s", value.c_str()); | ||||
|     } else if (!this->has_index(index)) { | ||||
|       value = this->initial_option_; | ||||
|       ESP_LOGD(TAG, "State from initial (restored index %zu out of bounds): %s", index, value.c_str()); | ||||
|     size_t restored_index; | ||||
|     if (this->pref_.load(&restored_index) && this->has_index(restored_index)) { | ||||
|       index = restored_index; | ||||
|       ESP_LOGD(TAG, "State from restore: %s", this->at(index).value().c_str()); | ||||
|     } else { | ||||
|       value = this->at(index).value(); | ||||
|       ESP_LOGD(TAG, "State from restore: %s", value.c_str()); | ||||
|       ESP_LOGD(TAG, "State from initial (could not load or invalid stored index): %s", this->at(index).value().c_str()); | ||||
|     } | ||||
|   } else { | ||||
|     ESP_LOGD(TAG, "State from initial: %s", this->at(index).value().c_str()); | ||||
|   } | ||||
|  | ||||
|   this->publish_state(value); | ||||
|   this->publish_state(this->at(index).value()); | ||||
| } | ||||
|  | ||||
| void TemplateSelect::update() { | ||||
| @@ -69,7 +64,8 @@ void TemplateSelect::dump_config() { | ||||
|                 "  Optimistic: %s\n" | ||||
|                 "  Initial Option: %s\n" | ||||
|                 "  Restore Value: %s", | ||||
|                 YESNO(this->optimistic_), this->initial_option_.c_str(), YESNO(this->restore_value_)); | ||||
|                 YESNO(this->optimistic_), this->at(this->initial_option_index_).value().c_str(), | ||||
|                 YESNO(this->restore_value_)); | ||||
| } | ||||
|  | ||||
| }  // namespace template_ | ||||
|   | ||||
| @@ -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; | ||||
| @@ -19,16 +19,16 @@ class TemplateSelect : public select::Select, public PollingComponent { | ||||
|  | ||||
|   Trigger<std::string> *get_set_trigger() const { return this->set_trigger_; } | ||||
|   void set_optimistic(bool optimistic) { this->optimistic_ = optimistic; } | ||||
|   void set_initial_option(const std::string &initial_option) { this->initial_option_ = initial_option; } | ||||
|   void set_initial_option_index(size_t initial_option_index) { this->initial_option_index_ = initial_option_index; } | ||||
|   void set_restore_value(bool restore_value) { this->restore_value_ = restore_value; } | ||||
|  | ||||
|  protected: | ||||
|   void control(const std::string &value) override; | ||||
|   bool optimistic_ = false; | ||||
|   std::string initial_option_; | ||||
|   size_t initial_option_index_{0}; | ||||
|   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: | ||||
|   | ||||
| @@ -67,7 +67,9 @@ void TuyaClimate::setup() { | ||||
|   } | ||||
|   if (this->eco_id_.has_value()) { | ||||
|     this->parent_->register_listener(*this->eco_id_, [this](const TuyaDatapoint &datapoint) { | ||||
|       // Whether data type is BOOL or ENUM, it will still be a 1 or a 0, so the functions below are valid in both cases | ||||
|       this->eco_ = datapoint.value_bool; | ||||
|       this->eco_type_ = datapoint.type; | ||||
|       ESP_LOGV(TAG, "MCU reported eco is: %s", ONOFF(this->eco_)); | ||||
|       this->compute_preset_(); | ||||
|       this->compute_target_temperature_(); | ||||
| @@ -176,7 +178,11 @@ void TuyaClimate::control(const climate::ClimateCall &call) { | ||||
|     if (this->eco_id_.has_value()) { | ||||
|       const bool eco = preset == climate::CLIMATE_PRESET_ECO; | ||||
|       ESP_LOGV(TAG, "Setting eco: %s", ONOFF(eco)); | ||||
|       this->parent_->set_boolean_datapoint_value(*this->eco_id_, eco); | ||||
|       if (this->eco_type_ == TuyaDatapointType::ENUM) { | ||||
|         this->parent_->set_enum_datapoint_value(*this->eco_id_, eco); | ||||
|       } else { | ||||
|         this->parent_->set_boolean_datapoint_value(*this->eco_id_, eco); | ||||
|       } | ||||
|     } | ||||
|     if (this->sleep_id_.has_value()) { | ||||
|       const bool sleep = preset == climate::CLIMATE_PRESET_SLEEP; | ||||
|   | ||||
| @@ -104,6 +104,7 @@ class TuyaClimate : public climate::Climate, public Component { | ||||
|   optional<uint8_t> eco_id_{}; | ||||
|   optional<uint8_t> sleep_id_{}; | ||||
|   optional<float> eco_temperature_{}; | ||||
|   TuyaDatapointType eco_type_{}; | ||||
|   uint8_t active_state_; | ||||
|   uint8_t fan_state_; | ||||
|   optional<uint8_t> swing_vertical_id_{}; | ||||
|   | ||||
| @@ -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) {} | ||||
|   | ||||
| @@ -87,6 +87,7 @@ | ||||
| #define USE_MDNS_STORE_SERVICES | ||||
| #define MDNS_SERVICE_COUNT 3 | ||||
| #define MDNS_DYNAMIC_TXT_COUNT 3 | ||||
| #define SNTP_SERVER_COUNT 3 | ||||
| #define USE_MEDIA_PLAYER | ||||
| #define USE_NEXTION_TFT_UPLOAD | ||||
| #define USE_NUMBER | ||||
| @@ -122,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 | ||||
|   | ||||
| @@ -46,24 +46,18 @@ struct tm ESPTime::to_c_tm() { | ||||
|   return c_tm; | ||||
| } | ||||
|  | ||||
| std::string ESPTime::strftime(const std::string &format) { | ||||
|   std::string timestr; | ||||
|   timestr.resize(format.size() * 4); | ||||
| std::string ESPTime::strftime(const char *format) { | ||||
|   struct tm c_tm = this->to_c_tm(); | ||||
|   size_t len = ::strftime(×tr[0], timestr.size(), format.c_str(), &c_tm); | ||||
|   while (len == 0) { | ||||
|     if (timestr.size() >= 128) { | ||||
|       // strftime has failed for reasons unrelated to the size of the buffer | ||||
|       // so return a formatting error | ||||
|       return "ERROR"; | ||||
|     } | ||||
|     timestr.resize(timestr.size() * 2); | ||||
|     len = ::strftime(×tr[0], timestr.size(), format.c_str(), &c_tm); | ||||
|   char buf[128]; | ||||
|   size_t len = ::strftime(buf, sizeof(buf), format, &c_tm); | ||||
|   if (len > 0) { | ||||
|     return std::string(buf, len); | ||||
|   } | ||||
|   timestr.resize(len); | ||||
|   return timestr; | ||||
|   return "ERROR"; | ||||
| } | ||||
|  | ||||
| std::string ESPTime::strftime(const std::string &format) { return this->strftime(format.c_str()); } | ||||
|  | ||||
| bool ESPTime::strptime(const std::string &time_to_parse, ESPTime &esp_time) { | ||||
|   uint16_t year; | ||||
|   uint8_t month; | ||||
|   | ||||
| @@ -44,17 +44,19 @@ struct ESPTime { | ||||
|   size_t strftime(char *buffer, size_t buffer_len, const char *format); | ||||
|  | ||||
|   /** Convert this ESPTime struct to a string as specified by the format argument. | ||||
|    * @see https://www.gnu.org/software/libc/manual/html_node/Formatting-Calendar-Time.html#index-strftime | ||||
|    * @see https://en.cppreference.com/w/c/chrono/strftime | ||||
|    * | ||||
|    * @warning This method uses dynamically allocated strings which can cause heap fragmentation with some | ||||
|    * @warning This method returns a dynamically allocated string which can cause heap fragmentation with some | ||||
|    * microcontrollers. | ||||
|    * | ||||
|    * @warning This method can return "ERROR" when the underlying strftime() call fails, e.g. when the | ||||
|    * format string contains unsupported specifiers or when the format string doesn't produce any | ||||
|    * output. | ||||
|    * @warning This method can return "ERROR" when the underlying strftime() call fails or when the | ||||
|    * output exceeds 128 bytes. | ||||
|    */ | ||||
|   std::string strftime(const std::string &format); | ||||
|  | ||||
|   /// @copydoc strftime(const std::string &format) | ||||
|   std::string strftime(const char *format); | ||||
|  | ||||
|   /// Check if this ESPTime is valid (all fields in range and year is greater than 2018) | ||||
|   bool is_valid() const { return this->year >= 2019 && this->fields_in_range(); } | ||||
|  | ||||
|   | ||||
| @@ -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") | ||||
|   | ||||
| @@ -120,7 +120,7 @@ def prepare( | ||||
|                 cert_file.flush() | ||||
|                 key_file.write(config[CONF_MQTT].get(CONF_CLIENT_CERTIFICATE_KEY)) | ||||
|                 key_file.flush() | ||||
|                 context.load_cert_chain(cert_file, key_file) | ||||
|                 context.load_cert_chain(cert_file.name, key_file.name) | ||||
|         client.tls_set_context(context) | ||||
|  | ||||
|     try: | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -41,6 +41,17 @@ select: | ||||
|       - ""  # Empty string at the end | ||||
|     initial_option: "Choice X" | ||||
|  | ||||
|   - platform: template | ||||
|     name: "Select Initial Option Test" | ||||
|     id: select_initial_option_test | ||||
|     optimistic: true | ||||
|     options: | ||||
|       - "First" | ||||
|       - "Second" | ||||
|       - "Third" | ||||
|       - "Fourth" | ||||
|     initial_option: "Third"  # Test non-default initial option | ||||
|  | ||||
| # Add a sensor to ensure we have other entities in the list | ||||
| sensor: | ||||
|   - platform: template | ||||
|   | ||||
| @@ -36,8 +36,8 @@ async def test_host_mode_empty_string_options( | ||||
|  | ||||
|         # Find our select entities | ||||
|         select_entities = [e for e in entity_info if isinstance(e, SelectInfo)] | ||||
|         assert len(select_entities) == 3, ( | ||||
|             f"Expected 3 select entities, got {len(select_entities)}" | ||||
|         assert len(select_entities) == 4, ( | ||||
|             f"Expected 4 select entities, got {len(select_entities)}" | ||||
|         ) | ||||
|  | ||||
|         # Verify each select entity by name and check their options | ||||
| @@ -71,6 +71,15 @@ async def test_host_mode_empty_string_options( | ||||
|         assert empty_last.options[2] == "Choice Z" | ||||
|         assert empty_last.options[3] == ""  # Empty string at end | ||||
|  | ||||
|         # Check "Select Initial Option Test" - verify non-default initial option | ||||
|         assert "Select Initial Option Test" in selects_by_name | ||||
|         initial_option_test = selects_by_name["Select Initial Option Test"] | ||||
|         assert len(initial_option_test.options) == 4 | ||||
|         assert initial_option_test.options[0] == "First" | ||||
|         assert initial_option_test.options[1] == "Second" | ||||
|         assert initial_option_test.options[2] == "Third" | ||||
|         assert initial_option_test.options[3] == "Fourth" | ||||
|  | ||||
|         # If we got here without protobuf decoding errors, the fix is working | ||||
|         # The bug would have caused "Invalid protobuf message" errors with trailing bytes | ||||
|  | ||||
| @@ -78,7 +87,12 @@ async def test_host_mode_empty_string_options( | ||||
|         # This ensures empty strings work properly in state messages too | ||||
|         states: dict[int, EntityState] = {} | ||||
|         states_received_future: asyncio.Future[None] = loop.create_future() | ||||
|         expected_select_keys = {empty_first.key, empty_middle.key, empty_last.key} | ||||
|         expected_select_keys = { | ||||
|             empty_first.key, | ||||
|             empty_middle.key, | ||||
|             empty_last.key, | ||||
|             initial_option_test.key, | ||||
|         } | ||||
|         received_select_keys = set() | ||||
|  | ||||
|         def on_state(state: EntityState) -> None: | ||||
| @@ -109,6 +123,14 @@ async def test_host_mode_empty_string_options( | ||||
|         assert empty_first.key in states | ||||
|         assert empty_middle.key in states | ||||
|         assert empty_last.key in states | ||||
|         assert initial_option_test.key in states | ||||
|  | ||||
|         # Verify the initial option is set correctly to "Third" (not the default "First") | ||||
|         initial_state = states[initial_option_test.key] | ||||
|         assert initial_state.state == "Third", ( | ||||
|             f"Expected initial state 'Third' but got '{initial_state.state}' - " | ||||
|             f"initial_option not correctly applied" | ||||
|         ) | ||||
|  | ||||
|         # The main test is that we got here without protobuf errors | ||||
|         # The select entities with empty string options were properly encoded | ||||
|   | ||||
| @@ -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