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