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,
|
CONF_UPDATE_INTERVAL,
|
||||||
)
|
)
|
||||||
from esphome.core import ID
|
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.schema_extractors import SCHEMA_EXTRACT, schema_extractor
|
||||||
from esphome.types import ConfigType
|
from esphome.types import ConfigType
|
||||||
from esphome.util import Registry
|
from esphome.util import Registry
|
||||||
@@ -87,6 +92,7 @@ def validate_potentially_or_condition(value):
|
|||||||
|
|
||||||
DelayAction = cg.esphome_ns.class_("DelayAction", Action, cg.Component)
|
DelayAction = cg.esphome_ns.class_("DelayAction", Action, cg.Component)
|
||||||
LambdaAction = cg.esphome_ns.class_("LambdaAction", Action)
|
LambdaAction = cg.esphome_ns.class_("LambdaAction", Action)
|
||||||
|
StatelessLambdaAction = cg.esphome_ns.class_("StatelessLambdaAction", Action)
|
||||||
IfAction = cg.esphome_ns.class_("IfAction", Action)
|
IfAction = cg.esphome_ns.class_("IfAction", Action)
|
||||||
WhileAction = cg.esphome_ns.class_("WhileAction", Action)
|
WhileAction = cg.esphome_ns.class_("WhileAction", Action)
|
||||||
RepeatAction = cg.esphome_ns.class_("RepeatAction", 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")
|
Automation = cg.esphome_ns.class_("Automation")
|
||||||
|
|
||||||
LambdaCondition = cg.esphome_ns.class_("LambdaCondition", Condition)
|
LambdaCondition = cg.esphome_ns.class_("LambdaCondition", Condition)
|
||||||
|
StatelessLambdaCondition = cg.esphome_ns.class_("StatelessLambdaCondition", Condition)
|
||||||
ForCondition = cg.esphome_ns.class_("ForCondition", Condition, cg.Component)
|
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):
|
def validate_automation(extra_schema=None, extra_validators=None, single=False):
|
||||||
if extra_schema is None:
|
if extra_schema is None:
|
||||||
extra_schema = {}
|
extra_schema = {}
|
||||||
@@ -240,7 +277,9 @@ async def lambda_condition_to_code(
|
|||||||
args: TemplateArgsType,
|
args: TemplateArgsType,
|
||||||
) -> MockObj:
|
) -> MockObj:
|
||||||
lambda_ = await cg.process_lambda(config, args, return_type=bool)
|
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(
|
@register_condition(
|
||||||
@@ -406,7 +445,7 @@ async def lambda_action_to_code(
|
|||||||
args: TemplateArgsType,
|
args: TemplateArgsType,
|
||||||
) -> MockObj:
|
) -> MockObj:
|
||||||
lambda_ = await cg.process_lambda(config, args, return_type=cg.void)
|
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(
|
@register_action(
|
||||||
|
|||||||
@@ -62,6 +62,7 @@ from esphome.cpp_types import ( # noqa: F401
|
|||||||
EntityBase,
|
EntityBase,
|
||||||
EntityCategory,
|
EntityCategory,
|
||||||
ESPTime,
|
ESPTime,
|
||||||
|
FixedVector,
|
||||||
GPIOPin,
|
GPIOPin,
|
||||||
InternalGPIOPin,
|
InternalGPIOPin,
|
||||||
JsonObject,
|
JsonObject,
|
||||||
|
|||||||
@@ -71,10 +71,12 @@ SERVICE_ARG_NATIVE_TYPES = {
|
|||||||
"int": cg.int32,
|
"int": cg.int32,
|
||||||
"float": float,
|
"float": float,
|
||||||
"string": cg.std_string,
|
"string": cg.std_string,
|
||||||
"bool[]": cg.std_vector.template(bool),
|
"bool[]": cg.FixedVector.template(bool).operator("const").operator("ref"),
|
||||||
"int[]": cg.std_vector.template(cg.int32),
|
"int[]": cg.FixedVector.template(cg.int32).operator("const").operator("ref"),
|
||||||
"float[]": cg.std_vector.template(float),
|
"float[]": cg.FixedVector.template(float).operator("const").operator("ref"),
|
||||||
"string[]": cg.std_vector.template(cg.std_string),
|
"string[]": cg.FixedVector.template(cg.std_string)
|
||||||
|
.operator("const")
|
||||||
|
.operator("ref"),
|
||||||
}
|
}
|
||||||
CONF_ENCRYPTION = "encryption"
|
CONF_ENCRYPTION = "encryption"
|
||||||
CONF_BATCH_DELAY = "batch_delay"
|
CONF_BATCH_DELAY = "batch_delay"
|
||||||
@@ -258,6 +260,10 @@ async def to_code(config):
|
|||||||
if config.get(CONF_ACTIONS) or config[CONF_CUSTOM_SERVICES]:
|
if config.get(CONF_ACTIONS) or config[CONF_CUSTOM_SERVICES]:
|
||||||
cg.add_define("USE_API_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]:
|
if config[CONF_HOMEASSISTANT_SERVICES]:
|
||||||
cg.add_define("USE_API_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")
|
cg.add_define("USE_API_HOMEASSISTANT_STATES")
|
||||||
|
|
||||||
if actions := config.get(CONF_ACTIONS, []):
|
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:
|
for conf in actions:
|
||||||
template_args = []
|
template_args = []
|
||||||
func_args = []
|
func_args = []
|
||||||
@@ -278,8 +286,10 @@ async def to_code(config):
|
|||||||
trigger = cg.new_Pvariable(
|
trigger = cg.new_Pvariable(
|
||||||
conf[CONF_TRIGGER_ID], templ, conf[CONF_ACTION], service_arg_names
|
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)
|
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:
|
if CONF_ON_CLIENT_CONNECTED in config:
|
||||||
cg.add_define("USE_API_CLIENT_CONNECTED_TRIGGER")
|
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_ACTION_RESPONSES
|
||||||
#endif // USE_API_HOMEASSISTANT_SERVICES
|
#endif // USE_API_HOMEASSISTANT_SERVICES
|
||||||
#ifdef USE_API_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); }
|
void register_user_service(UserServiceDescriptor *descriptor) { this->user_services_.push_back(descriptor); }
|
||||||
#endif
|
#endif
|
||||||
|
#endif
|
||||||
#ifdef USE_HOMEASSISTANT_TIME
|
#ifdef USE_HOMEASSISTANT_TIME
|
||||||
void request_time();
|
void request_time();
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -53,8 +53,14 @@ class CustomAPIDevice {
|
|||||||
template<typename T, typename... Ts>
|
template<typename T, typename... Ts>
|
||||||
void register_service(void (T::*callback)(Ts...), const std::string &name,
|
void register_service(void (T::*callback)(Ts...), const std::string &name,
|
||||||
const std::array<std::string, sizeof...(Ts)> &arg_names) {
|
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
|
auto *service = new CustomAPIDeviceService<T, Ts...>(name, arg_names, (T *) this, callback); // NOLINT
|
||||||
global_api_server->register_user_service(service);
|
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
|
#else
|
||||||
template<typename T, typename... Ts>
|
template<typename T, typename... Ts>
|
||||||
@@ -86,8 +92,14 @@ class CustomAPIDevice {
|
|||||||
*/
|
*/
|
||||||
#ifdef USE_API_SERVICES
|
#ifdef USE_API_SERVICES
|
||||||
template<typename T> void register_service(void (T::*callback)(), const std::string &name) {
|
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
|
auto *service = new CustomAPIDeviceService<T>(name, {}, (T *) this, callback); // NOLINT
|
||||||
global_api_server->register_user_service(service);
|
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
|
#else
|
||||||
template<typename T> void register_service(void (T::*callback)(), const std::string &name) {
|
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<> 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_; }
|
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) {
|
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) {
|
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) {
|
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) {
|
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<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<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<float>() { return enums::SERVICE_ARG_TYPE_FLOAT; }
|
||||||
template<> enums::ServiceArgType to_service_arg_type<std::string>() { return enums::SERVICE_ARG_TYPE_STRING; }
|
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<bool>>() { return enums::SERVICE_ARG_TYPE_BOOL_ARRAY; }
|
||||||
template<> enums::ServiceArgType to_service_arg_type<std::vector<int32_t>>() {
|
template<> enums::ServiceArgType to_service_arg_type<std::vector<int32_t>>() {
|
||||||
return enums::SERVICE_ARG_TYPE_INT_ARRAY;
|
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;
|
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
|
} // namespace esphome::api
|
||||||
|
|||||||
@@ -155,6 +155,7 @@ DelayedOffFilter = binary_sensor_ns.class_("DelayedOffFilter", Filter, cg.Compon
|
|||||||
InvertFilter = binary_sensor_ns.class_("InvertFilter", Filter)
|
InvertFilter = binary_sensor_ns.class_("InvertFilter", Filter)
|
||||||
AutorepeatFilter = binary_sensor_ns.class_("AutorepeatFilter", Filter, cg.Component)
|
AutorepeatFilter = binary_sensor_ns.class_("AutorepeatFilter", Filter, cg.Component)
|
||||||
LambdaFilter = binary_sensor_ns.class_("LambdaFilter", Filter)
|
LambdaFilter = binary_sensor_ns.class_("LambdaFilter", Filter)
|
||||||
|
StatelessLambdaFilter = binary_sensor_ns.class_("StatelessLambdaFilter", Filter)
|
||||||
SettleFilter = binary_sensor_ns.class_("SettleFilter", Filter, cg.Component)
|
SettleFilter = binary_sensor_ns.class_("SettleFilter", Filter, cg.Component)
|
||||||
|
|
||||||
_LOGGER = getLogger(__name__)
|
_LOGGER = getLogger(__name__)
|
||||||
@@ -299,7 +300,7 @@ async def lambda_filter_to_code(config, filter_id):
|
|||||||
lambda_ = await cg.process_lambda(
|
lambda_ = await cg.process_lambda(
|
||||||
config, [(bool, "x")], return_type=cg.optional.template(bool)
|
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(
|
@register_filter(
|
||||||
|
|||||||
@@ -111,6 +111,21 @@ class LambdaFilter : public Filter {
|
|||||||
std::function<optional<bool>(bool)> f_;
|
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 {
|
class SettleFilter : public Filter, public Component {
|
||||||
public:
|
public:
|
||||||
optional<bool> new_value(bool value) override;
|
optional<bool> new_value(bool value) override;
|
||||||
|
|||||||
@@ -40,13 +40,13 @@ class ESP32InternalGPIOPin : public InternalGPIOPin {
|
|||||||
// - 3 bytes for members below
|
// - 3 bytes for members below
|
||||||
// - 1 byte padding for alignment
|
// - 1 byte padding for alignment
|
||||||
// - 4 bytes for vtable pointer
|
// - 4 bytes for vtable pointer
|
||||||
uint8_t pin_; // GPIO pin number (0-255, actual max ~54 on ESP32)
|
uint8_t pin_; // GPIO pin number (0-255, actual max ~54 on ESP32)
|
||||||
gpio::Flags flags_; // GPIO flags (1 byte)
|
gpio::Flags flags_{}; // GPIO flags (1 byte)
|
||||||
struct PinFlags {
|
struct PinFlags {
|
||||||
uint8_t inverted : 1; // Invert pin logic (1 bit)
|
uint8_t inverted : 1; // Invert pin logic (1 bit)
|
||||||
uint8_t drive_strength : 2; // Drive strength 0-3 (2 bits)
|
uint8_t drive_strength : 2; // Drive strength 0-3 (2 bits)
|
||||||
uint8_t reserved : 5; // Reserved for future use (5 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)
|
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
|
||||||
static bool isr_service_installed;
|
static bool isr_service_installed;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -223,7 +223,10 @@ async def esp32_pin_to_code(config):
|
|||||||
var = cg.new_Pvariable(config[CONF_ID])
|
var = cg.new_Pvariable(config[CONF_ID])
|
||||||
num = config[CONF_NUMBER]
|
num = config[CONF_NUMBER]
|
||||||
cg.add(var.set_pin(getattr(gpio_num_t, f"GPIO_NUM_{num}")))
|
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:
|
if CONF_DRIVE_STRENGTH in config:
|
||||||
cg.add(var.set_drive_strength(config[CONF_DRIVE_STRENGTH]))
|
cg.add(var.set_drive_strength(config[CONF_DRIVE_STRENGTH]))
|
||||||
cg.add(var.set_flags(pins.gpio_flags_expr(config[CONF_MODE])))
|
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;
|
void attach_interrupt(void (*func)(void *), void *arg, gpio::InterruptType type) const override;
|
||||||
|
|
||||||
uint8_t pin_;
|
uint8_t pin_;
|
||||||
bool inverted_;
|
bool inverted_{};
|
||||||
gpio::Flags flags_;
|
gpio::Flags flags_{};
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace esp8266
|
} // namespace esp8266
|
||||||
|
|||||||
@@ -165,7 +165,10 @@ async def esp8266_pin_to_code(config):
|
|||||||
num = config[CONF_NUMBER]
|
num = config[CONF_NUMBER]
|
||||||
mode = config[CONF_MODE]
|
mode = config[CONF_MODE]
|
||||||
cg.add(var.set_pin(num))
|
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)))
|
cg.add(var.set_flags(pins.gpio_flags_expr(mode)))
|
||||||
if num < 16:
|
if num < 16:
|
||||||
initial_state: PinInitialState = CORE.data[KEY_ESP8266][KEY_PIN_INITIAL_STATES][
|
initial_state: PinInitialState = CORE.data[KEY_ESP8266][KEY_PIN_INITIAL_STATES][
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ from esphome.components.esp32.const import (
|
|||||||
VARIANT_ESP32S2,
|
VARIANT_ESP32S2,
|
||||||
VARIANT_ESP32S3,
|
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
|
from esphome.components.spi import CONF_INTERFACE_INDEX, get_spi_interface
|
||||||
import esphome.config_validation as cv
|
import esphome.config_validation as cv
|
||||||
from esphome.const import (
|
from esphome.const import (
|
||||||
@@ -320,11 +320,11 @@ def _final_validate_spi(config):
|
|||||||
def manual_ip(config):
|
def manual_ip(config):
|
||||||
return cg.StructInitializer(
|
return cg.StructInitializer(
|
||||||
ManualIP,
|
ManualIP,
|
||||||
("static_ip", IPAddress(str(config[CONF_STATIC_IP]))),
|
("static_ip", ip_address_literal(config[CONF_STATIC_IP])),
|
||||||
("gateway", IPAddress(str(config[CONF_GATEWAY]))),
|
("gateway", ip_address_literal(config[CONF_GATEWAY])),
|
||||||
("subnet", IPAddress(str(config[CONF_SUBNET]))),
|
("subnet", ip_address_literal(config[CONF_SUBNET])),
|
||||||
("dns1", IPAddress(str(config[CONF_DNS1]))),
|
("dns1", ip_address_literal(config[CONF_DNS1])),
|
||||||
("dns2", IPAddress(str(config[CONF_DNS2]))),
|
("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;
|
void attach_interrupt(void (*func)(void *), void *arg, gpio::InterruptType type) const override;
|
||||||
|
|
||||||
uint8_t pin_;
|
uint8_t pin_;
|
||||||
bool inverted_;
|
bool inverted_{};
|
||||||
gpio::Flags flags_;
|
gpio::Flags flags_{};
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace host
|
} // namespace host
|
||||||
|
|||||||
@@ -57,6 +57,9 @@ async def host_pin_to_code(config):
|
|||||||
var = cg.new_Pvariable(config[CONF_ID])
|
var = cg.new_Pvariable(config[CONF_ID])
|
||||||
num = config[CONF_NUMBER]
|
num = config[CONF_NUMBER]
|
||||||
cg.add(var.set_pin(num))
|
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])))
|
cg.add(var.set_flags(pins.gpio_flags_expr(config[CONF_MODE])))
|
||||||
return var
|
return var
|
||||||
|
|||||||
@@ -199,6 +199,9 @@ async def component_pin_to_code(config):
|
|||||||
var = cg.new_Pvariable(config[CONF_ID])
|
var = cg.new_Pvariable(config[CONF_ID])
|
||||||
num = config[CONF_NUMBER]
|
num = config[CONF_NUMBER]
|
||||||
cg.add(var.set_pin(num))
|
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])))
|
cg.add(var.set_flags(pins.gpio_flags_expr(config[CONF_MODE])))
|
||||||
return var
|
return var
|
||||||
|
|||||||
@@ -27,8 +27,8 @@ class ArduinoInternalGPIOPin : public InternalGPIOPin {
|
|||||||
void attach_interrupt(void (*func)(void *), void *arg, gpio::InterruptType type) const override;
|
void attach_interrupt(void (*func)(void *), void *arg, gpio::InterruptType type) const override;
|
||||||
|
|
||||||
uint8_t pin_;
|
uint8_t pin_;
|
||||||
bool inverted_;
|
bool inverted_{};
|
||||||
gpio::Flags flags_;
|
gpio::Flags flags_{};
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace libretiny
|
} // namespace libretiny
|
||||||
|
|||||||
@@ -57,9 +57,9 @@ class AddressableLightEffect : public LightEffect {
|
|||||||
|
|
||||||
class AddressableLambdaLightEffect : public AddressableLightEffect {
|
class AddressableLambdaLightEffect : public AddressableLightEffect {
|
||||||
public:
|
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)
|
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 start() override { this->initial_run_ = true; }
|
||||||
void apply(AddressableLight &it, const Color ¤t_color) override {
|
void apply(AddressableLight &it, const Color ¤t_color) override {
|
||||||
const uint32_t now = millis();
|
const uint32_t now = millis();
|
||||||
@@ -72,7 +72,7 @@ class AddressableLambdaLightEffect : public AddressableLightEffect {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
std::function<void(AddressableLight &, Color, bool initial_run)> f_;
|
void (*f_)(AddressableLight &, Color, bool initial_run);
|
||||||
uint32_t update_interval_;
|
uint32_t update_interval_;
|
||||||
uint32_t last_run_{0};
|
uint32_t last_run_{0};
|
||||||
bool initial_run_;
|
bool initial_run_;
|
||||||
|
|||||||
@@ -112,8 +112,8 @@ class RandomLightEffect : public LightEffect {
|
|||||||
|
|
||||||
class LambdaLightEffect : public LightEffect {
|
class LambdaLightEffect : public LightEffect {
|
||||||
public:
|
public:
|
||||||
LambdaLightEffect(const char *name, std::function<void(bool initial_run)> f, uint32_t update_interval)
|
LambdaLightEffect(const char *name, void (*f)(bool initial_run), uint32_t update_interval)
|
||||||
: LightEffect(name), f_(std::move(f)), update_interval_(update_interval) {}
|
: LightEffect(name), f_(f), update_interval_(update_interval) {}
|
||||||
|
|
||||||
void start() override { this->initial_run_ = true; }
|
void start() override { this->initial_run_ = true; }
|
||||||
void apply() override {
|
void apply() override {
|
||||||
@@ -130,7 +130,7 @@ class LambdaLightEffect : public LightEffect {
|
|||||||
uint32_t get_current_index() const { return this->get_index(); }
|
uint32_t get_current_index() const { return this->get_index(); }
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
std::function<void(bool initial_run)> f_;
|
void (*f_)(bool initial_run);
|
||||||
uint32_t update_interval_;
|
uint32_t update_interval_;
|
||||||
uint32_t last_run_{0};
|
uint32_t last_run_{0};
|
||||||
bool initial_run_;
|
bool initial_run_;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import re
|
import re
|
||||||
|
|
||||||
from esphome import automation
|
from esphome import automation
|
||||||
from esphome.automation import LambdaAction
|
from esphome.automation import LambdaAction, StatelessLambdaAction
|
||||||
import esphome.codegen as cg
|
import esphome.codegen as cg
|
||||||
from esphome.components.esp32 import add_idf_sdkconfig_option, get_esp32_variant
|
from esphome.components.esp32 import add_idf_sdkconfig_option, get_esp32_variant
|
||||||
from esphome.components.esp32.const import (
|
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_)))
|
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)
|
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(
|
@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)))
|
text = str(cg.statement(logger.set_log_level(level)))
|
||||||
|
|
||||||
lambda_ = await cg.process_lambda(Lambda(text), args, return_type=cg.void)
|
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(
|
FILTER_SOURCE_FILES = filter_source_files_from_platform(
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import ipaddress
|
||||||
|
|
||||||
import esphome.codegen as cg
|
import esphome.codegen as cg
|
||||||
from esphome.components.esp32 import add_idf_sdkconfig_option
|
from esphome.components.esp32 import add_idf_sdkconfig_option
|
||||||
import esphome.config_validation as cv
|
import esphome.config_validation as cv
|
||||||
@@ -10,6 +12,41 @@ AUTO_LOAD = ["mdns"]
|
|||||||
network_ns = cg.esphome_ns.namespace("network")
|
network_ns = cg.esphome_ns.namespace("network")
|
||||||
IPAddress = network_ns.class_("IPAddress")
|
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(
|
CONFIG_SCHEMA = cv.Schema(
|
||||||
{
|
{
|
||||||
cv.SplitDefault(
|
cv.SplitDefault(
|
||||||
|
|||||||
@@ -74,6 +74,9 @@ async def nrf52_pin_to_code(config):
|
|||||||
var = cg.new_Pvariable(config[CONF_ID])
|
var = cg.new_Pvariable(config[CONF_ID])
|
||||||
num = config[CONF_NUMBER]
|
num = config[CONF_NUMBER]
|
||||||
cg.add(var.set_pin(num))
|
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])))
|
cg.add(var.set_flags(pins.gpio_flags_expr(config[CONF_MODE])))
|
||||||
return var
|
return var
|
||||||
|
|||||||
@@ -29,8 +29,8 @@ class RP2040GPIOPin : public InternalGPIOPin {
|
|||||||
void attach_interrupt(void (*func)(void *), void *arg, gpio::InterruptType type) const override;
|
void attach_interrupt(void (*func)(void *), void *arg, gpio::InterruptType type) const override;
|
||||||
|
|
||||||
uint8_t pin_;
|
uint8_t pin_;
|
||||||
bool inverted_;
|
bool inverted_{};
|
||||||
gpio::Flags flags_;
|
gpio::Flags flags_{};
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace rp2040
|
} // namespace rp2040
|
||||||
|
|||||||
@@ -94,6 +94,9 @@ async def rp2040_pin_to_code(config):
|
|||||||
var = cg.new_Pvariable(config[CONF_ID])
|
var = cg.new_Pvariable(config[CONF_ID])
|
||||||
num = config[CONF_NUMBER]
|
num = config[CONF_NUMBER]
|
||||||
cg.add(var.set_pin(num))
|
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])))
|
cg.add(var.set_flags(pins.gpio_flags_expr(config[CONF_MODE])))
|
||||||
return var
|
return var
|
||||||
|
|||||||
@@ -261,6 +261,7 @@ ExponentialMovingAverageFilter = sensor_ns.class_(
|
|||||||
)
|
)
|
||||||
ThrottleAverageFilter = sensor_ns.class_("ThrottleAverageFilter", Filter, cg.Component)
|
ThrottleAverageFilter = sensor_ns.class_("ThrottleAverageFilter", Filter, cg.Component)
|
||||||
LambdaFilter = sensor_ns.class_("LambdaFilter", Filter)
|
LambdaFilter = sensor_ns.class_("LambdaFilter", Filter)
|
||||||
|
StatelessLambdaFilter = sensor_ns.class_("StatelessLambdaFilter", Filter)
|
||||||
OffsetFilter = sensor_ns.class_("OffsetFilter", Filter)
|
OffsetFilter = sensor_ns.class_("OffsetFilter", Filter)
|
||||||
MultiplyFilter = sensor_ns.class_("MultiplyFilter", Filter)
|
MultiplyFilter = sensor_ns.class_("MultiplyFilter", Filter)
|
||||||
ValueListFilter = sensor_ns.class_("ValueListFilter", 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(
|
lambda_ = await cg.process_lambda(
|
||||||
config, [(float, "x")], return_type=cg.optional.template(float)
|
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(
|
DELTA_SCHEMA = cv.Schema(
|
||||||
|
|||||||
@@ -296,6 +296,21 @@ class LambdaFilter : public Filter {
|
|||||||
lambda_filter_t lambda_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.
|
/// A simple filter that adds `offset` to each value it receives.
|
||||||
class OffsetFilter : public Filter {
|
class OffsetFilter : public Filter {
|
||||||
public:
|
public:
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
import logging
|
import logging
|
||||||
|
from re import Match
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
from esphome import core
|
from esphome import core
|
||||||
from esphome.config_helpers import Extend, Remove, merge_config, merge_dicts_ordered
|
from esphome.config_helpers import Extend, Remove, merge_config, merge_dicts_ordered
|
||||||
@@ -39,7 +41,34 @@ async def to_code(config):
|
|||||||
pass
|
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 has_jinja(value):
|
||||||
# If the original value passed in to this function is a JinjaStr, it means it contains an unresolved
|
# If the original value passed in to this function is a JinjaStr, it means it contains an unresolved
|
||||||
# Jinja expression from a previous pass.
|
# 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)}",
|
f"\nSee {'->'.join(str(x) for x in path)}",
|
||||||
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
|
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:
|
if "$" not in value:
|
||||||
return value
|
return value
|
||||||
|
|
||||||
@@ -76,14 +112,14 @@ def _expand_substitutions(substitutions, value, path, jinja, ignore_missing):
|
|||||||
|
|
||||||
i = 0
|
i = 0
|
||||||
while True:
|
while True:
|
||||||
m = cv.VARIABLE_PROG.search(value, i)
|
m: Match[str] = cv.VARIABLE_PROG.search(value, i)
|
||||||
if not m:
|
if not m:
|
||||||
# No more variable substitutions found. See if the remainder looks like a jinja template
|
# No more variable substitutions found. See if the remainder looks like a jinja template
|
||||||
value = _expand_jinja(value, orig_value, path, jinja, ignore_missing)
|
value = _expand_jinja(value, orig_value, path, jinja, ignore_missing)
|
||||||
break
|
break
|
||||||
|
|
||||||
i, j = m.span(0)
|
i, j = m.span(0)
|
||||||
name = m.group(1)
|
name: str = m.group(1)
|
||||||
if name.startswith("{") and name.endswith("}"):
|
if name.startswith("{") and name.endswith("}"):
|
||||||
name = name[1:-1]
|
name = name[1:-1]
|
||||||
if name not in substitutions:
|
if name not in substitutions:
|
||||||
@@ -98,7 +134,7 @@ def _expand_substitutions(substitutions, value, path, jinja, ignore_missing):
|
|||||||
i = j
|
i = j
|
||||||
continue
|
continue
|
||||||
|
|
||||||
sub = substitutions[name]
|
sub: Any = substitutions[name]
|
||||||
|
|
||||||
if i == 0 and j == len(value):
|
if i == 0 and j == len(value):
|
||||||
# The variable spans the whole expression, e.g., "${varName}". Return its resolved value directly
|
# 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
|
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):
|
if isinstance(item, ESPLiteralValue):
|
||||||
return None # do not substitute inside literal blocks
|
return None # do not substitute inside literal blocks
|
||||||
if isinstance(item, list):
|
if isinstance(item, list):
|
||||||
@@ -160,7 +202,9 @@ def _substitute_item(substitutions, item, path, jinja, ignore_missing):
|
|||||||
return None
|
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:
|
if CONF_SUBSTITUTIONS not in config and not command_line_substitutions:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,14 @@
|
|||||||
from ast import literal_eval
|
from ast import literal_eval
|
||||||
|
from collections.abc import Iterator
|
||||||
|
from itertools import chain, islice
|
||||||
import logging
|
import logging
|
||||||
import math
|
import math
|
||||||
import re
|
import re
|
||||||
|
from types import GeneratorType
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
import jinja2 as jinja
|
import jinja2 as jinja
|
||||||
from jinja2.sandbox import SandboxedEnvironment
|
from jinja2.nativetypes import NativeCodeGenerator, NativeTemplate
|
||||||
|
|
||||||
from esphome.yaml_util import ESPLiteralValue
|
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
|
return detect_jinja_re.search(st) is not None
|
||||||
|
|
||||||
|
|
||||||
@@ -109,12 +113,56 @@ class TrackerContext(jinja.runtime.Context):
|
|||||||
return val
|
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
|
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__(
|
super().__init__(
|
||||||
trim_blocks=True,
|
trim_blocks=True,
|
||||||
lstrip_blocks=True,
|
lstrip_blocks=True,
|
||||||
@@ -142,19 +190,10 @@ class Jinja(SandboxedEnvironment):
|
|||||||
**SAFE_GLOBALS,
|
**SAFE_GLOBALS,
|
||||||
}
|
}
|
||||||
|
|
||||||
def safe_eval(self, expr):
|
def expand(self, content_str: str | JinjaStr) -> Any:
|
||||||
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):
|
|
||||||
"""
|
"""
|
||||||
Renders a string that may contain Jinja expressions or statements
|
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
|
Otherwise, it returns a tagged (JinjaStr) string that captures variables
|
||||||
in scope (upvalues), like a closure for later evaluation.
|
in scope (upvalues), like a closure for later evaluation.
|
||||||
"""
|
"""
|
||||||
@@ -172,7 +211,7 @@ class Jinja(SandboxedEnvironment):
|
|||||||
self.context_trace = {}
|
self.context_trace = {}
|
||||||
try:
|
try:
|
||||||
template = self.from_string(content_str)
|
template = self.from_string(content_str)
|
||||||
result = self.safe_eval(template.render(override_vars))
|
result = template.render(override_vars)
|
||||||
if isinstance(result, Undefined):
|
if isinstance(result, Undefined):
|
||||||
print("" + result) # force a UndefinedError exception
|
print("" + result) # force a UndefinedError exception
|
||||||
except (TemplateSyntaxError, UndefinedError) as err:
|
except (TemplateSyntaxError, UndefinedError) as err:
|
||||||
@@ -201,3 +240,10 @@ class Jinja(SandboxedEnvironment):
|
|||||||
content_str.result = result
|
content_str.result = result
|
||||||
|
|
||||||
return result, None
|
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 = await automation.build_condition(
|
||||||
condition, cg.TemplateArguments(), []
|
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(
|
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_))
|
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::setup() { this->loop(); }
|
||||||
|
|
||||||
void TemplateBinarySensor::loop() {
|
void TemplateBinarySensor::loop() {
|
||||||
if (this->f_ == nullptr)
|
if (!this->f_.has_value())
|
||||||
return;
|
return;
|
||||||
|
|
||||||
auto s = this->f_();
|
auto s = (*this->f_)();
|
||||||
if (s.has_value()) {
|
if (s.has_value()) {
|
||||||
this->publish_state(*s);
|
this->publish_state(*s);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ namespace template_ {
|
|||||||
|
|
||||||
class TemplateBinarySensor : public Component, public binary_sensor::BinarySensor {
|
class TemplateBinarySensor : public Component, public binary_sensor::BinarySensor {
|
||||||
public:
|
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 setup() override;
|
||||||
void loop() 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; }
|
float get_setup_priority() const override { return setup_priority::HARDWARE; }
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
std::function<optional<bool>()> f_{nullptr};
|
optional<optional<bool> (*)()> f_;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace template_
|
} // namespace template_
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ void TemplateCover::loop() {
|
|||||||
}
|
}
|
||||||
void TemplateCover::set_optimistic(bool optimistic) { this->optimistic_ = optimistic; }
|
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_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; }
|
float TemplateCover::get_setup_priority() const { return setup_priority::HARDWARE; }
|
||||||
Trigger<> *TemplateCover::get_open_trigger() const { return this->open_trigger_; }
|
Trigger<> *TemplateCover::get_open_trigger() const { return this->open_trigger_; }
|
||||||
Trigger<> *TemplateCover::get_close_trigger() const { return this->close_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_position_trigger() const { return this->position_trigger_; }
|
||||||
Trigger<float> *TemplateCover::get_tilt_trigger() const { return this->tilt_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_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_toggle(bool has_toggle) { this->has_toggle_ = has_toggle; }
|
||||||
void TemplateCover::set_has_position(bool has_position) { this->has_position_ = has_position; }
|
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:
|
public:
|
||||||
TemplateCover();
|
TemplateCover();
|
||||||
|
|
||||||
void set_state_lambda(std::function<optional<float>()> &&f);
|
void set_state_lambda(optional<float> (*f)());
|
||||||
Trigger<> *get_open_trigger() const;
|
Trigger<> *get_open_trigger() const;
|
||||||
Trigger<> *get_close_trigger() const;
|
Trigger<> *get_close_trigger() const;
|
||||||
Trigger<> *get_stop_trigger() const;
|
Trigger<> *get_stop_trigger() const;
|
||||||
@@ -26,7 +26,7 @@ class TemplateCover : public cover::Cover, public Component {
|
|||||||
Trigger<float> *get_tilt_trigger() const;
|
Trigger<float> *get_tilt_trigger() const;
|
||||||
void set_optimistic(bool optimistic);
|
void set_optimistic(bool optimistic);
|
||||||
void set_assumed_state(bool assumed_state);
|
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_stop(bool has_stop);
|
||||||
void set_has_position(bool has_position);
|
void set_has_position(bool has_position);
|
||||||
void set_has_tilt(bool has_tilt);
|
void set_has_tilt(bool has_tilt);
|
||||||
@@ -45,8 +45,8 @@ class TemplateCover : public cover::Cover, public Component {
|
|||||||
void stop_prev_trigger_();
|
void stop_prev_trigger_();
|
||||||
|
|
||||||
TemplateCoverRestoreMode restore_mode_{COVER_RESTORE};
|
TemplateCoverRestoreMode restore_mode_{COVER_RESTORE};
|
||||||
optional<std::function<optional<float>()>> state_f_;
|
optional<optional<float> (*)()> state_f_;
|
||||||
optional<std::function<optional<float>()>> tilt_f_;
|
optional<optional<float> (*)()> tilt_f_;
|
||||||
bool assumed_state_{false};
|
bool assumed_state_{false};
|
||||||
bool optimistic_{false};
|
bool optimistic_{false};
|
||||||
Trigger<> *open_trigger_;
|
Trigger<> *open_trigger_;
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ namespace template_ {
|
|||||||
|
|
||||||
class TemplateDate : public datetime::DateEntity, public PollingComponent {
|
class TemplateDate : public datetime::DateEntity, public PollingComponent {
|
||||||
public:
|
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 setup() override;
|
||||||
void update() override;
|
void update() override;
|
||||||
@@ -35,7 +35,7 @@ class TemplateDate : public datetime::DateEntity, public PollingComponent {
|
|||||||
ESPTime initial_value_{};
|
ESPTime initial_value_{};
|
||||||
bool restore_value_{false};
|
bool restore_value_{false};
|
||||||
Trigger<ESPTime> *set_trigger_ = new Trigger<ESPTime>();
|
Trigger<ESPTime> *set_trigger_ = new Trigger<ESPTime>();
|
||||||
optional<std::function<optional<ESPTime>()>> f_;
|
optional<optional<ESPTime> (*)()> f_;
|
||||||
|
|
||||||
ESPPreferenceObject pref_;
|
ESPPreferenceObject pref_;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ namespace template_ {
|
|||||||
|
|
||||||
class TemplateDateTime : public datetime::DateTimeEntity, public PollingComponent {
|
class TemplateDateTime : public datetime::DateTimeEntity, public PollingComponent {
|
||||||
public:
|
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 setup() override;
|
||||||
void update() override;
|
void update() override;
|
||||||
@@ -35,7 +35,7 @@ class TemplateDateTime : public datetime::DateTimeEntity, public PollingComponen
|
|||||||
ESPTime initial_value_{};
|
ESPTime initial_value_{};
|
||||||
bool restore_value_{false};
|
bool restore_value_{false};
|
||||||
Trigger<ESPTime> *set_trigger_ = new Trigger<ESPTime>();
|
Trigger<ESPTime> *set_trigger_ = new Trigger<ESPTime>();
|
||||||
optional<std::function<optional<ESPTime>()>> f_;
|
optional<optional<ESPTime> (*)()> f_;
|
||||||
|
|
||||||
ESPPreferenceObject pref_;
|
ESPPreferenceObject pref_;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ namespace template_ {
|
|||||||
|
|
||||||
class TemplateTime : public datetime::TimeEntity, public PollingComponent {
|
class TemplateTime : public datetime::TimeEntity, public PollingComponent {
|
||||||
public:
|
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 setup() override;
|
||||||
void update() override;
|
void update() override;
|
||||||
@@ -35,7 +35,7 @@ class TemplateTime : public datetime::TimeEntity, public PollingComponent {
|
|||||||
ESPTime initial_value_{};
|
ESPTime initial_value_{};
|
||||||
bool restore_value_{false};
|
bool restore_value_{false};
|
||||||
Trigger<ESPTime> *set_trigger_ = new Trigger<ESPTime>();
|
Trigger<ESPTime> *set_trigger_ = new Trigger<ESPTime>();
|
||||||
optional<std::function<optional<ESPTime>()>> f_;
|
optional<optional<ESPTime> (*)()> f_;
|
||||||
|
|
||||||
ESPPreferenceObject pref_;
|
ESPPreferenceObject pref_;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ void TemplateLock::open_latch() {
|
|||||||
this->open_trigger_->trigger();
|
this->open_trigger_->trigger();
|
||||||
}
|
}
|
||||||
void TemplateLock::set_optimistic(bool optimistic) { this->optimistic_ = optimistic; }
|
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; }
|
float TemplateLock::get_setup_priority() const { return setup_priority::HARDWARE; }
|
||||||
Trigger<> *TemplateLock::get_lock_trigger() const { return this->lock_trigger_; }
|
Trigger<> *TemplateLock::get_lock_trigger() const { return this->lock_trigger_; }
|
||||||
Trigger<> *TemplateLock::get_unlock_trigger() const { return this->unlock_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 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_lock_trigger() const;
|
||||||
Trigger<> *get_unlock_trigger() const;
|
Trigger<> *get_unlock_trigger() const;
|
||||||
Trigger<> *get_open_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 control(const lock::LockCall &call) override;
|
||||||
void open_latch() override;
|
void open_latch() override;
|
||||||
|
|
||||||
optional<std::function<optional<lock::LockState>()>> f_;
|
optional<optional<lock::LockState> (*)()> f_;
|
||||||
bool optimistic_{false};
|
bool optimistic_{false};
|
||||||
Trigger<> *lock_trigger_;
|
Trigger<> *lock_trigger_;
|
||||||
Trigger<> *unlock_trigger_;
|
Trigger<> *unlock_trigger_;
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ namespace template_ {
|
|||||||
|
|
||||||
class TemplateNumber : public number::Number, public PollingComponent {
|
class TemplateNumber : public number::Number, public PollingComponent {
|
||||||
public:
|
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 setup() override;
|
||||||
void update() override;
|
void update() override;
|
||||||
@@ -28,7 +28,7 @@ class TemplateNumber : public number::Number, public PollingComponent {
|
|||||||
float initial_value_{NAN};
|
float initial_value_{NAN};
|
||||||
bool restore_value_{false};
|
bool restore_value_{false};
|
||||||
Trigger<float> *set_trigger_ = new Trigger<float>();
|
Trigger<float> *set_trigger_ = new Trigger<float>();
|
||||||
optional<std::function<optional<float>()>> f_;
|
optional<optional<float> (*)()> f_;
|
||||||
|
|
||||||
ESPPreferenceObject pref_;
|
ESPPreferenceObject pref_;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ namespace template_ {
|
|||||||
|
|
||||||
class TemplateSelect : public select::Select, public PollingComponent {
|
class TemplateSelect : public select::Select, public PollingComponent {
|
||||||
public:
|
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 setup() override;
|
||||||
void update() override;
|
void update() override;
|
||||||
@@ -28,7 +28,7 @@ class TemplateSelect : public select::Select, public PollingComponent {
|
|||||||
std::string initial_option_;
|
std::string initial_option_;
|
||||||
bool restore_value_ = false;
|
bool restore_value_ = false;
|
||||||
Trigger<std::string> *set_trigger_ = new Trigger<std::string>();
|
Trigger<std::string> *set_trigger_ = new Trigger<std::string>();
|
||||||
optional<std::function<optional<std::string>()>> f_;
|
optional<optional<std::string> (*)()> f_;
|
||||||
|
|
||||||
ESPPreferenceObject pref_;
|
ESPPreferenceObject pref_;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ void TemplateSensor::update() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
float TemplateSensor::get_setup_priority() const { return setup_priority::HARDWARE; }
|
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() {
|
void TemplateSensor::dump_config() {
|
||||||
LOG_SENSOR("", "Template Sensor", this);
|
LOG_SENSOR("", "Template Sensor", this);
|
||||||
LOG_UPDATE_INTERVAL(this);
|
LOG_UPDATE_INTERVAL(this);
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ namespace template_ {
|
|||||||
|
|
||||||
class TemplateSensor : public sensor::Sensor, public PollingComponent {
|
class TemplateSensor : public sensor::Sensor, public PollingComponent {
|
||||||
public:
|
public:
|
||||||
void set_template(std::function<optional<float>()> &&f);
|
void set_template(optional<float> (*f)());
|
||||||
|
|
||||||
void update() override;
|
void update() override;
|
||||||
|
|
||||||
@@ -17,7 +17,7 @@ class TemplateSensor : public sensor::Sensor, public PollingComponent {
|
|||||||
float get_setup_priority() const override;
|
float get_setup_priority() const override;
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
optional<std::function<optional<float>()>> f_;
|
optional<optional<float> (*)()> f_;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace template_
|
} // namespace template_
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ void TemplateSwitch::write_state(bool state) {
|
|||||||
}
|
}
|
||||||
void TemplateSwitch::set_optimistic(bool optimistic) { this->optimistic_ = optimistic; }
|
void TemplateSwitch::set_optimistic(bool optimistic) { this->optimistic_ = optimistic; }
|
||||||
bool TemplateSwitch::assumed_state() { return this->assumed_state_; }
|
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; }
|
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_on_trigger() const { return this->turn_on_trigger_; }
|
||||||
Trigger<> *TemplateSwitch::get_turn_off_trigger() const { return this->turn_off_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 setup() override;
|
||||||
void dump_config() 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_on_trigger() const;
|
||||||
Trigger<> *get_turn_off_trigger() const;
|
Trigger<> *get_turn_off_trigger() const;
|
||||||
void set_optimistic(bool optimistic);
|
void set_optimistic(bool optimistic);
|
||||||
@@ -28,7 +28,7 @@ class TemplateSwitch : public switch_::Switch, public Component {
|
|||||||
|
|
||||||
void write_state(bool state) override;
|
void write_state(bool state) override;
|
||||||
|
|
||||||
optional<std::function<optional<bool>()>> f_;
|
optional<optional<bool> (*)()> f_;
|
||||||
bool optimistic_{false};
|
bool optimistic_{false};
|
||||||
bool assumed_state_{false};
|
bool assumed_state_{false};
|
||||||
Trigger<> *turn_on_trigger_;
|
Trigger<> *turn_on_trigger_;
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ template<uint8_t SZ> class TextSaver : public TemplateTextSaverBase {
|
|||||||
|
|
||||||
class TemplateText : public text::Text, public PollingComponent {
|
class TemplateText : public text::Text, public PollingComponent {
|
||||||
public:
|
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 setup() override;
|
||||||
void update() override;
|
void update() override;
|
||||||
@@ -78,7 +78,7 @@ class TemplateText : public text::Text, public PollingComponent {
|
|||||||
bool optimistic_ = false;
|
bool optimistic_ = false;
|
||||||
std::string initial_value_;
|
std::string initial_value_;
|
||||||
Trigger<std::string> *set_trigger_ = new Trigger<std::string>();
|
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;
|
TemplateTextSaverBase *pref_ = nullptr;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ void TemplateTextSensor::update() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
float TemplateTextSensor::get_setup_priority() const { return setup_priority::HARDWARE; }
|
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); }
|
void TemplateTextSensor::dump_config() { LOG_TEXT_SENSOR("", "Template Sensor", this); }
|
||||||
|
|
||||||
} // namespace template_
|
} // namespace template_
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ namespace template_ {
|
|||||||
|
|
||||||
class TemplateTextSensor : public text_sensor::TextSensor, public PollingComponent {
|
class TemplateTextSensor : public text_sensor::TextSensor, public PollingComponent {
|
||||||
public:
|
public:
|
||||||
void set_template(std::function<optional<std::string>()> &&f);
|
void set_template(optional<std::string> (*f)());
|
||||||
|
|
||||||
void update() override;
|
void update() override;
|
||||||
|
|
||||||
@@ -18,7 +18,7 @@ class TemplateTextSensor : public text_sensor::TextSensor, public PollingCompone
|
|||||||
void dump_config() override;
|
void dump_config() override;
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
optional<std::function<optional<std::string>()>> f_{};
|
optional<optional<std::string> (*)()> f_{};
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace template_
|
} // namespace template_
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ void TemplateValve::loop() {
|
|||||||
|
|
||||||
void TemplateValve::set_optimistic(bool optimistic) { this->optimistic_ = optimistic; }
|
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_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; }
|
float TemplateValve::get_setup_priority() const { return setup_priority::HARDWARE; }
|
||||||
|
|
||||||
Trigger<> *TemplateValve::get_open_trigger() const { return this->open_trigger_; }
|
Trigger<> *TemplateValve::get_open_trigger() const { return this->open_trigger_; }
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ class TemplateValve : public valve::Valve, public Component {
|
|||||||
public:
|
public:
|
||||||
TemplateValve();
|
TemplateValve();
|
||||||
|
|
||||||
void set_state_lambda(std::function<optional<float>()> &&f);
|
void set_state_lambda(optional<float> (*f)());
|
||||||
Trigger<> *get_open_trigger() const;
|
Trigger<> *get_open_trigger() const;
|
||||||
Trigger<> *get_close_trigger() const;
|
Trigger<> *get_close_trigger() const;
|
||||||
Trigger<> *get_stop_trigger() const;
|
Trigger<> *get_stop_trigger() const;
|
||||||
@@ -42,7 +42,7 @@ class TemplateValve : public valve::Valve, public Component {
|
|||||||
void stop_prev_trigger_();
|
void stop_prev_trigger_();
|
||||||
|
|
||||||
TemplateValveRestoreMode restore_mode_{VALVE_NO_RESTORE};
|
TemplateValveRestoreMode restore_mode_{VALVE_NO_RESTORE};
|
||||||
optional<std::function<optional<float>()>> state_f_;
|
optional<optional<float> (*)()> state_f_;
|
||||||
bool assumed_state_{false};
|
bool assumed_state_{false};
|
||||||
bool optimistic_{false};
|
bool optimistic_{false};
|
||||||
Trigger<> *open_trigger_;
|
Trigger<> *open_trigger_;
|
||||||
|
|||||||
@@ -57,6 +57,7 @@ validate_filters = cv.validate_registry("filter", FILTER_REGISTRY)
|
|||||||
# Filters
|
# Filters
|
||||||
Filter = text_sensor_ns.class_("Filter")
|
Filter = text_sensor_ns.class_("Filter")
|
||||||
LambdaFilter = text_sensor_ns.class_("LambdaFilter", Filter)
|
LambdaFilter = text_sensor_ns.class_("LambdaFilter", Filter)
|
||||||
|
StatelessLambdaFilter = text_sensor_ns.class_("StatelessLambdaFilter", Filter)
|
||||||
ToUpperFilter = text_sensor_ns.class_("ToUpperFilter", Filter)
|
ToUpperFilter = text_sensor_ns.class_("ToUpperFilter", Filter)
|
||||||
ToLowerFilter = text_sensor_ns.class_("ToLowerFilter", Filter)
|
ToLowerFilter = text_sensor_ns.class_("ToLowerFilter", Filter)
|
||||||
AppendFilter = text_sensor_ns.class_("AppendFilter", 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(
|
lambda_ = await cg.process_lambda(
|
||||||
config, [(cg.std_string, "x")], return_type=cg.optional.template(cg.std_string)
|
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, {})
|
@FILTER_REGISTRY.register("to_upper", ToUpperFilter, {})
|
||||||
|
|||||||
@@ -62,6 +62,21 @@ class LambdaFilter : public Filter {
|
|||||||
lambda_filter_t lambda_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
|
/// A simple filter that converts all text to uppercase
|
||||||
class ToUpperFilter : public Filter {
|
class ToUpperFilter : public Filter {
|
||||||
public:
|
public:
|
||||||
|
|||||||
@@ -99,10 +99,26 @@ void IDFUARTComponent::setup() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void IDFUARTComponent::load_settings(bool dump_config) {
|
void IDFUARTComponent::load_settings(bool dump_config) {
|
||||||
uart_config_t uart_config = this->get_config_();
|
esp_err_t err;
|
||||||
esp_err_t err = uart_param_config(this->uart_num_, &uart_config);
|
|
||||||
|
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) {
|
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();
|
this->mark_failed();
|
||||||
return;
|
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;
|
int8_t flow_control = this->flow_control_pin_ != nullptr ? this->flow_control_pin_->get_pin() : -1;
|
||||||
|
|
||||||
uint32_t invert = 0;
|
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;
|
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;
|
invert |= UART_SIGNAL_RXD_INV;
|
||||||
|
}
|
||||||
|
|
||||||
err = uart_set_line_inverse(this->uart_num_, invert);
|
err = uart_set_line_inverse(this->uart_num_, invert);
|
||||||
if (err != ESP_OK) {
|
if (err != ESP_OK) {
|
||||||
@@ -138,26 +156,6 @@ void IDFUARTComponent::load_settings(bool dump_config) {
|
|||||||
return;
|
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_);
|
err = uart_set_rx_full_threshold(this->uart_num_, this->rx_full_threshold_);
|
||||||
if (err != ESP_OK) {
|
if (err != ESP_OK) {
|
||||||
ESP_LOGW(TAG, "uart_set_rx_full_threshold failed: %s", esp_err_to_name(err));
|
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;
|
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) {
|
if (err != ESP_OK) {
|
||||||
ESP_LOGW(TAG, "uart_set_mode failed: %s", esp_err_to_name(err));
|
ESP_LOGW(TAG, "uart_set_mode failed: %s", esp_err_to_name(err));
|
||||||
this->mark_failed();
|
this->mark_failed();
|
||||||
return;
|
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) {
|
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();
|
this->dump_config();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void IDFUARTComponent::dump_config() {
|
void IDFUARTComponent::dump_config() {
|
||||||
ESP_LOGCONFIG(TAG, "UART Bus %u:", this->uart_num_);
|
ESP_LOGCONFIG(TAG, "UART Bus %u:", this->uart_num_);
|
||||||
LOG_PIN(" TX Pin: ", tx_pin_);
|
LOG_PIN(" TX Pin: ", this->tx_pin_);
|
||||||
LOG_PIN(" RX Pin: ", rx_pin_);
|
LOG_PIN(" RX Pin: ", this->rx_pin_);
|
||||||
LOG_PIN(" Flow Control Pin: ", flow_control_pin_);
|
LOG_PIN(" Flow Control Pin: ", this->flow_control_pin_);
|
||||||
if (this->rx_pin_ != nullptr) {
|
if (this->rx_pin_ != nullptr) {
|
||||||
ESP_LOGCONFIG(TAG,
|
ESP_LOGCONFIG(TAG,
|
||||||
" RX Buffer Size: %u\n"
|
" RX Buffer Size: %u\n"
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ from esphome.automation import Condition
|
|||||||
import esphome.codegen as cg
|
import esphome.codegen as cg
|
||||||
from esphome.components.const import CONF_USE_PSRAM
|
from esphome.components.const import CONF_USE_PSRAM
|
||||||
from esphome.components.esp32 import add_idf_sdkconfig_option, const, get_esp32_variant
|
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
|
from esphome.config_helpers import filter_source_files_from_platform
|
||||||
import esphome.config_validation as cv
|
import esphome.config_validation as cv
|
||||||
from esphome.config_validation import only_with_esp_idf
|
from esphome.config_validation import only_with_esp_idf
|
||||||
@@ -334,9 +334,7 @@ def eap_auth(config):
|
|||||||
|
|
||||||
|
|
||||||
def safe_ip(ip):
|
def safe_ip(ip):
|
||||||
if ip is None:
|
return ip_address_literal(ip)
|
||||||
return IPAddress(0, 0, 0, 0)
|
|
||||||
return IPAddress(str(ip))
|
|
||||||
|
|
||||||
|
|
||||||
def manual_ip(config):
|
def manual_ip(config):
|
||||||
|
|||||||
@@ -26,10 +26,10 @@ class ZephyrGPIOPin : public InternalGPIOPin {
|
|||||||
protected:
|
protected:
|
||||||
void attach_interrupt(void (*func)(void *), void *arg, gpio::InterruptType type) const override;
|
void attach_interrupt(void (*func)(void *), void *arg, gpio::InterruptType type) const override;
|
||||||
uint8_t pin_;
|
uint8_t pin_;
|
||||||
bool inverted_;
|
bool inverted_{};
|
||||||
gpio::Flags flags_;
|
gpio::Flags flags_{};
|
||||||
const device *gpio_ = nullptr;
|
const device *gpio_{nullptr};
|
||||||
bool value_ = false;
|
bool value_{false};
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace zephyr
|
} // namespace zephyr
|
||||||
|
|||||||
@@ -4,6 +4,8 @@
|
|||||||
#include "esphome/core/defines.h"
|
#include "esphome/core/defines.h"
|
||||||
#include "esphome/core/helpers.h"
|
#include "esphome/core/helpers.h"
|
||||||
#include "esphome/core/preferences.h"
|
#include "esphome/core/preferences.h"
|
||||||
|
#include <concepts>
|
||||||
|
#include <functional>
|
||||||
#include <utility>
|
#include <utility>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
@@ -27,11 +29,20 @@ template<typename T, typename... X> class TemplatableValue {
|
|||||||
public:
|
public:
|
||||||
TemplatableValue() : type_(NONE) {}
|
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));
|
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));
|
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_);
|
new (&this->value_) T(other.value_);
|
||||||
} else if (type_ == LAMBDA) {
|
} else if (type_ == LAMBDA) {
|
||||||
this->f_ = new std::function<T(X...)>(*other.f_);
|
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) {
|
} else if (type_ == LAMBDA) {
|
||||||
this->f_ = other.f_;
|
this->f_ = other.f_;
|
||||||
other.f_ = nullptr;
|
other.f_ = nullptr;
|
||||||
|
} else if (type_ == STATELESS_LAMBDA) {
|
||||||
|
this->stateless_f_ = other.stateless_f_;
|
||||||
}
|
}
|
||||||
other.type_ = NONE;
|
other.type_ = NONE;
|
||||||
}
|
}
|
||||||
@@ -78,16 +93,23 @@ template<typename T, typename... X> class TemplatableValue {
|
|||||||
} else if (type_ == LAMBDA) {
|
} else if (type_ == LAMBDA) {
|
||||||
delete this->f_;
|
delete this->f_;
|
||||||
}
|
}
|
||||||
|
// STATELESS_LAMBDA/NONE: no cleanup needed (function pointer or empty, not heap-allocated)
|
||||||
}
|
}
|
||||||
|
|
||||||
bool has_value() { return this->type_ != NONE; }
|
bool has_value() { return this->type_ != NONE; }
|
||||||
|
|
||||||
T value(X... x) {
|
T value(X... x) {
|
||||||
if (this->type_ == LAMBDA) {
|
switch (this->type_) {
|
||||||
return (*this->f_)(x...);
|
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) {
|
optional<T> optional_value(X... x) {
|
||||||
@@ -109,11 +131,13 @@ template<typename T, typename... X> class TemplatableValue {
|
|||||||
NONE,
|
NONE,
|
||||||
VALUE,
|
VALUE,
|
||||||
LAMBDA,
|
LAMBDA,
|
||||||
|
STATELESS_LAMBDA,
|
||||||
} type_;
|
} type_;
|
||||||
|
|
||||||
union {
|
union {
|
||||||
T value_;
|
T value_;
|
||||||
std::function<T(X...)> *f_;
|
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_;
|
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 {
|
template<typename... Ts> class ForCondition : public Condition<Ts...>, public Component {
|
||||||
public:
|
public:
|
||||||
explicit ForCondition(Condition<> *condition) : condition_(condition) {}
|
explicit ForCondition(Condition<> *condition) : condition_(condition) {}
|
||||||
@@ -190,6 +202,19 @@ template<typename... Ts> class LambdaAction : public Action<Ts...> {
|
|||||||
std::function<void(Ts...)> f_;
|
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...> {
|
template<typename... Ts> class IfAction : public Action<Ts...> {
|
||||||
public:
|
public:
|
||||||
explicit IfAction(Condition<Ts...> *condition) : condition_(condition) {}
|
explicit IfAction(Condition<Ts...> *condition) : condition_(condition) {}
|
||||||
|
|||||||
@@ -123,6 +123,7 @@
|
|||||||
#define USE_API_NOISE
|
#define USE_API_NOISE
|
||||||
#define USE_API_PLAINTEXT
|
#define USE_API_PLAINTEXT
|
||||||
#define USE_API_SERVICES
|
#define USE_API_SERVICES
|
||||||
|
#define USE_API_CUSTOM_SERVICES
|
||||||
#define API_MAX_SEND_QUEUE 8
|
#define API_MAX_SEND_QUEUE 8
|
||||||
#define USE_MD5
|
#define USE_MD5
|
||||||
#define USE_SHA256
|
#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
|
self.return_type = safe_exp(return_type) if return_type is not None else None
|
||||||
|
|
||||||
def __str__(self):
|
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})"
|
cpp = f"[{self.capture}]({self.parameters})"
|
||||||
if self.return_type is not None:
|
if self.return_type is not None:
|
||||||
cpp += f" -> {self.return_type}"
|
cpp += f" -> {self.return_type}"
|
||||||
@@ -700,6 +702,12 @@ async def process_lambda(
|
|||||||
parts[i * 3 + 1] = var
|
parts[i * 3 + 1] = var
|
||||||
parts[i * 3 + 2] = ""
|
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:
|
if isinstance(value, ESPHomeDataBase) and value.esp_range is not None:
|
||||||
location = value.esp_range.start_mark
|
location = value.esp_range.start_mark
|
||||||
location.line += value.content_offset
|
location.line += value.content_offset
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ size_t = global_ns.namespace("size_t")
|
|||||||
const_char_ptr = global_ns.namespace("const char *")
|
const_char_ptr = global_ns.namespace("const char *")
|
||||||
NAN = global_ns.namespace("NAN")
|
NAN = global_ns.namespace("NAN")
|
||||||
esphome_ns = global_ns # using namespace esphome;
|
esphome_ns = global_ns # using namespace esphome;
|
||||||
|
FixedVector = esphome_ns.class_("FixedVector")
|
||||||
App = esphome_ns.App
|
App = esphome_ns.App
|
||||||
EntityBase = esphome_ns.class_("EntityBase")
|
EntityBase = esphome_ns.class_("EntityBase")
|
||||||
Component = esphome_ns.class_("Component")
|
Component = esphome_ns.class_("Component")
|
||||||
|
|||||||
@@ -19,7 +19,9 @@ classifiers = [
|
|||||||
"Programming Language :: Python :: 3",
|
"Programming Language :: Python :: 3",
|
||||||
"Topic :: Home Automation",
|
"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"]
|
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):
|
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
|
# 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")
|
main_cpp = generate_main("tests/component_tests/text/test_text.yaml")
|
||||||
|
|
||||||
# Then
|
# 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
|
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:
|
class TestLiterals:
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import glob
|
import glob
|
||||||
import logging
|
import logging
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
from esphome import config as config_module, yaml_util
|
from esphome import config as config_module, yaml_util
|
||||||
from esphome.components import substitutions
|
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")
|
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):
|
def test_substitutions_fixtures(fixture_path):
|
||||||
base_dir = fixture_path / "substitutions"
|
base_dir = fixture_path / "substitutions"
|
||||||
sources = sorted(glob.glob(str(base_dir / "*.input.yaml")))
|
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)
|
substitutions.do_substitution_pass(config, None)
|
||||||
|
|
||||||
resolve_extend_remove(config)
|
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
|
# Also load expected using ESPHome's loader, or use {} if missing and DEV_MODE
|
||||||
if expected_path.is_file():
|
if expected_path.is_file():
|
||||||
|
|||||||
Reference in New Issue
Block a user