1
0
mirror of https://github.com/esphome/esphome.git synced 2025-11-01 15:41:52 +00:00

Merge branch 'dev' into ota_handle_data_cleanups

This commit is contained in:
J. Nick Koston
2025-10-28 23:44:33 -05:00
committed by GitHub
168 changed files with 2015 additions and 723 deletions

View File

@@ -416,7 +416,7 @@ jobs:
} }
// Generate review messages // Generate review messages
function generateReviewMessages(finalLabels) { function generateReviewMessages(finalLabels, originalLabelCount) {
const messages = []; const messages = [];
const prAuthor = context.payload.pull_request.user.login; const prAuthor = context.payload.pull_request.user.login;
@@ -430,15 +430,15 @@ jobs:
.reduce((sum, file) => sum + (file.deletions || 0), 0); .reduce((sum, file) => sum + (file.deletions || 0), 0);
const nonTestChanges = (totalAdditions - testAdditions) - (totalDeletions - testDeletions); const nonTestChanges = (totalAdditions - testAdditions) - (totalDeletions - testDeletions);
const tooManyLabels = finalLabels.length > MAX_LABELS; const tooManyLabels = originalLabelCount > MAX_LABELS;
const tooManyChanges = nonTestChanges > TOO_BIG_THRESHOLD; const tooManyChanges = nonTestChanges > TOO_BIG_THRESHOLD;
let message = `${TOO_BIG_MARKER}\n### 📦 Pull Request Size\n\n`; let message = `${TOO_BIG_MARKER}\n### 📦 Pull Request Size\n\n`;
if (tooManyLabels && tooManyChanges) { if (tooManyLabels && tooManyChanges) {
message += `This PR is too large with ${nonTestChanges} line changes (excluding tests) and affects ${finalLabels.length} different components/areas.`; message += `This PR is too large with ${nonTestChanges} line changes (excluding tests) and affects ${originalLabelCount} different components/areas.`;
} else if (tooManyLabels) { } else if (tooManyLabels) {
message += `This PR affects ${finalLabels.length} different components/areas.`; message += `This PR affects ${originalLabelCount} different components/areas.`;
} else { } else {
message += `This PR is too large with ${nonTestChanges} line changes (excluding tests).`; message += `This PR is too large with ${nonTestChanges} line changes (excluding tests).`;
} }
@@ -466,8 +466,8 @@ jobs:
} }
// Handle reviews // Handle reviews
async function handleReviews(finalLabels) { async function handleReviews(finalLabels, originalLabelCount) {
const reviewMessages = generateReviewMessages(finalLabels); const reviewMessages = generateReviewMessages(finalLabels, originalLabelCount);
const hasReviewableLabels = finalLabels.some(label => const hasReviewableLabels = finalLabels.some(label =>
['too-big', 'needs-codeowners'].includes(label) ['too-big', 'needs-codeowners'].includes(label)
); );
@@ -627,6 +627,7 @@ jobs:
// Handle too many labels (only for non-mega PRs) // Handle too many labels (only for non-mega PRs)
const tooManyLabels = finalLabels.length > MAX_LABELS; const tooManyLabels = finalLabels.length > MAX_LABELS;
const originalLabelCount = finalLabels.length;
if (tooManyLabels && !isMegaPR && !finalLabels.includes('too-big')) { if (tooManyLabels && !isMegaPR && !finalLabels.includes('too-big')) {
finalLabels = ['too-big']; finalLabels = ['too-big'];
@@ -635,7 +636,7 @@ jobs:
console.log('Computed labels:', finalLabels.join(', ')); console.log('Computed labels:', finalLabels.join(', '));
// Handle reviews // Handle reviews
await handleReviews(finalLabels); await handleReviews(finalLabels, originalLabelCount);
// Apply labels // Apply labels
if (finalLabels.length > 0) { if (finalLabels.length > 0) {

View File

@@ -114,7 +114,7 @@ jobs:
matrix: matrix:
python-version: python-version:
- "3.11" - "3.11"
- "3.14" - "3.13"
os: os:
- ubuntu-latest - ubuntu-latest
- macOS-latest - macOS-latest
@@ -123,9 +123,9 @@ jobs:
# Minimize CI resource usage # Minimize CI resource usage
# by only running the Python version # by only running the Python version
# version used for docker images on Windows and macOS # version used for docker images on Windows and macOS
- python-version: "3.14" - python-version: "3.13"
os: windows-latest os: windows-latest
- python-version: "3.14" - python-version: "3.13"
os: macOS-latest os: macOS-latest
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
needs: needs:

View File

@@ -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(

View File

@@ -62,6 +62,7 @@ from esphome.cpp_types import ( # noqa: F401
EntityBase, EntityBase,
EntityCategory, EntityCategory,
ESPTime, ESPTime,
FixedVector,
GPIOPin, GPIOPin,
InternalGPIOPin, InternalGPIOPin,
JsonObject, JsonObject,

View File

@@ -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")

View File

@@ -989,7 +989,7 @@ message ListEntitiesClimateResponse {
bool supports_current_temperature = 5; // Deprecated: use feature_flags bool supports_current_temperature = 5; // Deprecated: use feature_flags
bool supports_two_point_target_temperature = 6; // Deprecated: use feature_flags bool supports_two_point_target_temperature = 6; // Deprecated: use feature_flags
repeated ClimateMode supported_modes = 7 [(container_pointer) = "std::set<climate::ClimateMode>"]; repeated ClimateMode supported_modes = 7 [(container_pointer_no_template) = "climate::ClimateModeMask"];
float visual_min_temperature = 8; float visual_min_temperature = 8;
float visual_max_temperature = 9; float visual_max_temperature = 9;
float visual_target_temperature_step = 10; float visual_target_temperature_step = 10;
@@ -998,11 +998,11 @@ message ListEntitiesClimateResponse {
// Deprecated in API version 1.5 // Deprecated in API version 1.5
bool legacy_supports_away = 11 [deprecated=true]; bool legacy_supports_away = 11 [deprecated=true];
bool supports_action = 12; // Deprecated: use feature_flags bool supports_action = 12; // Deprecated: use feature_flags
repeated ClimateFanMode supported_fan_modes = 13 [(container_pointer) = "std::set<climate::ClimateFanMode>"]; repeated ClimateFanMode supported_fan_modes = 13 [(container_pointer_no_template) = "climate::ClimateFanModeMask"];
repeated ClimateSwingMode supported_swing_modes = 14 [(container_pointer) = "std::set<climate::ClimateSwingMode>"]; repeated ClimateSwingMode supported_swing_modes = 14 [(container_pointer_no_template) = "climate::ClimateSwingModeMask"];
repeated string supported_custom_fan_modes = 15 [(container_pointer) = "std::set"]; repeated string supported_custom_fan_modes = 15 [(container_pointer) = "std::vector"];
repeated ClimatePreset supported_presets = 16 [(container_pointer) = "std::set<climate::ClimatePreset>"]; repeated ClimatePreset supported_presets = 16 [(container_pointer_no_template) = "climate::ClimatePresetMask"];
repeated string supported_custom_presets = 17 [(container_pointer) = "std::set"]; repeated string supported_custom_presets = 17 [(container_pointer) = "std::vector"];
bool disabled_by_default = 18; bool disabled_by_default = 18;
string icon = 19 [(field_ifdef) = "USE_ENTITY_ICON"]; string icon = 19 [(field_ifdef) = "USE_ENTITY_ICON"];
EntityCategory entity_category = 20; EntityCategory entity_category = 20;
@@ -1143,7 +1143,7 @@ message ListEntitiesSelectResponse {
reserved 4; // Deprecated: was string unique_id reserved 4; // Deprecated: was string unique_id
string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON"]; string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON"];
repeated string options = 6 [(container_pointer) = "std::vector"]; repeated string options = 6 [(container_pointer_no_template) = "FixedVector<const char *>"];
bool disabled_by_default = 7; bool disabled_by_default = 7;
EntityCategory entity_category = 8; EntityCategory entity_category = 8;
uint32 device_id = 9 [(field_ifdef) = "USE_DEVICES"]; uint32 device_id = 9 [(field_ifdef) = "USE_DEVICES"];

View File

@@ -669,18 +669,18 @@ uint16_t APIConnection::try_send_climate_info(EntityBase *entity, APIConnection
msg.supports_action = traits.has_feature_flags(climate::CLIMATE_SUPPORTS_ACTION); msg.supports_action = traits.has_feature_flags(climate::CLIMATE_SUPPORTS_ACTION);
// Current feature flags and other supported parameters // Current feature flags and other supported parameters
msg.feature_flags = traits.get_feature_flags(); msg.feature_flags = traits.get_feature_flags();
msg.supported_modes = &traits.get_supported_modes_for_api_(); msg.supported_modes = &traits.get_supported_modes();
msg.visual_min_temperature = traits.get_visual_min_temperature(); msg.visual_min_temperature = traits.get_visual_min_temperature();
msg.visual_max_temperature = traits.get_visual_max_temperature(); msg.visual_max_temperature = traits.get_visual_max_temperature();
msg.visual_target_temperature_step = traits.get_visual_target_temperature_step(); msg.visual_target_temperature_step = traits.get_visual_target_temperature_step();
msg.visual_current_temperature_step = traits.get_visual_current_temperature_step(); msg.visual_current_temperature_step = traits.get_visual_current_temperature_step();
msg.visual_min_humidity = traits.get_visual_min_humidity(); msg.visual_min_humidity = traits.get_visual_min_humidity();
msg.visual_max_humidity = traits.get_visual_max_humidity(); msg.visual_max_humidity = traits.get_visual_max_humidity();
msg.supported_fan_modes = &traits.get_supported_fan_modes_for_api_(); msg.supported_fan_modes = &traits.get_supported_fan_modes();
msg.supported_custom_fan_modes = &traits.get_supported_custom_fan_modes_for_api_(); msg.supported_custom_fan_modes = &traits.get_supported_custom_fan_modes();
msg.supported_presets = &traits.get_supported_presets_for_api_(); msg.supported_presets = &traits.get_supported_presets();
msg.supported_custom_presets = &traits.get_supported_custom_presets_for_api_(); msg.supported_custom_presets = &traits.get_supported_custom_presets();
msg.supported_swing_modes = &traits.get_supported_swing_modes_for_api_(); msg.supported_swing_modes = &traits.get_supported_swing_modes();
return fill_and_encode_entity_info(climate, msg, ListEntitiesClimateResponse::MESSAGE_TYPE, conn, remaining_size, return fill_and_encode_entity_info(climate, msg, ListEntitiesClimateResponse::MESSAGE_TYPE, conn, remaining_size,
is_single); is_single);
} }

View File

@@ -1475,8 +1475,8 @@ void ListEntitiesSelectResponse::encode(ProtoWriteBuffer buffer) const {
#ifdef USE_ENTITY_ICON #ifdef USE_ENTITY_ICON
buffer.encode_string(5, this->icon_ref_); buffer.encode_string(5, this->icon_ref_);
#endif #endif
for (const auto &it : *this->options) { for (const char *it : *this->options) {
buffer.encode_string(6, it, true); buffer.encode_string(6, it, strlen(it), true);
} }
buffer.encode_bool(7, this->disabled_by_default); buffer.encode_bool(7, this->disabled_by_default);
buffer.encode_uint32(8, static_cast<uint32_t>(this->entity_category)); buffer.encode_uint32(8, static_cast<uint32_t>(this->entity_category));
@@ -1492,8 +1492,8 @@ void ListEntitiesSelectResponse::calculate_size(ProtoSize &size) const {
size.add_length(1, this->icon_ref_.size()); size.add_length(1, this->icon_ref_.size());
#endif #endif
if (!this->options->empty()) { if (!this->options->empty()) {
for (const auto &it : *this->options) { for (const char *it : *this->options) {
size.add_length_force(1, it.size()); size.add_length_force(1, strlen(it));
} }
} }
size.add_bool(1, this->disabled_by_default); size.add_bool(1, this->disabled_by_default);

View File

@@ -1377,16 +1377,16 @@ class ListEntitiesClimateResponse final : public InfoResponseProtoMessage {
#endif #endif
bool supports_current_temperature{false}; bool supports_current_temperature{false};
bool supports_two_point_target_temperature{false}; bool supports_two_point_target_temperature{false};
const std::set<climate::ClimateMode> *supported_modes{}; const climate::ClimateModeMask *supported_modes{};
float visual_min_temperature{0.0f}; float visual_min_temperature{0.0f};
float visual_max_temperature{0.0f}; float visual_max_temperature{0.0f};
float visual_target_temperature_step{0.0f}; float visual_target_temperature_step{0.0f};
bool supports_action{false}; bool supports_action{false};
const std::set<climate::ClimateFanMode> *supported_fan_modes{}; const climate::ClimateFanModeMask *supported_fan_modes{};
const std::set<climate::ClimateSwingMode> *supported_swing_modes{}; const climate::ClimateSwingModeMask *supported_swing_modes{};
const std::set<std::string> *supported_custom_fan_modes{}; const std::vector<std::string> *supported_custom_fan_modes{};
const std::set<climate::ClimatePreset> *supported_presets{}; const climate::ClimatePresetMask *supported_presets{};
const std::set<std::string> *supported_custom_presets{}; const std::vector<std::string> *supported_custom_presets{};
float visual_current_temperature_step{0.0f}; float visual_current_temperature_step{0.0f};
bool supports_current_humidity{false}; bool supports_current_humidity{false};
bool supports_target_humidity{false}; bool supports_target_humidity{false};
@@ -1534,7 +1534,7 @@ class ListEntitiesSelectResponse final : public InfoResponseProtoMessage {
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "list_entities_select_response"; } const char *message_name() const override { return "list_entities_select_response"; }
#endif #endif
const std::vector<std::string> *options{}; const FixedVector<const char *> *options{};
void encode(ProtoWriteBuffer buffer) const override; void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(ProtoSize &size) const override; void calculate_size(ProtoSize &size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP

View File

@@ -88,6 +88,12 @@ static void dump_field(std::string &out, const char *field_name, StringRef value
out.append("\n"); out.append("\n");
} }
static void dump_field(std::string &out, const char *field_name, const char *value, int indent = 2) {
append_field_prefix(out, field_name, indent);
out.append("'").append(value).append("'");
out.append("\n");
}
template<typename T> static void dump_field(std::string &out, const char *field_name, T value, int indent = 2) { template<typename T> static void dump_field(std::string &out, const char *field_name, T value, int indent = 2) {
append_field_prefix(out, field_name, indent); append_field_prefix(out, field_name, indent);
out.append(proto_enum_to_string<T>(value)); out.append(proto_enum_to_string<T>(value));

View File

@@ -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

View File

@@ -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) {

View File

@@ -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

View File

@@ -99,9 +99,8 @@ enum BedjetCommand : uint8_t {
static const uint8_t BEDJET_FAN_SPEED_COUNT = 20; static const uint8_t BEDJET_FAN_SPEED_COUNT = 20;
static const char *const BEDJET_FAN_STEP_NAMES[BEDJET_FAN_SPEED_COUNT] = BEDJET_FAN_STEP_NAMES_; static constexpr const char *const BEDJET_FAN_STEP_NAMES[BEDJET_FAN_SPEED_COUNT] = BEDJET_FAN_STEP_NAMES_;
static const std::string BEDJET_FAN_STEP_NAME_STRINGS[BEDJET_FAN_SPEED_COUNT] = BEDJET_FAN_STEP_NAMES_; static const std::string BEDJET_FAN_STEP_NAME_STRINGS[BEDJET_FAN_SPEED_COUNT] = BEDJET_FAN_STEP_NAMES_;
static const std::set<std::string> BEDJET_FAN_STEP_NAMES_SET BEDJET_FAN_STEP_NAMES_;
} // namespace bedjet } // namespace bedjet
} // namespace esphome } // namespace esphome

View File

@@ -43,7 +43,7 @@ class BedJetClimate : public climate::Climate, public BedJetClient, public Polli
}); });
// It would be better if we had a slider for the fan modes. // It would be better if we had a slider for the fan modes.
traits.set_supported_custom_fan_modes(BEDJET_FAN_STEP_NAMES_SET); traits.set_supported_custom_fan_modes(BEDJET_FAN_STEP_NAMES);
traits.set_supported_presets({ traits.set_supported_presets({
// If we support NONE, then have to decide what happens if the user switches to it (turn off?) // If we support NONE, then have to decide what happens if the user switches to it (turn off?)
// climate::CLIMATE_PRESET_NONE, // climate::CLIMATE_PRESET_NONE,

View File

@@ -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(

View File

@@ -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;

View File

@@ -96,8 +96,11 @@ template<typename... Ts> class BLEClientWriteAction : public Action<Ts...>, publ
BLEClientWriteAction(BLEClient *ble_client) { BLEClientWriteAction(BLEClient *ble_client) {
ble_client->register_ble_node(this); ble_client->register_ble_node(this);
ble_client_ = ble_client; ble_client_ = ble_client;
this->construct_simple_value_();
} }
~BLEClientWriteAction() { this->destroy_simple_value_(); }
void set_service_uuid16(uint16_t uuid) { this->service_uuid_ = espbt::ESPBTUUID::from_uint16(uuid); } void set_service_uuid16(uint16_t uuid) { this->service_uuid_ = espbt::ESPBTUUID::from_uint16(uuid); }
void set_service_uuid32(uint32_t uuid) { this->service_uuid_ = espbt::ESPBTUUID::from_uint32(uuid); } void set_service_uuid32(uint32_t uuid) { this->service_uuid_ = espbt::ESPBTUUID::from_uint32(uuid); }
void set_service_uuid128(uint8_t *uuid) { this->service_uuid_ = espbt::ESPBTUUID::from_raw(uuid); } void set_service_uuid128(uint8_t *uuid) { this->service_uuid_ = espbt::ESPBTUUID::from_raw(uuid); }
@@ -106,14 +109,18 @@ template<typename... Ts> class BLEClientWriteAction : public Action<Ts...>, publ
void set_char_uuid32(uint32_t uuid) { this->char_uuid_ = espbt::ESPBTUUID::from_uint32(uuid); } void set_char_uuid32(uint32_t uuid) { this->char_uuid_ = espbt::ESPBTUUID::from_uint32(uuid); }
void set_char_uuid128(uint8_t *uuid) { this->char_uuid_ = espbt::ESPBTUUID::from_raw(uuid); } void set_char_uuid128(uint8_t *uuid) { this->char_uuid_ = espbt::ESPBTUUID::from_raw(uuid); }
void set_value_template(std::function<std::vector<uint8_t>(Ts...)> func) { void set_value_template(std::vector<uint8_t> (*func)(Ts...)) {
this->value_template_ = std::move(func); this->destroy_simple_value_();
has_simple_value_ = false; this->value_.template_func = func;
this->has_simple_value_ = false;
} }
void set_value_simple(const std::vector<uint8_t> &value) { void set_value_simple(const std::vector<uint8_t> &value) {
this->value_simple_ = value; if (!this->has_simple_value_) {
has_simple_value_ = true; this->construct_simple_value_();
}
this->value_.simple = value;
this->has_simple_value_ = true;
} }
void play(Ts... x) override {} void play(Ts... x) override {}
@@ -121,7 +128,7 @@ template<typename... Ts> class BLEClientWriteAction : public Action<Ts...>, publ
void play_complex(Ts... x) override { void play_complex(Ts... x) override {
this->num_running_++; this->num_running_++;
this->var_ = std::make_tuple(x...); this->var_ = std::make_tuple(x...);
auto value = this->has_simple_value_ ? this->value_simple_ : this->value_template_(x...); auto value = this->has_simple_value_ ? this->value_.simple : this->value_.template_func(x...);
// on write failure, continue the automation chain rather than stopping so that e.g. disconnect can work. // on write failure, continue the automation chain rather than stopping so that e.g. disconnect can work.
if (!write(value)) if (!write(value))
this->play_next_(x...); this->play_next_(x...);
@@ -194,10 +201,22 @@ template<typename... Ts> class BLEClientWriteAction : public Action<Ts...>, publ
} }
private: private:
void construct_simple_value_() { new (&this->value_.simple) std::vector<uint8_t>(); }
void destroy_simple_value_() {
if (this->has_simple_value_) {
this->value_.simple.~vector();
}
}
BLEClient *ble_client_; BLEClient *ble_client_;
bool has_simple_value_ = true; bool has_simple_value_ = true;
std::vector<uint8_t> value_simple_; union Value {
std::function<std::vector<uint8_t>(Ts...)> value_template_{}; std::vector<uint8_t> simple;
std::vector<uint8_t> (*template_func)(Ts...);
Value() {} // trivial constructor
~Value() {} // trivial destructor - we manage lifetime via discriminator
} value_;
espbt::ESPBTUUID service_uuid_; espbt::ESPBTUUID service_uuid_;
espbt::ESPBTUUID char_uuid_; espbt::ESPBTUUID char_uuid_;
std::tuple<Ts...> var_{}; std::tuple<Ts...> var_{};
@@ -213,9 +232,9 @@ template<typename... Ts> class BLEClientPasskeyReplyAction : public Action<Ts...
void play(Ts... x) override { void play(Ts... x) override {
uint32_t passkey; uint32_t passkey;
if (has_simple_value_) { if (has_simple_value_) {
passkey = this->value_simple_; passkey = this->value_.simple;
} else { } else {
passkey = this->value_template_(x...); passkey = this->value_.template_func(x...);
} }
if (passkey > 999999) if (passkey > 999999)
return; return;
@@ -224,21 +243,23 @@ template<typename... Ts> class BLEClientPasskeyReplyAction : public Action<Ts...
esp_ble_passkey_reply(remote_bda, true, passkey); esp_ble_passkey_reply(remote_bda, true, passkey);
} }
void set_value_template(std::function<uint32_t(Ts...)> func) { void set_value_template(uint32_t (*func)(Ts...)) {
this->value_template_ = std::move(func); this->value_.template_func = func;
has_simple_value_ = false; this->has_simple_value_ = false;
} }
void set_value_simple(const uint32_t &value) { void set_value_simple(const uint32_t &value) {
this->value_simple_ = value; this->value_.simple = value;
has_simple_value_ = true; this->has_simple_value_ = true;
} }
private: private:
BLEClient *parent_{nullptr}; BLEClient *parent_{nullptr};
bool has_simple_value_ = true; bool has_simple_value_ = true;
uint32_t value_simple_{0}; union {
std::function<uint32_t(Ts...)> value_template_{}; uint32_t simple;
uint32_t (*template_func)(Ts...);
} value_{.simple = 0};
}; };
template<typename... Ts> class BLEClientNumericComparisonReplyAction : public Action<Ts...> { template<typename... Ts> class BLEClientNumericComparisonReplyAction : public Action<Ts...> {
@@ -249,27 +270,29 @@ template<typename... Ts> class BLEClientNumericComparisonReplyAction : public Ac
esp_bd_addr_t remote_bda; esp_bd_addr_t remote_bda;
memcpy(remote_bda, parent_->get_remote_bda(), sizeof(esp_bd_addr_t)); memcpy(remote_bda, parent_->get_remote_bda(), sizeof(esp_bd_addr_t));
if (has_simple_value_) { if (has_simple_value_) {
esp_ble_confirm_reply(remote_bda, this->value_simple_); esp_ble_confirm_reply(remote_bda, this->value_.simple);
} else { } else {
esp_ble_confirm_reply(remote_bda, this->value_template_(x...)); esp_ble_confirm_reply(remote_bda, this->value_.template_func(x...));
} }
} }
void set_value_template(std::function<bool(Ts...)> func) { void set_value_template(bool (*func)(Ts...)) {
this->value_template_ = std::move(func); this->value_.template_func = func;
has_simple_value_ = false; this->has_simple_value_ = false;
} }
void set_value_simple(const bool &value) { void set_value_simple(const bool &value) {
this->value_simple_ = value; this->value_.simple = value;
has_simple_value_ = true; this->has_simple_value_ = true;
} }
private: private:
BLEClient *parent_{nullptr}; BLEClient *parent_{nullptr};
bool has_simple_value_ = true; bool has_simple_value_ = true;
bool value_simple_{false}; union {
std::function<bool(Ts...)> value_template_{}; bool simple;
bool (*template_func)(Ts...);
} value_{.simple = false};
}; };
template<typename... Ts> class BLEClientRemoveBondAction : public Action<Ts...> { template<typename... Ts> class BLEClientRemoveBondAction : public Action<Ts...> {

View File

@@ -117,9 +117,9 @@ void BLESensor::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t ga
} }
float BLESensor::parse_data_(uint8_t *value, uint16_t value_len) { float BLESensor::parse_data_(uint8_t *value, uint16_t value_len) {
if (this->data_to_value_func_.has_value()) { if (this->has_data_to_value_) {
std::vector<uint8_t> data(value, value + value_len); std::vector<uint8_t> data(value, value + value_len);
return (*this->data_to_value_func_)(data); return this->data_to_value_func_(data);
} else { } else {
return value[0]; return value[0];
} }

View File

@@ -15,8 +15,6 @@ namespace ble_client {
namespace espbt = esphome::esp32_ble_tracker; namespace espbt = esphome::esp32_ble_tracker;
using data_to_value_t = std::function<float(std::vector<uint8_t>)>;
class BLESensor : public sensor::Sensor, public PollingComponent, public BLEClientNode { class BLESensor : public sensor::Sensor, public PollingComponent, public BLEClientNode {
public: public:
void loop() override; void loop() override;
@@ -33,13 +31,17 @@ class BLESensor : public sensor::Sensor, public PollingComponent, public BLEClie
void set_descr_uuid16(uint16_t uuid) { this->descr_uuid_ = espbt::ESPBTUUID::from_uint16(uuid); } void set_descr_uuid16(uint16_t uuid) { this->descr_uuid_ = espbt::ESPBTUUID::from_uint16(uuid); }
void set_descr_uuid32(uint32_t uuid) { this->descr_uuid_ = espbt::ESPBTUUID::from_uint32(uuid); } void set_descr_uuid32(uint32_t uuid) { this->descr_uuid_ = espbt::ESPBTUUID::from_uint32(uuid); }
void set_descr_uuid128(uint8_t *uuid) { this->descr_uuid_ = espbt::ESPBTUUID::from_raw(uuid); } void set_descr_uuid128(uint8_t *uuid) { this->descr_uuid_ = espbt::ESPBTUUID::from_raw(uuid); }
void set_data_to_value(data_to_value_t &&lambda) { this->data_to_value_func_ = lambda; } void set_data_to_value(float (*lambda)(const std::vector<uint8_t> &)) {
this->data_to_value_func_ = lambda;
this->has_data_to_value_ = true;
}
void set_enable_notify(bool notify) { this->notify_ = notify; } void set_enable_notify(bool notify) { this->notify_ = notify; }
uint16_t handle; uint16_t handle;
protected: protected:
float parse_data_(uint8_t *value, uint16_t value_len); float parse_data_(uint8_t *value, uint16_t value_len);
optional<data_to_value_t> data_to_value_func_{}; bool has_data_to_value_{false};
float (*data_to_value_func_)(const std::vector<uint8_t> &){};
bool notify_; bool notify_;
espbt::ESPBTUUID service_uuid_; espbt::ESPBTUUID service_uuid_;
espbt::ESPBTUUID char_uuid_; espbt::ESPBTUUID char_uuid_;

View File

@@ -385,7 +385,7 @@ void Climate::save_state_() {
if (!traits.get_supported_custom_fan_modes().empty() && custom_fan_mode.has_value()) { if (!traits.get_supported_custom_fan_modes().empty() && custom_fan_mode.has_value()) {
state.uses_custom_fan_mode = true; state.uses_custom_fan_mode = true;
const auto &supported = traits.get_supported_custom_fan_modes(); const auto &supported = traits.get_supported_custom_fan_modes();
// std::set has consistent order (lexicographic for strings) // std::vector maintains insertion order
size_t i = 0; size_t i = 0;
for (const auto &mode : supported) { for (const auto &mode : supported) {
if (mode == custom_fan_mode) { if (mode == custom_fan_mode) {
@@ -402,7 +402,7 @@ void Climate::save_state_() {
if (!traits.get_supported_custom_presets().empty() && custom_preset.has_value()) { if (!traits.get_supported_custom_presets().empty() && custom_preset.has_value()) {
state.uses_custom_preset = true; state.uses_custom_preset = true;
const auto &supported = traits.get_supported_custom_presets(); const auto &supported = traits.get_supported_custom_presets();
// std::set has consistent order (lexicographic for strings) // std::vector maintains insertion order
size_t i = 0; size_t i = 0;
for (const auto &preset : supported) { for (const auto &preset : supported) {
if (preset == custom_preset) { if (preset == custom_preset) {

View File

@@ -7,6 +7,7 @@ namespace esphome {
namespace climate { namespace climate {
/// Enum for all modes a climate device can be in. /// Enum for all modes a climate device can be in.
/// NOTE: If adding values, update ClimateModeMask in climate_traits.h to use the new last value
enum ClimateMode : uint8_t { enum ClimateMode : uint8_t {
/// The climate device is off /// The climate device is off
CLIMATE_MODE_OFF = 0, CLIMATE_MODE_OFF = 0,
@@ -24,7 +25,7 @@ enum ClimateMode : uint8_t {
* For example, the target temperature can be adjusted based on a schedule, or learned behavior. * For example, the target temperature can be adjusted based on a schedule, or learned behavior.
* The target temperature can't be adjusted when in this mode. * The target temperature can't be adjusted when in this mode.
*/ */
CLIMATE_MODE_AUTO = 6 CLIMATE_MODE_AUTO = 6 // Update ClimateModeMask in climate_traits.h if adding values after this
}; };
/// Enum for the current action of the climate device. Values match those of ClimateMode. /// Enum for the current action of the climate device. Values match those of ClimateMode.
@@ -43,6 +44,7 @@ enum ClimateAction : uint8_t {
CLIMATE_ACTION_FAN = 6, CLIMATE_ACTION_FAN = 6,
}; };
/// NOTE: If adding values, update ClimateFanModeMask in climate_traits.h to use the new last value
enum ClimateFanMode : uint8_t { enum ClimateFanMode : uint8_t {
/// The fan mode is set to On /// The fan mode is set to On
CLIMATE_FAN_ON = 0, CLIMATE_FAN_ON = 0,
@@ -63,10 +65,11 @@ enum ClimateFanMode : uint8_t {
/// The fan mode is set to Diffuse /// The fan mode is set to Diffuse
CLIMATE_FAN_DIFFUSE = 8, CLIMATE_FAN_DIFFUSE = 8,
/// The fan mode is set to Quiet /// The fan mode is set to Quiet
CLIMATE_FAN_QUIET = 9, CLIMATE_FAN_QUIET = 9, // Update ClimateFanModeMask in climate_traits.h if adding values after this
}; };
/// Enum for all modes a climate swing can be in /// Enum for all modes a climate swing can be in
/// NOTE: If adding values, update ClimateSwingModeMask in climate_traits.h to use the new last value
enum ClimateSwingMode : uint8_t { enum ClimateSwingMode : uint8_t {
/// The swing mode is set to Off /// The swing mode is set to Off
CLIMATE_SWING_OFF = 0, CLIMATE_SWING_OFF = 0,
@@ -75,10 +78,11 @@ enum ClimateSwingMode : uint8_t {
/// The fan mode is set to Vertical /// The fan mode is set to Vertical
CLIMATE_SWING_VERTICAL = 2, CLIMATE_SWING_VERTICAL = 2,
/// The fan mode is set to Horizontal /// The fan mode is set to Horizontal
CLIMATE_SWING_HORIZONTAL = 3, CLIMATE_SWING_HORIZONTAL = 3, // Update ClimateSwingModeMask in climate_traits.h if adding values after this
}; };
/// Enum for all preset modes /// Enum for all preset modes
/// NOTE: If adding values, update ClimatePresetMask in climate_traits.h to use the new last value
enum ClimatePreset : uint8_t { enum ClimatePreset : uint8_t {
/// No preset is active /// No preset is active
CLIMATE_PRESET_NONE = 0, CLIMATE_PRESET_NONE = 0,
@@ -95,7 +99,7 @@ enum ClimatePreset : uint8_t {
/// Device is prepared for sleep /// Device is prepared for sleep
CLIMATE_PRESET_SLEEP = 6, CLIMATE_PRESET_SLEEP = 6,
/// Device is reacting to activity (e.g., movement sensors) /// Device is reacting to activity (e.g., movement sensors)
CLIMATE_PRESET_ACTIVITY = 7, CLIMATE_PRESET_ACTIVITY = 7, // Update ClimatePresetMask in climate_traits.h if adding values after this
}; };
enum ClimateFeature : uint32_t { enum ClimateFeature : uint32_t {

View File

@@ -1,19 +1,33 @@
#pragma once #pragma once
#include <set> #include <vector>
#include "climate_mode.h" #include "climate_mode.h"
#include "esphome/core/finite_set_mask.h"
#include "esphome/core/helpers.h" #include "esphome/core/helpers.h"
namespace esphome { namespace esphome {
#ifdef USE_API
namespace api {
class APIConnection;
} // namespace api
#endif
namespace climate { namespace climate {
// Type aliases for climate enum bitmasks
// These replace std::set<EnumType> to eliminate red-black tree overhead
// For contiguous enums starting at 0, DefaultBitPolicy provides 1:1 mapping (enum value = bit position)
// Bitmask size is automatically calculated from the last enum value
using ClimateModeMask = FiniteSetMask<ClimateMode, DefaultBitPolicy<ClimateMode, CLIMATE_MODE_AUTO + 1>>;
using ClimateFanModeMask = FiniteSetMask<ClimateFanMode, DefaultBitPolicy<ClimateFanMode, CLIMATE_FAN_QUIET + 1>>;
using ClimateSwingModeMask =
FiniteSetMask<ClimateSwingMode, DefaultBitPolicy<ClimateSwingMode, CLIMATE_SWING_HORIZONTAL + 1>>;
using ClimatePresetMask = FiniteSetMask<ClimatePreset, DefaultBitPolicy<ClimatePreset, CLIMATE_PRESET_ACTIVITY + 1>>;
// Lightweight linear search for small vectors (1-20 items)
// Avoids std::find template overhead
template<typename T> inline bool vector_contains(const std::vector<T> &vec, const T &value) {
for (const auto &item : vec) {
if (item == value)
return true;
}
return false;
}
/** This class contains all static data for climate devices. /** This class contains all static data for climate devices.
* *
* All climate devices must support these features: * All climate devices must support these features:
@@ -107,48 +121,60 @@ class ClimateTraits {
} }
} }
void set_supported_modes(std::set<ClimateMode> modes) { this->supported_modes_ = std::move(modes); } void set_supported_modes(ClimateModeMask modes) { this->supported_modes_ = modes; }
void add_supported_mode(ClimateMode mode) { this->supported_modes_.insert(mode); } void add_supported_mode(ClimateMode mode) { this->supported_modes_.insert(mode); }
bool supports_mode(ClimateMode mode) const { return this->supported_modes_.count(mode); } bool supports_mode(ClimateMode mode) const { return this->supported_modes_.count(mode); }
const std::set<ClimateMode> &get_supported_modes() const { return this->supported_modes_; } const ClimateModeMask &get_supported_modes() const { return this->supported_modes_; }
void set_supported_fan_modes(std::set<ClimateFanMode> modes) { this->supported_fan_modes_ = std::move(modes); } void set_supported_fan_modes(ClimateFanModeMask modes) { this->supported_fan_modes_ = modes; }
void add_supported_fan_mode(ClimateFanMode mode) { this->supported_fan_modes_.insert(mode); } void add_supported_fan_mode(ClimateFanMode mode) { this->supported_fan_modes_.insert(mode); }
void add_supported_custom_fan_mode(const std::string &mode) { this->supported_custom_fan_modes_.insert(mode); } void add_supported_custom_fan_mode(const std::string &mode) { this->supported_custom_fan_modes_.push_back(mode); }
bool supports_fan_mode(ClimateFanMode fan_mode) const { return this->supported_fan_modes_.count(fan_mode); } bool supports_fan_mode(ClimateFanMode fan_mode) const { return this->supported_fan_modes_.count(fan_mode); }
bool get_supports_fan_modes() const { bool get_supports_fan_modes() const {
return !this->supported_fan_modes_.empty() || !this->supported_custom_fan_modes_.empty(); return !this->supported_fan_modes_.empty() || !this->supported_custom_fan_modes_.empty();
} }
const std::set<ClimateFanMode> &get_supported_fan_modes() const { return this->supported_fan_modes_; } const ClimateFanModeMask &get_supported_fan_modes() const { return this->supported_fan_modes_; }
void set_supported_custom_fan_modes(std::set<std::string> supported_custom_fan_modes) { void set_supported_custom_fan_modes(std::vector<std::string> supported_custom_fan_modes) {
this->supported_custom_fan_modes_ = std::move(supported_custom_fan_modes); this->supported_custom_fan_modes_ = std::move(supported_custom_fan_modes);
} }
const std::set<std::string> &get_supported_custom_fan_modes() const { return this->supported_custom_fan_modes_; } void set_supported_custom_fan_modes(std::initializer_list<std::string> modes) {
this->supported_custom_fan_modes_ = modes;
}
template<size_t N> void set_supported_custom_fan_modes(const char *const (&modes)[N]) {
this->supported_custom_fan_modes_.assign(modes, modes + N);
}
const std::vector<std::string> &get_supported_custom_fan_modes() const { return this->supported_custom_fan_modes_; }
bool supports_custom_fan_mode(const std::string &custom_fan_mode) const { bool supports_custom_fan_mode(const std::string &custom_fan_mode) const {
return this->supported_custom_fan_modes_.count(custom_fan_mode); return vector_contains(this->supported_custom_fan_modes_, custom_fan_mode);
} }
void set_supported_presets(std::set<ClimatePreset> presets) { this->supported_presets_ = std::move(presets); } void set_supported_presets(ClimatePresetMask presets) { this->supported_presets_ = presets; }
void add_supported_preset(ClimatePreset preset) { this->supported_presets_.insert(preset); } void add_supported_preset(ClimatePreset preset) { this->supported_presets_.insert(preset); }
void add_supported_custom_preset(const std::string &preset) { this->supported_custom_presets_.insert(preset); } void add_supported_custom_preset(const std::string &preset) { this->supported_custom_presets_.push_back(preset); }
bool supports_preset(ClimatePreset preset) const { return this->supported_presets_.count(preset); } bool supports_preset(ClimatePreset preset) const { return this->supported_presets_.count(preset); }
bool get_supports_presets() const { return !this->supported_presets_.empty(); } bool get_supports_presets() const { return !this->supported_presets_.empty(); }
const std::set<climate::ClimatePreset> &get_supported_presets() const { return this->supported_presets_; } const ClimatePresetMask &get_supported_presets() const { return this->supported_presets_; }
void set_supported_custom_presets(std::set<std::string> supported_custom_presets) { void set_supported_custom_presets(std::vector<std::string> supported_custom_presets) {
this->supported_custom_presets_ = std::move(supported_custom_presets); this->supported_custom_presets_ = std::move(supported_custom_presets);
} }
const std::set<std::string> &get_supported_custom_presets() const { return this->supported_custom_presets_; } void set_supported_custom_presets(std::initializer_list<std::string> presets) {
this->supported_custom_presets_ = presets;
}
template<size_t N> void set_supported_custom_presets(const char *const (&presets)[N]) {
this->supported_custom_presets_.assign(presets, presets + N);
}
const std::vector<std::string> &get_supported_custom_presets() const { return this->supported_custom_presets_; }
bool supports_custom_preset(const std::string &custom_preset) const { bool supports_custom_preset(const std::string &custom_preset) const {
return this->supported_custom_presets_.count(custom_preset); return vector_contains(this->supported_custom_presets_, custom_preset);
} }
void set_supported_swing_modes(std::set<ClimateSwingMode> modes) { this->supported_swing_modes_ = std::move(modes); } void set_supported_swing_modes(ClimateSwingModeMask modes) { this->supported_swing_modes_ = modes; }
void add_supported_swing_mode(ClimateSwingMode mode) { this->supported_swing_modes_.insert(mode); } void add_supported_swing_mode(ClimateSwingMode mode) { this->supported_swing_modes_.insert(mode); }
bool supports_swing_mode(ClimateSwingMode swing_mode) const { return this->supported_swing_modes_.count(swing_mode); } bool supports_swing_mode(ClimateSwingMode swing_mode) const { return this->supported_swing_modes_.count(swing_mode); }
bool get_supports_swing_modes() const { return !this->supported_swing_modes_.empty(); } bool get_supports_swing_modes() const { return !this->supported_swing_modes_.empty(); }
const std::set<ClimateSwingMode> &get_supported_swing_modes() const { return this->supported_swing_modes_; } const ClimateSwingModeMask &get_supported_swing_modes() const { return this->supported_swing_modes_; }
float get_visual_min_temperature() const { return this->visual_min_temperature_; } float get_visual_min_temperature() const { return this->visual_min_temperature_; }
void set_visual_min_temperature(float visual_min_temperature) { void set_visual_min_temperature(float visual_min_temperature) {
@@ -179,23 +205,6 @@ class ClimateTraits {
void set_visual_max_humidity(float visual_max_humidity) { this->visual_max_humidity_ = visual_max_humidity; } void set_visual_max_humidity(float visual_max_humidity) { this->visual_max_humidity_ = visual_max_humidity; }
protected: protected:
#ifdef USE_API
// The API connection is a friend class to access internal methods
friend class api::APIConnection;
// These methods return references to internal data structures.
// They are used by the API to avoid copying data when encoding messages.
// Warning: Do not use these methods outside of the API connection code.
// They return references to internal data that can be invalidated.
const std::set<ClimateMode> &get_supported_modes_for_api_() const { return this->supported_modes_; }
const std::set<ClimateFanMode> &get_supported_fan_modes_for_api_() const { return this->supported_fan_modes_; }
const std::set<std::string> &get_supported_custom_fan_modes_for_api_() const {
return this->supported_custom_fan_modes_;
}
const std::set<climate::ClimatePreset> &get_supported_presets_for_api_() const { return this->supported_presets_; }
const std::set<std::string> &get_supported_custom_presets_for_api_() const { return this->supported_custom_presets_; }
const std::set<ClimateSwingMode> &get_supported_swing_modes_for_api_() const { return this->supported_swing_modes_; }
#endif
void set_mode_support_(climate::ClimateMode mode, bool supported) { void set_mode_support_(climate::ClimateMode mode, bool supported) {
if (supported) { if (supported) {
this->supported_modes_.insert(mode); this->supported_modes_.insert(mode);
@@ -226,12 +235,12 @@ class ClimateTraits {
float visual_min_humidity_{30}; float visual_min_humidity_{30};
float visual_max_humidity_{99}; float visual_max_humidity_{99};
std::set<climate::ClimateMode> supported_modes_ = {climate::CLIMATE_MODE_OFF}; climate::ClimateModeMask supported_modes_{climate::CLIMATE_MODE_OFF};
std::set<climate::ClimateFanMode> supported_fan_modes_; climate::ClimateFanModeMask supported_fan_modes_;
std::set<climate::ClimateSwingMode> supported_swing_modes_; climate::ClimateSwingModeMask supported_swing_modes_;
std::set<climate::ClimatePreset> supported_presets_; climate::ClimatePresetMask supported_presets_;
std::set<std::string> supported_custom_fan_modes_; std::vector<std::string> supported_custom_fan_modes_;
std::set<std::string> supported_custom_presets_; std::vector<std::string> supported_custom_presets_;
}; };
} // namespace climate } // namespace climate

View File

@@ -24,16 +24,18 @@ class ClimateIR : public Component,
public remote_base::RemoteTransmittable { public remote_base::RemoteTransmittable {
public: public:
ClimateIR(float minimum_temperature, float maximum_temperature, float temperature_step = 1.0f, ClimateIR(float minimum_temperature, float maximum_temperature, float temperature_step = 1.0f,
bool supports_dry = false, bool supports_fan_only = false, std::set<climate::ClimateFanMode> fan_modes = {}, bool supports_dry = false, bool supports_fan_only = false,
std::set<climate::ClimateSwingMode> swing_modes = {}, std::set<climate::ClimatePreset> presets = {}) { climate::ClimateFanModeMask fan_modes = climate::ClimateFanModeMask(),
climate::ClimateSwingModeMask swing_modes = climate::ClimateSwingModeMask(),
climate::ClimatePresetMask presets = climate::ClimatePresetMask()) {
this->minimum_temperature_ = minimum_temperature; this->minimum_temperature_ = minimum_temperature;
this->maximum_temperature_ = maximum_temperature; this->maximum_temperature_ = maximum_temperature;
this->temperature_step_ = temperature_step; this->temperature_step_ = temperature_step;
this->supports_dry_ = supports_dry; this->supports_dry_ = supports_dry;
this->supports_fan_only_ = supports_fan_only; this->supports_fan_only_ = supports_fan_only;
this->fan_modes_ = std::move(fan_modes); this->fan_modes_ = fan_modes;
this->swing_modes_ = std::move(swing_modes); this->swing_modes_ = swing_modes;
this->presets_ = std::move(presets); this->presets_ = presets;
} }
void setup() override; void setup() override;
@@ -60,9 +62,9 @@ class ClimateIR : public Component,
bool supports_heat_{true}; bool supports_heat_{true};
bool supports_dry_{false}; bool supports_dry_{false};
bool supports_fan_only_{false}; bool supports_fan_only_{false};
std::set<climate::ClimateFanMode> fan_modes_ = {}; climate::ClimateFanModeMask fan_modes_{};
std::set<climate::ClimateSwingMode> swing_modes_ = {}; climate::ClimateSwingModeMask swing_modes_{};
std::set<climate::ClimatePreset> presets_ = {}; climate::ClimatePresetMask presets_{};
sensor::Sensor *sensor_{nullptr}; sensor::Sensor *sensor_{nullptr};
}; };

View File

@@ -304,9 +304,13 @@ def _format_framework_arduino_version(ver: cv.Version) -> str:
def _format_framework_espidf_version(ver: cv.Version, release: str) -> str: def _format_framework_espidf_version(ver: cv.Version, release: str) -> str:
# format the given espidf (https://github.com/pioarduino/esp-idf/releases) version to # format the given espidf (https://github.com/pioarduino/esp-idf/releases) version to
# a PIO platformio/framework-espidf value # a PIO platformio/framework-espidf value
if ver == cv.Version(5, 4, 3) or ver >= cv.Version(5, 5, 1):
ext = "tar.xz"
else:
ext = "zip"
if release: if release:
return f"pioarduino/framework-espidf@https://github.com/pioarduino/esp-idf/releases/download/v{str(ver)}.{release}/esp-idf-v{str(ver)}.zip" return f"pioarduino/framework-espidf@https://github.com/pioarduino/esp-idf/releases/download/v{str(ver)}.{release}/esp-idf-v{str(ver)}.{ext}"
return f"pioarduino/framework-espidf@https://github.com/pioarduino/esp-idf/releases/download/v{str(ver)}/esp-idf-v{str(ver)}.zip" return f"pioarduino/framework-espidf@https://github.com/pioarduino/esp-idf/releases/download/v{str(ver)}/esp-idf-v{str(ver)}.{ext}"
def _is_framework_url(source: str) -> str: def _is_framework_url(source: str) -> str:
@@ -355,6 +359,7 @@ ESP_IDF_FRAMEWORK_VERSION_LOOKUP = {
ESP_IDF_PLATFORM_VERSION_LOOKUP = { ESP_IDF_PLATFORM_VERSION_LOOKUP = {
cv.Version(5, 5, 1): cv.Version(55, 3, 31, "1"), cv.Version(5, 5, 1): cv.Version(55, 3, 31, "1"),
cv.Version(5, 5, 0): cv.Version(55, 3, 31, "1"), cv.Version(5, 5, 0): cv.Version(55, 3, 31, "1"),
cv.Version(5, 4, 3): cv.Version(55, 3, 32),
cv.Version(5, 4, 2): cv.Version(54, 3, 21, "2"), cv.Version(5, 4, 2): cv.Version(54, 3, 21, "2"),
cv.Version(5, 4, 1): cv.Version(54, 3, 21, "2"), cv.Version(5, 4, 1): cv.Version(54, 3, 21, "2"),
cv.Version(5, 4, 0): cv.Version(54, 3, 21, "2"), cv.Version(5, 4, 0): cv.Version(54, 3, 21, "2"),

View File

@@ -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;
}; };

View File

@@ -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])))

View File

@@ -461,7 +461,9 @@ async def parse_value(value_config, args):
if isinstance(value, str): if isinstance(value, str):
value = list(value.encode(value_config[CONF_STRING_ENCODING])) value = list(value.encode(value_config[CONF_STRING_ENCODING]))
if isinstance(value, list): if isinstance(value, list):
return cg.std_vector.template(cg.uint8)(value) # Generate initializer list {1, 2, 3} instead of std::vector<uint8_t>({1, 2, 3})
# This calls the set_value(std::initializer_list<uint8_t>) overload
return cg.ArrayInitializer(*value)
val = cg.RawExpression(f"{value_config[CONF_TYPE]}({cg.safe_exp(value)})") val = cg.RawExpression(f"{value_config[CONF_TYPE]}({cg.safe_exp(value)})")
return ByteBuffer_ns.wrap(val, value_config[CONF_ENDIANNESS]) return ByteBuffer_ns.wrap(val, value_config[CONF_ENDIANNESS])

View File

@@ -35,13 +35,18 @@ BLECharacteristic::BLECharacteristic(const ESPBTUUID uuid, uint32_t properties)
void BLECharacteristic::set_value(ByteBuffer buffer) { this->set_value(buffer.get_data()); } void BLECharacteristic::set_value(ByteBuffer buffer) { this->set_value(buffer.get_data()); }
void BLECharacteristic::set_value(const std::vector<uint8_t> &buffer) { void BLECharacteristic::set_value(std::vector<uint8_t> &&buffer) {
xSemaphoreTake(this->set_value_lock_, 0L); xSemaphoreTake(this->set_value_lock_, 0L);
this->value_ = buffer; this->value_ = std::move(buffer);
xSemaphoreGive(this->set_value_lock_); xSemaphoreGive(this->set_value_lock_);
} }
void BLECharacteristic::set_value(std::initializer_list<uint8_t> data) {
this->set_value(std::vector<uint8_t>(data)); // Delegate to move overload
}
void BLECharacteristic::set_value(const std::string &buffer) { void BLECharacteristic::set_value(const std::string &buffer) {
this->set_value(std::vector<uint8_t>(buffer.begin(), buffer.end())); this->set_value(std::vector<uint8_t>(buffer.begin(), buffer.end())); // Delegate to move overload
} }
void BLECharacteristic::notify() { void BLECharacteristic::notify() {

View File

@@ -33,7 +33,8 @@ class BLECharacteristic {
~BLECharacteristic(); ~BLECharacteristic();
void set_value(ByteBuffer buffer); void set_value(ByteBuffer buffer);
void set_value(const std::vector<uint8_t> &buffer); void set_value(std::vector<uint8_t> &&buffer);
void set_value(std::initializer_list<uint8_t> data);
void set_value(const std::string &buffer); void set_value(const std::string &buffer);
void set_broadcast_property(bool value); void set_broadcast_property(bool value);

View File

@@ -46,15 +46,17 @@ void BLEDescriptor::do_create(BLECharacteristic *characteristic) {
this->state_ = CREATING; this->state_ = CREATING;
} }
void BLEDescriptor::set_value(std::vector<uint8_t> buffer) { void BLEDescriptor::set_value(std::vector<uint8_t> &&buffer) { this->set_value_impl_(buffer.data(), buffer.size()); }
size_t length = buffer.size();
void BLEDescriptor::set_value(std::initializer_list<uint8_t> data) { this->set_value_impl_(data.begin(), data.size()); }
void BLEDescriptor::set_value_impl_(const uint8_t *data, size_t length) {
if (length > this->value_.attr_max_len) { if (length > this->value_.attr_max_len) {
ESP_LOGE(TAG, "Size %d too large, must be no bigger than %d", length, this->value_.attr_max_len); ESP_LOGE(TAG, "Size %d too large, must be no bigger than %d", length, this->value_.attr_max_len);
return; return;
} }
this->value_.attr_len = length; this->value_.attr_len = length;
memcpy(this->value_.attr_value, buffer.data(), length); memcpy(this->value_.attr_value, data, length);
} }
void BLEDescriptor::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if, void BLEDescriptor::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if,

View File

@@ -27,7 +27,8 @@ class BLEDescriptor {
void do_create(BLECharacteristic *characteristic); void do_create(BLECharacteristic *characteristic);
ESPBTUUID get_uuid() const { return this->uuid_; } ESPBTUUID get_uuid() const { return this->uuid_; }
void set_value(std::vector<uint8_t> buffer); void set_value(std::vector<uint8_t> &&buffer);
void set_value(std::initializer_list<uint8_t> data);
void set_value(ByteBuffer buffer) { this->set_value(buffer.get_data()); } void set_value(ByteBuffer buffer) { this->set_value(buffer.get_data()); }
void gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if, esp_ble_gatts_cb_param_t *param); void gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if, esp_ble_gatts_cb_param_t *param);
@@ -42,6 +43,8 @@ class BLEDescriptor {
} }
protected: protected:
void set_value_impl_(const uint8_t *data, size_t length);
BLECharacteristic *characteristic_{nullptr}; BLECharacteristic *characteristic_{nullptr};
ESPBTUUID uuid_; ESPBTUUID uuid_;
uint16_t handle_{0xFFFF}; uint16_t handle_{0xFFFF};

View File

@@ -270,8 +270,8 @@ void ESP32ImprovComponent::set_error_(improv::Error error) {
} }
} }
void ESP32ImprovComponent::send_response_(std::vector<uint8_t> &response) { void ESP32ImprovComponent::send_response_(std::vector<uint8_t> &&response) {
this->rpc_response_->set_value(ByteBuffer::wrap(response)); this->rpc_response_->set_value(std::move(response));
if (this->state_ != improv::STATE_STOPPED) if (this->state_ != improv::STATE_STOPPED)
this->rpc_response_->notify(); this->rpc_response_->notify();
} }
@@ -409,10 +409,8 @@ void ESP32ImprovComponent::check_wifi_connection_() {
} }
} }
#endif #endif
// Pass to build_rpc_response using vector constructor from iterators to avoid extra copies this->send_response_(improv::build_rpc_response(improv::WIFI_SETTINGS,
std::vector<uint8_t> data = improv::build_rpc_response( std::vector<std::string>(url_strings, url_strings + url_count)));
improv::WIFI_SETTINGS, std::vector<std::string>(url_strings, url_strings + url_count));
this->send_response_(data);
} else if (this->is_active() && this->state_ != improv::STATE_PROVISIONED) { } else if (this->is_active() && this->state_ != improv::STATE_PROVISIONED) {
ESP_LOGD(TAG, "WiFi provisioned externally"); ESP_LOGD(TAG, "WiFi provisioned externally");
} }

View File

@@ -109,7 +109,7 @@ class ESP32ImprovComponent : public Component, public improv_base::ImprovBase {
void set_state_(improv::State state, bool update_advertising = true); void set_state_(improv::State state, bool update_advertising = true);
void set_error_(improv::Error error); void set_error_(improv::Error error);
improv::State get_initial_state_() const; improv::State get_initial_state_() const;
void send_response_(std::vector<uint8_t> &response); void send_response_(std::vector<uint8_t> &&response);
void process_incoming_data_(); void process_incoming_data_();
void on_wifi_connect_timeout_(); void on_wifi_connect_timeout_();
void check_wifi_connection_(); void check_wifi_connection_();

View File

@@ -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

View File

@@ -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][

View File

@@ -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])),
) )

View File

@@ -60,8 +60,6 @@ class FanCall {
this->speed_ = speed; this->speed_ = speed;
return *this; return *this;
} }
ESPDEPRECATED("set_speed() with string argument is deprecated, use integer argument instead.", "2021.9")
FanCall &set_speed(const char *legacy_speed);
optional<int> get_speed() const { return this->speed_; } optional<int> get_speed() const { return this->speed_; }
FanCall &set_direction(FanDirection direction) { FanCall &set_direction(FanDirection direction) {
this->direction_ = direction; this->direction_ = direction;

View File

@@ -171,7 +171,7 @@ void HaierClimateBase::toggle_power() {
PendingAction({ActionRequest::TOGGLE_POWER, esphome::optional<haier_protocol::HaierMessage>()}); PendingAction({ActionRequest::TOGGLE_POWER, esphome::optional<haier_protocol::HaierMessage>()});
} }
void HaierClimateBase::set_supported_swing_modes(const std::set<climate::ClimateSwingMode> &modes) { void HaierClimateBase::set_supported_swing_modes(climate::ClimateSwingModeMask modes) {
this->traits_.set_supported_swing_modes(modes); this->traits_.set_supported_swing_modes(modes);
if (!modes.empty()) if (!modes.empty())
this->traits_.add_supported_swing_mode(climate::CLIMATE_SWING_OFF); this->traits_.add_supported_swing_mode(climate::CLIMATE_SWING_OFF);
@@ -179,13 +179,13 @@ void HaierClimateBase::set_supported_swing_modes(const std::set<climate::Climate
void HaierClimateBase::set_answer_timeout(uint32_t timeout) { this->haier_protocol_.set_answer_timeout(timeout); } void HaierClimateBase::set_answer_timeout(uint32_t timeout) { this->haier_protocol_.set_answer_timeout(timeout); }
void HaierClimateBase::set_supported_modes(const std::set<climate::ClimateMode> &modes) { void HaierClimateBase::set_supported_modes(climate::ClimateModeMask modes) {
this->traits_.set_supported_modes(modes); this->traits_.set_supported_modes(modes);
this->traits_.add_supported_mode(climate::CLIMATE_MODE_OFF); // Always available this->traits_.add_supported_mode(climate::CLIMATE_MODE_OFF); // Always available
this->traits_.add_supported_mode(climate::CLIMATE_MODE_HEAT_COOL); // Always available this->traits_.add_supported_mode(climate::CLIMATE_MODE_HEAT_COOL); // Always available
} }
void HaierClimateBase::set_supported_presets(const std::set<climate::ClimatePreset> &presets) { void HaierClimateBase::set_supported_presets(climate::ClimatePresetMask presets) {
this->traits_.set_supported_presets(presets); this->traits_.set_supported_presets(presets);
if (!presets.empty()) if (!presets.empty())
this->traits_.add_supported_preset(climate::CLIMATE_PRESET_NONE); this->traits_.add_supported_preset(climate::CLIMATE_PRESET_NONE);

View File

@@ -1,7 +1,6 @@
#pragma once #pragma once
#include <chrono> #include <chrono>
#include <set>
#include "esphome/components/climate/climate.h" #include "esphome/components/climate/climate.h"
#include "esphome/components/uart/uart.h" #include "esphome/components/uart/uart.h"
#include "esphome/core/automation.h" #include "esphome/core/automation.h"
@@ -60,9 +59,9 @@ class HaierClimateBase : public esphome::Component,
void send_power_off_command(); void send_power_off_command();
void toggle_power(); void toggle_power();
void reset_protocol() { this->reset_protocol_request_ = true; }; void reset_protocol() { this->reset_protocol_request_ = true; };
void set_supported_modes(const std::set<esphome::climate::ClimateMode> &modes); void set_supported_modes(esphome::climate::ClimateModeMask modes);
void set_supported_swing_modes(const std::set<esphome::climate::ClimateSwingMode> &modes); void set_supported_swing_modes(esphome::climate::ClimateSwingModeMask modes);
void set_supported_presets(const std::set<esphome::climate::ClimatePreset> &presets); void set_supported_presets(esphome::climate::ClimatePresetMask presets);
bool valid_connection() const { return this->protocol_phase_ >= ProtocolPhases::IDLE; }; bool valid_connection() const { return this->protocol_phase_ >= ProtocolPhases::IDLE; };
size_t available() noexcept override { return esphome::uart::UARTDevice::available(); }; size_t available() noexcept override { return esphome::uart::UARTDevice::available(); };
size_t read_array(uint8_t *data, size_t len) noexcept override { size_t read_array(uint8_t *data, size_t len) noexcept override {

View File

@@ -1033,9 +1033,9 @@ haier_protocol::HandlerError HonClimate::process_status_message_(const uint8_t *
{ {
// Swing mode // Swing mode
ClimateSwingMode old_swing_mode = this->swing_mode; ClimateSwingMode old_swing_mode = this->swing_mode;
const std::set<ClimateSwingMode> &swing_modes = traits_.get_supported_swing_modes(); const auto &swing_modes = traits_.get_supported_swing_modes();
bool vertical_swing_supported = swing_modes.find(CLIMATE_SWING_VERTICAL) != swing_modes.end(); bool vertical_swing_supported = swing_modes.count(CLIMATE_SWING_VERTICAL);
bool horizontal_swing_supported = swing_modes.find(CLIMATE_SWING_HORIZONTAL) != swing_modes.end(); bool horizontal_swing_supported = swing_modes.count(CLIMATE_SWING_HORIZONTAL);
if (horizontal_swing_supported && if (horizontal_swing_supported &&
(packet.control.horizontal_swing_mode == (uint8_t) hon_protocol::HorizontalSwingMode::AUTO)) { (packet.control.horizontal_swing_mode == (uint8_t) hon_protocol::HorizontalSwingMode::AUTO)) {
if (vertical_swing_supported && if (vertical_swing_supported &&
@@ -1218,13 +1218,13 @@ void HonClimate::fill_control_messages_queue_() {
(uint8_t) hon_protocol::DataParameters::QUIET_MODE, (uint8_t) hon_protocol::DataParameters::QUIET_MODE,
quiet_mode_buf, 2); quiet_mode_buf, 2);
} }
if ((fast_mode_buf[1] != 0xFF) && ((presets.find(climate::ClimatePreset::CLIMATE_PRESET_BOOST) != presets.end()))) { if ((fast_mode_buf[1] != 0xFF) && presets.count(climate::ClimatePreset::CLIMATE_PRESET_BOOST)) {
this->control_messages_queue_.emplace(haier_protocol::FrameType::CONTROL, this->control_messages_queue_.emplace(haier_protocol::FrameType::CONTROL,
(uint16_t) hon_protocol::SubcommandsControl::SET_SINGLE_PARAMETER + (uint16_t) hon_protocol::SubcommandsControl::SET_SINGLE_PARAMETER +
(uint8_t) hon_protocol::DataParameters::FAST_MODE, (uint8_t) hon_protocol::DataParameters::FAST_MODE,
fast_mode_buf, 2); fast_mode_buf, 2);
} }
if ((away_mode_buf[1] != 0xFF) && ((presets.find(climate::ClimatePreset::CLIMATE_PRESET_AWAY) != presets.end()))) { if ((away_mode_buf[1] != 0xFF) && presets.count(climate::ClimatePreset::CLIMATE_PRESET_AWAY)) {
this->control_messages_queue_.emplace(haier_protocol::FrameType::CONTROL, this->control_messages_queue_.emplace(haier_protocol::FrameType::CONTROL,
(uint16_t) hon_protocol::SubcommandsControl::SET_SINGLE_PARAMETER + (uint16_t) hon_protocol::SubcommandsControl::SET_SINGLE_PARAMETER +
(uint8_t) hon_protocol::DataParameters::TEN_DEGREE, (uint8_t) hon_protocol::DataParameters::TEN_DEGREE,

View File

@@ -97,12 +97,11 @@ const float TEMP_MAX = 100; // Celsius
class HeatpumpIRClimate : public climate_ir::ClimateIR { class HeatpumpIRClimate : public climate_ir::ClimateIR {
public: public:
HeatpumpIRClimate() HeatpumpIRClimate()
: climate_ir::ClimateIR( : climate_ir::ClimateIR(TEMP_MIN, TEMP_MAX, 1.0f, true, true,
TEMP_MIN, TEMP_MAX, 1.0f, true, true, {climate::CLIMATE_FAN_LOW, climate::CLIMATE_FAN_MEDIUM, climate::CLIMATE_FAN_HIGH,
std::set<climate::ClimateFanMode>{climate::CLIMATE_FAN_LOW, climate::CLIMATE_FAN_MEDIUM, climate::CLIMATE_FAN_AUTO},
climate::CLIMATE_FAN_HIGH, climate::CLIMATE_FAN_AUTO}, {climate::CLIMATE_SWING_OFF, climate::CLIMATE_SWING_HORIZONTAL,
std::set<climate::ClimateSwingMode>{climate::CLIMATE_SWING_OFF, climate::CLIMATE_SWING_HORIZONTAL, climate::CLIMATE_SWING_VERTICAL, climate::CLIMATE_SWING_BOTH}) {}
climate::CLIMATE_SWING_VERTICAL, climate::CLIMATE_SWING_BOTH}) {}
void setup() override; void setup() override;
void set_protocol(Protocol protocol) { this->protocol_ = protocol; } void set_protocol(Protocol protocol) { this->protocol_ = protocol; }
void set_horizontal_default(HorizontalDirection horizontal_direction) { void set_horizontal_default(HorizontalDirection horizontal_direction) {

View File

@@ -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

View File

@@ -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

View File

@@ -12,7 +12,6 @@ from esphome.const import (
CONF_ON_ERROR, CONF_ON_ERROR,
CONF_ON_RESPONSE, CONF_ON_RESPONSE,
CONF_TIMEOUT, CONF_TIMEOUT,
CONF_TRIGGER_ID,
CONF_URL, CONF_URL,
CONF_WATCHDOG_TIMEOUT, CONF_WATCHDOG_TIMEOUT,
PLATFORM_HOST, PLATFORM_HOST,
@@ -216,16 +215,8 @@ HTTP_REQUEST_ACTION_SCHEMA = cv.Schema(
f"{CONF_VERIFY_SSL} has moved to the base component configuration." f"{CONF_VERIFY_SSL} has moved to the base component configuration."
), ),
cv.Optional(CONF_CAPTURE_RESPONSE, default=False): cv.boolean, cv.Optional(CONF_CAPTURE_RESPONSE, default=False): cv.boolean,
cv.Optional(CONF_ON_RESPONSE): automation.validate_automation( cv.Optional(CONF_ON_RESPONSE): automation.validate_automation(single=True),
{cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(HttpRequestResponseTrigger)} cv.Optional(CONF_ON_ERROR): automation.validate_automation(single=True),
),
cv.Optional(CONF_ON_ERROR): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
automation.Trigger.template()
)
}
),
cv.Optional(CONF_MAX_RESPONSE_BUFFER_SIZE, default="1kB"): cv.validate_bytes, cv.Optional(CONF_MAX_RESPONSE_BUFFER_SIZE, default="1kB"): cv.validate_bytes,
} }
) )
@@ -280,7 +271,12 @@ async def http_request_action_to_code(config, action_id, template_arg, args):
template_ = await cg.templatable(config[CONF_URL], args, cg.std_string) template_ = await cg.templatable(config[CONF_URL], args, cg.std_string)
cg.add(var.set_url(template_)) cg.add(var.set_url(template_))
cg.add(var.set_method(config[CONF_METHOD])) cg.add(var.set_method(config[CONF_METHOD]))
cg.add(var.set_capture_response(config[CONF_CAPTURE_RESPONSE]))
capture_response = config[CONF_CAPTURE_RESPONSE]
if capture_response:
cg.add(var.set_capture_response(capture_response))
cg.add_define("USE_HTTP_REQUEST_RESPONSE")
cg.add(var.set_max_response_buffer_size(config[CONF_MAX_RESPONSE_BUFFER_SIZE])) cg.add(var.set_max_response_buffer_size(config[CONF_MAX_RESPONSE_BUFFER_SIZE]))
if CONF_BODY in config: if CONF_BODY in config:
@@ -303,21 +299,26 @@ async def http_request_action_to_code(config, action_id, template_arg, args):
for value in config.get(CONF_COLLECT_HEADERS, []): for value in config.get(CONF_COLLECT_HEADERS, []):
cg.add(var.add_collect_header(value)) cg.add(var.add_collect_header(value))
for conf in config.get(CONF_ON_RESPONSE, []): if response_conf := config.get(CONF_ON_RESPONSE):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID]) if capture_response:
cg.add(var.register_response_trigger(trigger)) await automation.build_automation(
await automation.build_automation( var.get_success_trigger_with_response(),
trigger, [
[ (cg.std_shared_ptr.template(HttpContainer), "response"),
(cg.std_shared_ptr.template(HttpContainer), "response"), (cg.std_string_ref, "body"),
(cg.std_string_ref, "body"), *args,
], ],
conf, response_conf,
) )
for conf in config.get(CONF_ON_ERROR, []): else:
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID]) await automation.build_automation(
cg.add(var.register_error_trigger(trigger)) var.get_success_trigger(),
await automation.build_automation(trigger, [], conf) [(cg.std_shared_ptr.template(HttpContainer), "response"), *args],
response_conf,
)
if error_conf := config.get(CONF_ON_ERROR):
await automation.build_automation(var.get_error_trigger(), args, error_conf)
return var return var

View File

@@ -124,7 +124,7 @@ class HttpRequestComponent : public Component {
float get_setup_priority() const override { return setup_priority::AFTER_WIFI; } float get_setup_priority() const override { return setup_priority::AFTER_WIFI; }
void set_useragent(const char *useragent) { this->useragent_ = useragent; } void set_useragent(const char *useragent) { this->useragent_ = useragent; }
void set_timeout(uint16_t timeout) { this->timeout_ = timeout; } void set_timeout(uint32_t timeout) { this->timeout_ = timeout; }
void set_watchdog_timeout(uint32_t watchdog_timeout) { this->watchdog_timeout_ = watchdog_timeout; } void set_watchdog_timeout(uint32_t watchdog_timeout) { this->watchdog_timeout_ = watchdog_timeout; }
uint32_t get_watchdog_timeout() const { return this->watchdog_timeout_; } uint32_t get_watchdog_timeout() const { return this->watchdog_timeout_; }
void set_follow_redirects(bool follow_redirects) { this->follow_redirects_ = follow_redirects; } void set_follow_redirects(bool follow_redirects) { this->follow_redirects_ = follow_redirects; }
@@ -173,7 +173,7 @@ class HttpRequestComponent : public Component {
const char *useragent_{nullptr}; const char *useragent_{nullptr};
bool follow_redirects_{}; bool follow_redirects_{};
uint16_t redirect_limit_{}; uint16_t redirect_limit_{};
uint16_t timeout_{4500}; uint32_t timeout_{4500};
uint32_t watchdog_timeout_{0}; uint32_t watchdog_timeout_{0};
}; };
@@ -183,7 +183,9 @@ template<typename... Ts> class HttpRequestSendAction : public Action<Ts...> {
TEMPLATABLE_VALUE(std::string, url) TEMPLATABLE_VALUE(std::string, url)
TEMPLATABLE_VALUE(const char *, method) TEMPLATABLE_VALUE(const char *, method)
TEMPLATABLE_VALUE(std::string, body) TEMPLATABLE_VALUE(std::string, body)
#ifdef USE_HTTP_REQUEST_RESPONSE
TEMPLATABLE_VALUE(bool, capture_response) TEMPLATABLE_VALUE(bool, capture_response)
#endif
void add_request_header(const char *key, TemplatableValue<const char *, Ts...> value) { void add_request_header(const char *key, TemplatableValue<const char *, Ts...> value) {
this->request_headers_.insert({key, value}); this->request_headers_.insert({key, value});
@@ -195,9 +197,14 @@ template<typename... Ts> class HttpRequestSendAction : public Action<Ts...> {
void set_json(std::function<void(Ts..., JsonObject)> json_func) { this->json_func_ = json_func; } void set_json(std::function<void(Ts..., JsonObject)> json_func) { this->json_func_ = json_func; }
void register_response_trigger(HttpRequestResponseTrigger *trigger) { this->response_triggers_.push_back(trigger); } #ifdef USE_HTTP_REQUEST_RESPONSE
Trigger<std::shared_ptr<HttpContainer>, std::string &, Ts...> *get_success_trigger_with_response() const {
return this->success_trigger_with_response_;
}
#endif
Trigger<std::shared_ptr<HttpContainer>, Ts...> *get_success_trigger() const { return this->success_trigger_; }
void register_error_trigger(Trigger<> *trigger) { this->error_triggers_.push_back(trigger); } Trigger<Ts...> *get_error_trigger() const { return this->error_trigger_; }
void set_max_response_buffer_size(size_t max_response_buffer_size) { void set_max_response_buffer_size(size_t max_response_buffer_size) {
this->max_response_buffer_size_ = max_response_buffer_size; this->max_response_buffer_size_ = max_response_buffer_size;
@@ -228,17 +235,20 @@ template<typename... Ts> class HttpRequestSendAction : public Action<Ts...> {
auto container = this->parent_->start(this->url_.value(x...), this->method_.value(x...), body, request_headers, auto container = this->parent_->start(this->url_.value(x...), this->method_.value(x...), body, request_headers,
this->collect_headers_); this->collect_headers_);
auto captured_args = std::make_tuple(x...);
if (container == nullptr) { if (container == nullptr) {
for (auto *trigger : this->error_triggers_) std::apply([this](Ts... captured_args_inner) { this->error_trigger_->trigger(captured_args_inner...); },
trigger->trigger(); captured_args);
return; return;
} }
size_t content_length = container->content_length; size_t content_length = container->content_length;
size_t max_length = std::min(content_length, this->max_response_buffer_size_); size_t max_length = std::min(content_length, this->max_response_buffer_size_);
std::string response_body; #ifdef USE_HTTP_REQUEST_RESPONSE
if (this->capture_response_.value(x...)) { if (this->capture_response_.value(x...)) {
std::string response_body;
RAMAllocator<uint8_t> allocator; RAMAllocator<uint8_t> allocator;
uint8_t *buf = allocator.allocate(max_length); uint8_t *buf = allocator.allocate(max_length);
if (buf != nullptr) { if (buf != nullptr) {
@@ -253,19 +263,17 @@ template<typename... Ts> class HttpRequestSendAction : public Action<Ts...> {
response_body.assign((char *) buf, read_index); response_body.assign((char *) buf, read_index);
allocator.deallocate(buf, max_length); allocator.deallocate(buf, max_length);
} }
} std::apply(
[this, &container, &response_body](Ts... captured_args_inner) {
if (this->response_triggers_.size() == 1) { this->success_trigger_with_response_->trigger(container, response_body, captured_args_inner...);
// if there is only one trigger, no need to copy the response body },
this->response_triggers_[0]->process(container, response_body); captured_args);
} else { } else
for (auto *trigger : this->response_triggers_) { #endif
// with multiple triggers, pass a copy of the response body to each {
// one so that modifications made in one trigger are not visible to std::apply([this, &container](
// the others Ts... captured_args_inner) { this->success_trigger_->trigger(container, captured_args_inner...); },
auto response_body_copy = std::string(response_body); captured_args);
trigger->process(container, response_body_copy);
}
} }
container->end(); container->end();
} }
@@ -283,8 +291,13 @@ template<typename... Ts> class HttpRequestSendAction : public Action<Ts...> {
std::set<std::string> collect_headers_{"content-type", "content-length"}; std::set<std::string> collect_headers_{"content-type", "content-length"};
std::map<const char *, TemplatableValue<std::string, Ts...>> json_{}; std::map<const char *, TemplatableValue<std::string, Ts...>> json_{};
std::function<void(Ts..., JsonObject)> json_func_{nullptr}; std::function<void(Ts..., JsonObject)> json_func_{nullptr};
std::vector<HttpRequestResponseTrigger *> response_triggers_{}; #ifdef USE_HTTP_REQUEST_RESPONSE
std::vector<Trigger<> *> error_triggers_{}; Trigger<std::shared_ptr<HttpContainer>, std::string &, Ts...> *success_trigger_with_response_ =
new Trigger<std::shared_ptr<HttpContainer>, std::string &, Ts...>();
#endif
Trigger<std::shared_ptr<HttpContainer>, Ts...> *success_trigger_ =
new Trigger<std::shared_ptr<HttpContainer>, Ts...>();
Trigger<Ts...> *error_trigger_ = new Trigger<Ts...>();
size_t max_response_buffer_size_{SIZE_MAX}; size_t max_response_buffer_size_{SIZE_MAX};
}; };

View File

@@ -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

View File

@@ -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

View File

@@ -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 &current_color) override { void apply(AddressableLight &it, const Color &current_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_;

View File

@@ -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_;

View File

@@ -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 (
@@ -173,14 +173,34 @@ def uart_selection(value):
raise NotImplementedError raise NotImplementedError
def validate_local_no_higher_than_global(value): def validate_local_no_higher_than_global(config):
global_level = LOG_LEVEL_SEVERITY.index(value[CONF_LEVEL]) global_level = config[CONF_LEVEL]
for tag, level in value.get(CONF_LOGS, {}).items(): global_level_index = LOG_LEVEL_SEVERITY.index(global_level)
if LOG_LEVEL_SEVERITY.index(level) > global_level: errs = []
raise cv.Invalid( for tag, level in config.get(CONF_LOGS, {}).items():
f"The configured log level for {tag} ({level}) must be no more severe than the global log level {value[CONF_LEVEL]}." if LOG_LEVEL_SEVERITY.index(level) > global_level_index:
errs.append(
cv.Invalid(
f"The configured log level for {tag} ({level}) must not be less severe than the global log level ({global_level})",
[CONF_LOGS, tag],
)
) )
return value if errs:
raise cv.MultipleInvalid(errs)
return config
def validate_initial_no_higher_than_global(config):
if initial_level := config.get(CONF_INITIAL_LEVEL):
global_level = config[CONF_LEVEL]
if LOG_LEVEL_SEVERITY.index(initial_level) > LOG_LEVEL_SEVERITY.index(
global_level
):
raise cv.Invalid(
f"The initial log level ({initial_level}) must not be less severe than the global log level ({global_level})",
[CONF_INITIAL_LEVEL],
)
return config
Logger = logger_ns.class_("Logger", cg.Component) Logger = logger_ns.class_("Logger", cg.Component)
@@ -263,6 +283,7 @@ CONFIG_SCHEMA = cv.All(
} }
).extend(cv.COMPONENT_SCHEMA), ).extend(cv.COMPONENT_SCHEMA),
validate_local_no_higher_than_global, validate_local_no_higher_than_global,
validate_initial_no_higher_than_global,
) )
@@ -430,7 +451,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 +478,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(

View File

@@ -358,7 +358,7 @@ class LvSelectable : public LvCompound {
virtual void set_selected_index(size_t index, lv_anim_enable_t anim) = 0; virtual void set_selected_index(size_t index, lv_anim_enable_t anim) = 0;
void set_selected_text(const std::string &text, lv_anim_enable_t anim); void set_selected_text(const std::string &text, lv_anim_enable_t anim);
std::string get_selected_text(); std::string get_selected_text();
std::vector<std::string> get_options() { return this->options_; } const std::vector<std::string> &get_options() { return this->options_; }
void set_options(std::vector<std::string> options); void set_options(std::vector<std::string> options);
protected: protected:

View File

@@ -53,7 +53,17 @@ class LVGLSelect : public select::Select, public Component {
this->widget_->set_selected_text(value, this->anim_); this->widget_->set_selected_text(value, this->anim_);
this->publish(); this->publish();
} }
void set_options_() { this->traits.set_options(this->widget_->get_options()); } void set_options_() {
// Widget uses std::vector<std::string>, SelectTraits uses FixedVector<const char*>
// Convert by extracting c_str() pointers
const auto &opts = this->widget_->get_options();
FixedVector<const char *> opt_ptrs;
opt_ptrs.init(opts.size());
for (size_t i = 0; i < opts.size(); i++) {
opt_ptrs[i] = opts[i].c_str();
}
this->traits.set_options(opt_ptrs);
}
LvSelectable *widget_; LvSelectable *widget_;
lv_anim_enable_t anim_; lv_anim_enable_t anim_;

View File

@@ -19,6 +19,9 @@ using climate::ClimateTraits;
using climate::ClimateMode; using climate::ClimateMode;
using climate::ClimateSwingMode; using climate::ClimateSwingMode;
using climate::ClimateFanMode; using climate::ClimateFanMode;
using climate::ClimateModeMask;
using climate::ClimateSwingModeMask;
using climate::ClimatePresetMask;
class AirConditioner : public ApplianceBase<dudanov::midea::ac::AirConditioner>, public climate::Climate { class AirConditioner : public ApplianceBase<dudanov::midea::ac::AirConditioner>, public climate::Climate {
public: public:
@@ -40,20 +43,20 @@ class AirConditioner : public ApplianceBase<dudanov::midea::ac::AirConditioner>,
void do_power_on() { this->base_.setPowerState(true); } void do_power_on() { this->base_.setPowerState(true); }
void do_power_off() { this->base_.setPowerState(false); } void do_power_off() { this->base_.setPowerState(false); }
void do_power_toggle() { this->base_.setPowerState(this->mode == ClimateMode::CLIMATE_MODE_OFF); } void do_power_toggle() { this->base_.setPowerState(this->mode == ClimateMode::CLIMATE_MODE_OFF); }
void set_supported_modes(const std::set<ClimateMode> &modes) { this->supported_modes_ = modes; } void set_supported_modes(ClimateModeMask modes) { this->supported_modes_ = modes; }
void set_supported_swing_modes(const std::set<ClimateSwingMode> &modes) { this->supported_swing_modes_ = modes; } void set_supported_swing_modes(ClimateSwingModeMask modes) { this->supported_swing_modes_ = modes; }
void set_supported_presets(const std::set<ClimatePreset> &presets) { this->supported_presets_ = presets; } void set_supported_presets(ClimatePresetMask presets) { this->supported_presets_ = presets; }
void set_custom_presets(const std::set<std::string> &presets) { this->supported_custom_presets_ = presets; } void set_custom_presets(const std::vector<std::string> &presets) { this->supported_custom_presets_ = presets; }
void set_custom_fan_modes(const std::set<std::string> &modes) { this->supported_custom_fan_modes_ = modes; } void set_custom_fan_modes(const std::vector<std::string> &modes) { this->supported_custom_fan_modes_ = modes; }
protected: protected:
void control(const ClimateCall &call) override; void control(const ClimateCall &call) override;
ClimateTraits traits() override; ClimateTraits traits() override;
std::set<ClimateMode> supported_modes_{}; ClimateModeMask supported_modes_{};
std::set<ClimateSwingMode> supported_swing_modes_{}; ClimateSwingModeMask supported_swing_modes_{};
std::set<ClimatePreset> supported_presets_{}; ClimatePresetMask supported_presets_{};
std::set<std::string> supported_custom_presets_{}; std::vector<std::string> supported_custom_presets_{};
std::set<std::string> supported_custom_fan_modes_{}; std::vector<std::string> supported_custom_fan_modes_{};
Sensor *outdoor_sensor_{nullptr}; Sensor *outdoor_sensor_{nullptr};
Sensor *humidity_sensor_{nullptr}; Sensor *humidity_sensor_{nullptr};
Sensor *power_sensor_{nullptr}; Sensor *power_sensor_{nullptr};

View File

@@ -384,6 +384,18 @@ class DriverChip:
transform[CONF_TRANSFORM] = True transform[CONF_TRANSFORM] = True
return transform return transform
def swap_xy_schema(self):
uses_swap = self.get_default(CONF_SWAP_XY, None) != cv.UNDEFINED
def validator(value):
if value:
raise cv.Invalid("Axis swapping not supported by this model")
return cv.boolean(value)
if uses_swap:
return {cv.Required(CONF_SWAP_XY): cv.boolean}
return {cv.Optional(CONF_SWAP_XY, default=False): validator}
def add_madctl(self, sequence: list, config: dict): def add_madctl(self, sequence: list, config: dict):
# Add the MADCTL command to the sequence based on the configuration. # Add the MADCTL command to the sequence based on the configuration.
use_flip = config.get(CONF_USE_AXIS_FLIPS) use_flip = config.get(CONF_USE_AXIS_FLIPS)

View File

@@ -46,6 +46,7 @@ from esphome.const import (
CONF_DATA_RATE, CONF_DATA_RATE,
CONF_DC_PIN, CONF_DC_PIN,
CONF_DIMENSIONS, CONF_DIMENSIONS,
CONF_DISABLED,
CONF_ENABLE_PIN, CONF_ENABLE_PIN,
CONF_GREEN, CONF_GREEN,
CONF_HSYNC_PIN, CONF_HSYNC_PIN,
@@ -117,16 +118,16 @@ def data_pin_set(length):
def model_schema(config): def model_schema(config):
model = MODELS[config[CONF_MODEL].upper()] model = MODELS[config[CONF_MODEL].upper()]
if transforms := model.transforms: transform = cv.Any(
transform = cv.Schema({cv.Required(x): cv.boolean for x in transforms}) cv.Schema(
for x in (CONF_SWAP_XY, CONF_MIRROR_X, CONF_MIRROR_Y): {
if x not in transforms: cv.Required(CONF_MIRROR_X): cv.boolean,
transform = transform.extend( cv.Required(CONF_MIRROR_Y): cv.boolean,
{cv.Optional(x): cv.invalid(f"{x} not supported by this model")} **model.swap_xy_schema(),
) }
else: ),
transform = cv.invalid("This model does not support transforms") cv.one_of(CONF_DISABLED, lower=True),
)
# RPI model does not use an init sequence, indicates with empty list # RPI model does not use an init sequence, indicates with empty list
if model.initsequence is None: if model.initsequence is None:
# Custom model requires an init sequence # Custom model requires an init sequence
@@ -135,12 +136,16 @@ def model_schema(config):
else: else:
iseqconf = cv.Optional(CONF_INIT_SEQUENCE) iseqconf = cv.Optional(CONF_INIT_SEQUENCE)
uses_spi = CONF_INIT_SEQUENCE in config or len(model.initsequence) != 0 uses_spi = CONF_INIT_SEQUENCE in config or len(model.initsequence) != 0
swap_xy = config.get(CONF_TRANSFORM, {}).get(CONF_SWAP_XY, False) # Dimensions are optional if the model has a default width and the x-y transform is not overridden
transform_config = config.get(CONF_TRANSFORM, {})
# Dimensions are optional if the model has a default width and the swap_xy transform is not overridden is_swapped = (
cv_dimensions = ( isinstance(transform_config, dict)
cv.Optional if model.get_default(CONF_WIDTH) and not swap_xy else cv.Required and transform_config.get(CONF_SWAP_XY, False) is True
) )
cv_dimensions = (
cv.Optional if model.get_default(CONF_WIDTH) and not is_swapped else cv.Required
)
pixel_modes = (PIXEL_MODE_16BIT, PIXEL_MODE_18BIT, "16", "18") pixel_modes = (PIXEL_MODE_16BIT, PIXEL_MODE_18BIT, "16", "18")
schema = display.FULL_DISPLAY_SCHEMA.extend( schema = display.FULL_DISPLAY_SCHEMA.extend(
{ {
@@ -157,7 +162,7 @@ def model_schema(config):
model.option(CONF_PIXEL_MODE, PIXEL_MODE_16BIT): cv.one_of( model.option(CONF_PIXEL_MODE, PIXEL_MODE_16BIT): cv.one_of(
*pixel_modes, lower=True *pixel_modes, lower=True
), ),
model.option(CONF_TRANSFORM, cv.UNDEFINED): transform, cv.Optional(CONF_TRANSFORM): transform,
cv.Required(CONF_MODEL): cv.one_of(model.name, upper=True), cv.Required(CONF_MODEL): cv.one_of(model.name, upper=True),
model.option(CONF_INVERT_COLORS, False): cv.boolean, model.option(CONF_INVERT_COLORS, False): cv.boolean,
model.option(CONF_USE_AXIS_FLIPS, True): cv.boolean, model.option(CONF_USE_AXIS_FLIPS, True): cv.boolean,
@@ -270,7 +275,6 @@ async def to_code(config):
cg.add(var.set_vsync_front_porch(config[CONF_VSYNC_FRONT_PORCH])) cg.add(var.set_vsync_front_porch(config[CONF_VSYNC_FRONT_PORCH]))
cg.add(var.set_pclk_inverted(config[CONF_PCLK_INVERTED])) cg.add(var.set_pclk_inverted(config[CONF_PCLK_INVERTED]))
cg.add(var.set_pclk_frequency(config[CONF_PCLK_FREQUENCY])) cg.add(var.set_pclk_frequency(config[CONF_PCLK_FREQUENCY]))
index = 0
dpins = [] dpins = []
if CONF_RED in config[CONF_DATA_PINS]: if CONF_RED in config[CONF_DATA_PINS]:
red_pins = config[CONF_DATA_PINS][CONF_RED] red_pins = config[CONF_DATA_PINS][CONF_RED]

View File

@@ -131,19 +131,6 @@ def denominator(config):
) from StopIteration ) from StopIteration
def swap_xy_schema(model):
uses_swap = model.get_default(CONF_SWAP_XY, None) != cv.UNDEFINED
def validator(value):
if value:
raise cv.Invalid("Axis swapping not supported by this model")
return cv.boolean(value)
if uses_swap:
return {cv.Required(CONF_SWAP_XY): cv.boolean}
return {cv.Optional(CONF_SWAP_XY, default=False): validator}
def model_schema(config): def model_schema(config):
model = MODELS[config[CONF_MODEL]] model = MODELS[config[CONF_MODEL]]
bus_mode = config[CONF_BUS_MODE] bus_mode = config[CONF_BUS_MODE]
@@ -152,7 +139,7 @@ def model_schema(config):
{ {
cv.Required(CONF_MIRROR_X): cv.boolean, cv.Required(CONF_MIRROR_X): cv.boolean,
cv.Required(CONF_MIRROR_Y): cv.boolean, cv.Required(CONF_MIRROR_Y): cv.boolean,
**swap_xy_schema(model), **model.swap_xy_schema(),
} }
), ),
cv.one_of(CONF_DISABLED, lower=True), cv.one_of(CONF_DISABLED, lower=True),

View File

@@ -33,8 +33,8 @@ class ModbusBinarySensor : public Component, public binary_sensor::BinarySensor,
void dump_config() override; void dump_config() override;
using transform_func_t = std::function<optional<bool>(ModbusBinarySensor *, bool, const std::vector<uint8_t> &)>; using transform_func_t = optional<bool> (*)(ModbusBinarySensor *, bool, const std::vector<uint8_t> &);
void set_template(transform_func_t &&f) { this->transform_func_ = f; } void set_template(transform_func_t f) { this->transform_func_ = f; }
protected: protected:
optional<transform_func_t> transform_func_{nullopt}; optional<transform_func_t> transform_func_{nullopt};

View File

@@ -31,10 +31,10 @@ class ModbusNumber : public number::Number, public Component, public SensorItem
void set_parent(ModbusController *parent) { this->parent_ = parent; } void set_parent(ModbusController *parent) { this->parent_ = parent; }
void set_write_multiply(float factor) { this->multiply_by_ = factor; } void set_write_multiply(float factor) { this->multiply_by_ = factor; }
using transform_func_t = std::function<optional<float>(ModbusNumber *, float, const std::vector<uint8_t> &)>; using transform_func_t = optional<float> (*)(ModbusNumber *, float, const std::vector<uint8_t> &);
using write_transform_func_t = std::function<optional<float>(ModbusNumber *, float, std::vector<uint16_t> &)>; using write_transform_func_t = optional<float> (*)(ModbusNumber *, float, std::vector<uint16_t> &);
void set_template(transform_func_t &&f) { this->transform_func_ = f; } void set_template(transform_func_t f) { this->transform_func_ = f; }
void set_write_template(write_transform_func_t &&f) { this->write_transform_func_ = f; } void set_write_template(write_transform_func_t f) { this->write_transform_func_ = f; }
void set_use_write_mutiple(bool use_write_multiple) { this->use_write_multiple_ = use_write_multiple; } void set_use_write_mutiple(bool use_write_multiple) { this->use_write_multiple_ = use_write_multiple; }
protected: protected:

View File

@@ -29,8 +29,8 @@ class ModbusFloatOutput : public output::FloatOutput, public Component, public S
// Do nothing // Do nothing
void parse_and_publish(const std::vector<uint8_t> &data) override{}; void parse_and_publish(const std::vector<uint8_t> &data) override{};
using write_transform_func_t = std::function<optional<float>(ModbusFloatOutput *, float, std::vector<uint16_t> &)>; using write_transform_func_t = optional<float> (*)(ModbusFloatOutput *, float, std::vector<uint16_t> &);
void set_write_template(write_transform_func_t &&f) { this->write_transform_func_ = f; } void set_write_template(write_transform_func_t f) { this->write_transform_func_ = f; }
void set_use_write_mutiple(bool use_write_multiple) { this->use_write_multiple_ = use_write_multiple; } void set_use_write_mutiple(bool use_write_multiple) { this->use_write_multiple_ = use_write_multiple; }
protected: protected:
@@ -60,8 +60,8 @@ class ModbusBinaryOutput : public output::BinaryOutput, public Component, public
// Do nothing // Do nothing
void parse_and_publish(const std::vector<uint8_t> &data) override{}; void parse_and_publish(const std::vector<uint8_t> &data) override{};
using write_transform_func_t = std::function<optional<bool>(ModbusBinaryOutput *, bool, std::vector<uint8_t> &)>; using write_transform_func_t = optional<bool> (*)(ModbusBinaryOutput *, bool, std::vector<uint8_t> &);
void set_write_template(write_transform_func_t &&f) { this->write_transform_func_ = f; } void set_write_template(write_transform_func_t f) { this->write_transform_func_ = f; }
void set_use_write_mutiple(bool use_write_multiple) { this->use_write_multiple_ = use_write_multiple; } void set_use_write_mutiple(bool use_write_multiple) { this->use_write_multiple_ = use_write_multiple; }
protected: protected:

View File

@@ -28,7 +28,7 @@ void ModbusSelect::parse_and_publish(const std::vector<uint8_t> &data) {
if (map_it != this->mapping_.cend()) { if (map_it != this->mapping_.cend()) {
size_t idx = std::distance(this->mapping_.cbegin(), map_it); size_t idx = std::distance(this->mapping_.cbegin(), map_it);
new_state = this->traits.get_options()[idx]; new_state = std::string(this->option_at(idx));
ESP_LOGV(TAG, "Found option %s for value %lld", new_state->c_str(), value); ESP_LOGV(TAG, "Found option %s for value %lld", new_state->c_str(), value);
} else { } else {
ESP_LOGE(TAG, "No option found for mapping %lld", value); ESP_LOGE(TAG, "No option found for mapping %lld", value);
@@ -41,10 +41,12 @@ void ModbusSelect::parse_and_publish(const std::vector<uint8_t> &data) {
} }
void ModbusSelect::control(const std::string &value) { void ModbusSelect::control(const std::string &value) {
auto options = this->traits.get_options(); auto idx = this->index_of(value);
auto opt_it = std::find(options.cbegin(), options.cend(), value); if (!idx.has_value()) {
size_t idx = std::distance(options.cbegin(), opt_it); ESP_LOGW(TAG, "Invalid option '%s'", value.c_str());
optional<int64_t> mapval = this->mapping_[idx]; return;
}
optional<int64_t> mapval = this->mapping_[idx.value()];
ESP_LOGD(TAG, "Found value %lld for option '%s'", *mapval, value.c_str()); ESP_LOGD(TAG, "Found value %lld for option '%s'", *mapval, value.c_str());
std::vector<uint16_t> data; std::vector<uint16_t> data;

View File

@@ -26,16 +26,15 @@ class ModbusSelect : public Component, public select::Select, public SensorItem
this->mapping_ = std::move(mapping); this->mapping_ = std::move(mapping);
} }
using transform_func_t = using transform_func_t = optional<std::string> (*)(ModbusSelect *const, int64_t, const std::vector<uint8_t> &);
std::function<optional<std::string>(ModbusSelect *const, int64_t, const std::vector<uint8_t> &)>; using write_transform_func_t = optional<int64_t> (*)(ModbusSelect *const, const std::string &, int64_t,
using write_transform_func_t = std::vector<uint16_t> &);
std::function<optional<int64_t>(ModbusSelect *const, const std::string &, int64_t, std::vector<uint16_t> &)>;
void set_parent(ModbusController *const parent) { this->parent_ = parent; } void set_parent(ModbusController *const parent) { this->parent_ = parent; }
void set_use_write_mutiple(bool use_write_multiple) { this->use_write_multiple_ = use_write_multiple; } void set_use_write_mutiple(bool use_write_multiple) { this->use_write_multiple_ = use_write_multiple; }
void set_optimistic(bool optimistic) { this->optimistic_ = optimistic; } void set_optimistic(bool optimistic) { this->optimistic_ = optimistic; }
void set_template(transform_func_t &&f) { this->transform_func_ = f; } void set_template(transform_func_t f) { this->transform_func_ = f; }
void set_write_template(write_transform_func_t &&f) { this->write_transform_func_ = f; } void set_write_template(write_transform_func_t f) { this->write_transform_func_ = f; }
void dump_config() override; void dump_config() override;
void parse_and_publish(const std::vector<uint8_t> &data) override; void parse_and_publish(const std::vector<uint8_t> &data) override;

View File

@@ -25,9 +25,9 @@ class ModbusSensor : public Component, public sensor::Sensor, public SensorItem
void parse_and_publish(const std::vector<uint8_t> &data) override; void parse_and_publish(const std::vector<uint8_t> &data) override;
void dump_config() override; void dump_config() override;
using transform_func_t = std::function<optional<float>(ModbusSensor *, float, const std::vector<uint8_t> &)>; using transform_func_t = optional<float> (*)(ModbusSensor *, float, const std::vector<uint8_t> &);
void set_template(transform_func_t &&f) { this->transform_func_ = f; } void set_template(transform_func_t f) { this->transform_func_ = f; }
protected: protected:
optional<transform_func_t> transform_func_{nullopt}; optional<transform_func_t> transform_func_{nullopt};

View File

@@ -34,10 +34,10 @@ class ModbusSwitch : public Component, public switch_::Switch, public SensorItem
void parse_and_publish(const std::vector<uint8_t> &data) override; void parse_and_publish(const std::vector<uint8_t> &data) override;
void set_parent(ModbusController *parent) { this->parent_ = parent; } void set_parent(ModbusController *parent) { this->parent_ = parent; }
using transform_func_t = std::function<optional<bool>(ModbusSwitch *, bool, const std::vector<uint8_t> &)>; using transform_func_t = optional<bool> (*)(ModbusSwitch *, bool, const std::vector<uint8_t> &);
using write_transform_func_t = std::function<optional<bool>(ModbusSwitch *, bool, std::vector<uint8_t> &)>; using write_transform_func_t = optional<bool> (*)(ModbusSwitch *, bool, std::vector<uint8_t> &);
void set_template(transform_func_t &&f) { this->publish_transform_func_ = f; } void set_template(transform_func_t f) { this->publish_transform_func_ = f; }
void set_write_template(write_transform_func_t &&f) { this->write_transform_func_ = f; } void set_write_template(write_transform_func_t f) { this->write_transform_func_ = f; }
void set_use_write_mutiple(bool use_write_multiple) { this->use_write_multiple_ = use_write_multiple; } void set_use_write_mutiple(bool use_write_multiple) { this->use_write_multiple_ = use_write_multiple; }
protected: protected:

View File

@@ -30,9 +30,8 @@ class ModbusTextSensor : public Component, public text_sensor::TextSensor, publi
void dump_config() override; void dump_config() override;
void parse_and_publish(const std::vector<uint8_t> &data) override; void parse_and_publish(const std::vector<uint8_t> &data) override;
using transform_func_t = using transform_func_t = optional<std::string> (*)(ModbusTextSensor *, std::string, const std::vector<uint8_t> &);
std::function<optional<std::string>(ModbusTextSensor *, std::string, const std::vector<uint8_t> &)>; void set_template(transform_func_t f) { this->transform_func_ = f; }
void set_template(transform_func_t &&f) { this->transform_func_ = f; }
protected: protected:
optional<transform_func_t> transform_func_{nullopt}; optional<transform_func_t> transform_func_{nullopt};

View File

@@ -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(

View File

@@ -540,6 +540,23 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe
*/ */
void goto_page(uint8_t page); void goto_page(uint8_t page);
/**
* Set the visibility of a component.
*
* @param component The component name.
* @param show True to show the component, false to hide it.
*
* @see show_component()
* @see hide_component()
*
* Example:
* ```cpp
* it.set_component_visibility("textview", true); // Equivalent to show_component("textview")
* it.set_component_visibility("textview", false); // Equivalent to hide_component("textview")
* ```
*/
void set_component_visibility(const char *component, bool show) override;
/** /**
* Hide a component. * Hide a component.
* @param component The component name. * @param component The component name.

View File

@@ -45,6 +45,7 @@ class NextionBase {
virtual void set_component_pressed_font_color(const char *component, Color color) = 0; virtual void set_component_pressed_font_color(const char *component, Color color) = 0;
virtual void set_component_font(const char *component, uint8_t font_id) = 0; virtual void set_component_font(const char *component, uint8_t font_id) = 0;
virtual void set_component_visibility(const char *component, bool show) = 0;
virtual void show_component(const char *component) = 0; virtual void show_component(const char *component) = 0;
virtual void hide_component(const char *component) = 0; virtual void hide_component(const char *component) = 0;

View File

@@ -201,13 +201,13 @@ void Nextion::set_component_font(const char *component, uint8_t font_id) {
this->add_no_result_to_queue_with_printf_("set_component_font", "%s.font=%" PRIu8, component, font_id); this->add_no_result_to_queue_with_printf_("set_component_font", "%s.font=%" PRIu8, component, font_id);
} }
void Nextion::hide_component(const char *component) { void Nextion::set_component_visibility(const char *component, bool show) {
this->add_no_result_to_queue_with_printf_("hide_component", "vis %s,0", component); this->add_no_result_to_queue_with_printf_("set_component_visibility", "vis %s,%d", component, show ? 1 : 0);
} }
void Nextion::show_component(const char *component) { void Nextion::hide_component(const char *component) { this->set_component_visibility(component, false); }
this->add_no_result_to_queue_with_printf_("show_component", "vis %s,1", component);
} void Nextion::show_component(const char *component) { this->set_component_visibility(component, true); }
void Nextion::enable_component_touch(const char *component) { void Nextion::enable_component_touch(const char *component) {
this->add_no_result_to_queue_with_printf_("enable_component_touch", "tsw %s,1", component); this->add_no_result_to_queue_with_printf_("enable_component_touch", "tsw %s,1", component);

View File

@@ -81,13 +81,11 @@ void NextionComponent::update_component_settings(bool force_update) {
this->component_flags_.visible_needs_update = false; this->component_flags_.visible_needs_update = false;
if (this->component_flags_.visible) { this->nextion_->set_component_visibility(name_to_send.c_str(), this->component_flags_.visible);
this->nextion_->show_component(name_to_send.c_str()); if (!this->component_flags_.visible) {
this->send_state_to_nextion();
} else {
this->nextion_->hide_component(name_to_send.c_str());
return; return;
} }
this->send_state_to_nextion();
} }
if (this->component_flags_.bco_needs_update || (force_update && this->component_flags_.bco2_is_set)) { if (this->component_flags_.bco_needs_update || (force_update && this->component_flags_.bco2_is_set)) {

View File

@@ -174,11 +174,6 @@ bool Nextion::upload_tft(uint32_t baud_rate, bool exit_reparse) {
// Check if baud rate is supported // Check if baud rate is supported
this->original_baud_rate_ = this->parent_->get_baud_rate(); this->original_baud_rate_ = this->parent_->get_baud_rate();
static const std::vector<uint32_t> SUPPORTED_BAUD_RATES = {2400, 4800, 9600, 19200, 31250, 38400, 57600,
115200, 230400, 250000, 256000, 512000, 921600};
if (std::find(SUPPORTED_BAUD_RATES.begin(), SUPPORTED_BAUD_RATES.end(), baud_rate) == SUPPORTED_BAUD_RATES.end()) {
baud_rate = this->original_baud_rate_;
}
ESP_LOGD(TAG, "Baud rate: %" PRIu32, baud_rate); ESP_LOGD(TAG, "Baud rate: %" PRIu32, baud_rate);
// Define the configuration for the HTTP client // Define the configuration for the HTTP client

View File

@@ -177,11 +177,6 @@ bool Nextion::upload_tft(uint32_t baud_rate, bool exit_reparse) {
// Check if baud rate is supported // Check if baud rate is supported
this->original_baud_rate_ = this->parent_->get_baud_rate(); this->original_baud_rate_ = this->parent_->get_baud_rate();
static const std::vector<uint32_t> SUPPORTED_BAUD_RATES = {2400, 4800, 9600, 19200, 31250, 38400, 57600,
115200, 230400, 250000, 256000, 512000, 921600};
if (std::find(SUPPORTED_BAUD_RATES.begin(), SUPPORTED_BAUD_RATES.end(), baud_rate) == SUPPORTED_BAUD_RATES.end()) {
baud_rate = this->original_baud_rate_;
}
ESP_LOGD(TAG, "Baud rate: %" PRIu32, baud_rate); ESP_LOGD(TAG, "Baud rate: %" PRIu32, baud_rate);
// Define the configuration for the HTTP client // Define the configuration for the HTTP client

View File

@@ -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

View File

@@ -252,7 +252,10 @@ async def setup_number_core_(
cg.add(var.traits.set_max_value(max_value)) cg.add(var.traits.set_max_value(max_value))
cg.add(var.traits.set_step(step)) cg.add(var.traits.set_step(step))
cg.add(var.traits.set_mode(config[CONF_MODE])) # Only set if non-default to avoid bloating setup() function
# (mode_ is initialized to NUMBER_MODE_AUTO in the header)
if config[CONF_MODE] != NumberMode.NUMBER_MODE_AUTO:
cg.add(var.traits.set_mode(config[CONF_MODE]))
for conf in config.get(CONF_ON_VALUE, []): for conf in config.get(CONF_ON_VALUE, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)

View File

@@ -102,7 +102,7 @@ CONFIG_SCHEMA = cv.Any(
str: PACKAGE_SCHEMA, str: PACKAGE_SCHEMA,
} }
), ),
cv.ensure_list(PACKAGE_SCHEMA), [PACKAGE_SCHEMA],
) )

View File

@@ -17,6 +17,7 @@ from esphome.const import (
CONF_FAMILY, CONF_FAMILY,
CONF_GROUP, CONF_GROUP,
CONF_ID, CONF_ID,
CONF_INDEX,
CONF_INVERTED, CONF_INVERTED,
CONF_LEVEL, CONF_LEVEL,
CONF_MAGNITUDE, CONF_MAGNITUDE,
@@ -616,6 +617,49 @@ async def dooya_action(var, config, args):
cg.add(var.set_check(template_)) cg.add(var.set_check(template_))
# Dyson
DysonData, DysonBinarySensor, DysonTrigger, DysonAction, DysonDumper = declare_protocol(
"Dyson"
)
DYSON_SCHEMA = cv.Schema(
{
cv.Required(CONF_CODE): cv.hex_uint16_t,
cv.Optional(CONF_INDEX, default=0xFF): cv.hex_uint8_t,
}
)
@register_binary_sensor("dyson", DysonBinarySensor, DYSON_SCHEMA)
def dyson_binary_sensor(var, config):
cg.add(
var.set_data(
cg.StructInitializer(
DysonData,
("code", config[CONF_CODE]),
("index", config[CONF_INDEX]),
)
)
)
@register_trigger("dyson", DysonTrigger, DysonData)
def dyson_trigger(var, config):
pass
@register_dumper("dyson", DysonDumper)
def dyson_dumper(var, config):
pass
@register_action("dyson", DysonAction, DYSON_SCHEMA)
async def dyson_action(var, config, args):
template_ = await cg.templatable(config[CONF_CODE], args, cg.uint16)
cg.add(var.set_code(template_))
template_ = await cg.templatable(config[CONF_INDEX], args, cg.uint8)
cg.add(var.set_index(template_))
# JVC # JVC
JVCData, JVCBinarySensor, JVCTrigger, JVCAction, JVCDumper = declare_protocol("JVC") JVCData, JVCBinarySensor, JVCTrigger, JVCAction, JVCDumper = declare_protocol("JVC")
JVC_SCHEMA = cv.Schema({cv.Required(CONF_DATA): cv.hex_uint32_t}) JVC_SCHEMA = cv.Schema({cv.Required(CONF_DATA): cv.hex_uint32_t})

View File

@@ -0,0 +1,71 @@
#include "dyson_protocol.h"
#include "esphome/core/log.h"
#include <cinttypes>
namespace esphome {
namespace remote_base {
static const char *const TAG = "remote.dyson";
// pulsewidth [µs]
constexpr uint32_t PW_MARK_US = 780;
constexpr uint32_t PW_SHORT_US = 720;
constexpr uint32_t PW_LONG_US = 1500;
constexpr uint32_t PW_START_US = 2280;
// MSB of 15 bit dyson code
constexpr uint16_t MSB_DYSON = (1 << 14);
// required symbols in transmit buffer = (start_symbol + 15 data_symbols)
constexpr uint32_t N_SYMBOLS_REQ = 2u * (1 + 15);
void DysonProtocol::encode(RemoteTransmitData *dst, const DysonData &data) {
uint32_t raw_code = (data.code << 2) + (data.index & 3);
dst->set_carrier_frequency(36000);
dst->reserve(N_SYMBOLS_REQ + 1);
dst->item(PW_START_US, PW_SHORT_US);
for (uint16_t mask = MSB_DYSON; mask != 0; mask >>= 1) {
if (mask == (mask & raw_code)) {
dst->item(PW_MARK_US, PW_LONG_US);
} else {
dst->item(PW_MARK_US, PW_SHORT_US);
}
}
dst->mark(PW_MARK_US); // final carrier pulse
}
optional<DysonData> DysonProtocol::decode(RemoteReceiveData src) {
uint32_t n_received = static_cast<uint32_t>(src.size());
uint16_t raw_code = 0;
DysonData data{
.code = 0,
.index = 0,
};
if (n_received < N_SYMBOLS_REQ)
return {}; // invalid frame length
if (!src.expect_item(PW_START_US, PW_SHORT_US))
return {}; // start not found
for (uint16_t mask = MSB_DYSON; mask != 0; mask >>= 1) {
if (src.expect_item(PW_MARK_US, PW_SHORT_US)) {
raw_code &= ~mask; // zero detected
} else if (src.expect_item(PW_MARK_US, PW_LONG_US)) {
raw_code |= mask; // one detected
} else {
return {}; // invalid data item
}
}
data.code = raw_code >> 2; // extract button code
data.index = raw_code & 3; // extract rolling index
if (src.expect_mark(PW_MARK_US)) { // check total length
return data;
}
return {}; // frame not complete
}
void DysonProtocol::dump(const DysonData &data) {
ESP_LOGI(TAG, "Dyson: code=0x%x rolling index=%d", data.code, data.index);
}
} // namespace remote_base
} // namespace esphome

View File

@@ -0,0 +1,46 @@
#pragma once
#include "remote_base.h"
#include <cinttypes>
namespace esphome {
namespace remote_base {
static constexpr uint8_t IGNORE_INDEX = 0xFF;
struct DysonData {
uint16_t code; // the button, e.g. power, swing, fan++, ...
uint8_t index; // the rolling index counter
bool operator==(const DysonData &rhs) const {
if (IGNORE_INDEX == index || IGNORE_INDEX == rhs.index) {
return code == rhs.code;
}
return code == rhs.code && index == rhs.index;
}
};
class DysonProtocol : public RemoteProtocol<DysonData> {
public:
void encode(RemoteTransmitData *dst, const DysonData &data) override;
optional<DysonData> decode(RemoteReceiveData src) override;
void dump(const DysonData &data) override;
};
DECLARE_REMOTE_PROTOCOL(Dyson)
template<typename... Ts> class DysonAction : public RemoteTransmitterActionBase<Ts...> {
public:
TEMPLATABLE_VALUE(uint16_t, code)
TEMPLATABLE_VALUE(uint8_t, index)
void encode(RemoteTransmitData *dst, Ts... x) override {
DysonData data{};
data.code = this->code_.value(x...);
data.index = this->index_.value(x...);
DysonProtocol().encode(dst, data);
}
};
} // namespace remote_base
} // namespace esphome

View File

@@ -12,6 +12,25 @@
namespace esphome { namespace esphome {
namespace remote_transmitter { namespace remote_transmitter {
#ifdef USE_ESP32
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 5, 1)
// IDF version 5.5.1 and above is required because of a bug in
// the RMT encoder: https://github.com/espressif/esp-idf/issues/17244
typedef union { // NOLINT(modernize-use-using)
struct {
uint16_t duration : 15;
uint16_t level : 1;
};
uint16_t val;
} rmt_symbol_half_t;
struct RemoteTransmitterComponentStore {
uint32_t times{0};
uint32_t index{0};
};
#endif
#endif
class RemoteTransmitterComponent : public remote_base::RemoteTransmitterBase, class RemoteTransmitterComponent : public remote_base::RemoteTransmitterBase,
public Component public Component
#ifdef USE_ESP32 #ifdef USE_ESP32
@@ -56,9 +75,14 @@ class RemoteTransmitterComponent : public remote_base::RemoteTransmitterBase,
#ifdef USE_ESP32 #ifdef USE_ESP32
void configure_rmt_(); void configure_rmt_();
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 5, 1)
RemoteTransmitterComponentStore store_{};
std::vector<rmt_symbol_half_t> rmt_temp_;
#else
std::vector<rmt_symbol_word_t> rmt_temp_;
#endif
uint32_t current_carrier_frequency_{38000}; uint32_t current_carrier_frequency_{38000};
bool initialized_{false}; bool initialized_{false};
std::vector<rmt_symbol_word_t> rmt_temp_;
bool with_dma_{false}; bool with_dma_{false};
bool eot_level_{false}; bool eot_level_{false};
rmt_channel_handle_t channel_{NULL}; rmt_channel_handle_t channel_{NULL};

View File

@@ -10,6 +10,46 @@ namespace remote_transmitter {
static const char *const TAG = "remote_transmitter"; static const char *const TAG = "remote_transmitter";
// Maximum RMT symbol duration (15-bit field)
static constexpr uint32_t RMT_SYMBOL_DURATION_MAX = 0x7FFF;
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 5, 1)
static size_t IRAM_ATTR HOT encoder_callback(const void *data, size_t size, size_t written, size_t free,
rmt_symbol_word_t *symbols, bool *done, void *arg) {
auto *store = static_cast<RemoteTransmitterComponentStore *>(arg);
const auto *encoded = static_cast<const rmt_symbol_half_t *>(data);
size_t length = size / sizeof(rmt_symbol_half_t);
size_t count = 0;
// copy symbols
for (size_t i = 0; i < free; i++) {
uint16_t sym_0 = encoded[store->index++].val;
if (store->index >= length) {
store->index = 0;
store->times--;
if (store->times == 0) {
*done = true;
symbols[count++].val = sym_0;
return count;
}
}
uint16_t sym_1 = encoded[store->index++].val;
if (store->index >= length) {
store->index = 0;
store->times--;
if (store->times == 0) {
*done = true;
symbols[count++].val = sym_0 | (sym_1 << 16);
return count;
}
}
symbols[count++].val = sym_0 | (sym_1 << 16);
}
*done = false;
return count;
}
#endif
void RemoteTransmitterComponent::setup() { void RemoteTransmitterComponent::setup() {
this->inverted_ = this->pin_->is_inverted(); this->inverted_ = this->pin_->is_inverted();
this->configure_rmt_(); this->configure_rmt_();
@@ -34,6 +74,17 @@ void RemoteTransmitterComponent::dump_config() {
} }
void RemoteTransmitterComponent::digital_write(bool value) { void RemoteTransmitterComponent::digital_write(bool value) {
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 5, 1)
rmt_symbol_half_t symbol = {
.duration = 1,
.level = value,
};
rmt_transmit_config_t config;
memset(&config, 0, sizeof(config));
config.flags.eot_level = value;
this->store_.times = 1;
this->store_.index = 0;
#else
rmt_symbol_word_t symbol = { rmt_symbol_word_t symbol = {
.duration0 = 1, .duration0 = 1,
.level0 = value, .level0 = value,
@@ -42,8 +93,8 @@ void RemoteTransmitterComponent::digital_write(bool value) {
}; };
rmt_transmit_config_t config; rmt_transmit_config_t config;
memset(&config, 0, sizeof(config)); memset(&config, 0, sizeof(config));
config.loop_count = 0;
config.flags.eot_level = value; config.flags.eot_level = value;
#endif
esp_err_t error = rmt_transmit(this->channel_, this->encoder_, &symbol, sizeof(symbol), &config); esp_err_t error = rmt_transmit(this->channel_, this->encoder_, &symbol, sizeof(symbol), &config);
if (error != ESP_OK) { if (error != ESP_OK) {
ESP_LOGW(TAG, "rmt_transmit failed: %s", esp_err_to_name(error)); ESP_LOGW(TAG, "rmt_transmit failed: %s", esp_err_to_name(error));
@@ -90,6 +141,20 @@ void RemoteTransmitterComponent::configure_rmt_() {
gpio_pullup_dis(gpio_num_t(this->pin_->get_pin())); gpio_pullup_dis(gpio_num_t(this->pin_->get_pin()));
} }
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 5, 1)
rmt_simple_encoder_config_t encoder;
memset(&encoder, 0, sizeof(encoder));
encoder.callback = encoder_callback;
encoder.arg = &this->store_;
encoder.min_chunk_size = 1;
error = rmt_new_simple_encoder(&encoder, &this->encoder_);
if (error != ESP_OK) {
this->error_code_ = error;
this->error_string_ = "in rmt_new_simple_encoder";
this->mark_failed();
return;
}
#else
rmt_copy_encoder_config_t encoder; rmt_copy_encoder_config_t encoder;
memset(&encoder, 0, sizeof(encoder)); memset(&encoder, 0, sizeof(encoder));
error = rmt_new_copy_encoder(&encoder, &this->encoder_); error = rmt_new_copy_encoder(&encoder, &this->encoder_);
@@ -99,6 +164,7 @@ void RemoteTransmitterComponent::configure_rmt_() {
this->mark_failed(); this->mark_failed();
return; return;
} }
#endif
error = rmt_enable(this->channel_); error = rmt_enable(this->channel_);
if (error != ESP_OK) { if (error != ESP_OK) {
@@ -130,6 +196,79 @@ void RemoteTransmitterComponent::configure_rmt_() {
} }
} }
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 5, 1)
void RemoteTransmitterComponent::send_internal(uint32_t send_times, uint32_t send_wait) {
if (this->is_failed()) {
return;
}
if (this->current_carrier_frequency_ != this->temp_.get_carrier_frequency()) {
this->current_carrier_frequency_ = this->temp_.get_carrier_frequency();
this->configure_rmt_();
}
this->rmt_temp_.clear();
this->rmt_temp_.reserve(this->temp_.get_data().size() + 1);
// encode any delay at the start of the buffer to simplify the encoder callback
// this will be skipped the first time around
send_wait = this->from_microseconds_(static_cast<uint32_t>(send_wait));
while (send_wait > 0) {
int32_t duration = std::min(send_wait, uint32_t(RMT_SYMBOL_DURATION_MAX));
this->rmt_temp_.push_back({
.duration = static_cast<uint16_t>(duration),
.level = static_cast<uint16_t>(this->eot_level_),
});
send_wait -= duration;
}
// encode data
size_t offset = this->rmt_temp_.size();
for (int32_t value : this->temp_.get_data()) {
bool level = value >= 0;
if (!level) {
value = -value;
}
value = this->from_microseconds_(static_cast<uint32_t>(value));
while (value > 0) {
int32_t duration = std::min(value, int32_t(RMT_SYMBOL_DURATION_MAX));
this->rmt_temp_.push_back({
.duration = static_cast<uint16_t>(duration),
.level = static_cast<uint16_t>(level ^ this->inverted_),
});
value -= duration;
}
}
if ((this->rmt_temp_.data() == nullptr) || this->rmt_temp_.size() <= offset) {
ESP_LOGE(TAG, "Empty data");
return;
}
this->transmit_trigger_->trigger();
rmt_transmit_config_t config;
memset(&config, 0, sizeof(config));
config.flags.eot_level = this->eot_level_;
this->store_.times = send_times;
this->store_.index = offset;
esp_err_t error = rmt_transmit(this->channel_, this->encoder_, this->rmt_temp_.data(),
this->rmt_temp_.size() * sizeof(rmt_symbol_half_t), &config);
if (error != ESP_OK) {
ESP_LOGW(TAG, "rmt_transmit failed: %s", esp_err_to_name(error));
this->status_set_warning();
} else {
this->status_clear_warning();
}
error = rmt_tx_wait_all_done(this->channel_, -1);
if (error != ESP_OK) {
ESP_LOGW(TAG, "rmt_tx_wait_all_done failed: %s", esp_err_to_name(error));
this->status_set_warning();
}
this->complete_trigger_->trigger();
}
#else
void RemoteTransmitterComponent::send_internal(uint32_t send_times, uint32_t send_wait) { void RemoteTransmitterComponent::send_internal(uint32_t send_times, uint32_t send_wait) {
if (this->is_failed()) if (this->is_failed())
return; return;
@@ -151,7 +290,7 @@ void RemoteTransmitterComponent::send_internal(uint32_t send_times, uint32_t sen
val = this->from_microseconds_(static_cast<uint32_t>(val)); val = this->from_microseconds_(static_cast<uint32_t>(val));
do { do {
int32_t item = std::min(val, int32_t(32767)); int32_t item = std::min(val, int32_t(RMT_SYMBOL_DURATION_MAX));
val -= item; val -= item;
if (rmt_i % 2 == 0) { if (rmt_i % 2 == 0) {
@@ -180,7 +319,6 @@ void RemoteTransmitterComponent::send_internal(uint32_t send_times, uint32_t sen
for (uint32_t i = 0; i < send_times; i++) { for (uint32_t i = 0; i < send_times; i++) {
rmt_transmit_config_t config; rmt_transmit_config_t config;
memset(&config, 0, sizeof(config)); memset(&config, 0, sizeof(config));
config.loop_count = 0;
config.flags.eot_level = this->eot_level_; config.flags.eot_level = this->eot_level_;
esp_err_t error = rmt_transmit(this->channel_, this->encoder_, this->rmt_temp_.data(), esp_err_t error = rmt_transmit(this->channel_, this->encoder_, this->rmt_temp_.data(),
this->rmt_temp_.size() * sizeof(rmt_symbol_word_t), &config); this->rmt_temp_.size() * sizeof(rmt_symbol_word_t), &config);
@@ -200,6 +338,7 @@ void RemoteTransmitterComponent::send_internal(uint32_t send_times, uint32_t sen
} }
this->complete_trigger_->trigger(); this->complete_trigger_->trigger();
} }
#endif
} // namespace remote_transmitter } // namespace remote_transmitter
} // namespace esphome } // namespace esphome

View File

@@ -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

View File

@@ -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

View File

@@ -1,5 +1,6 @@
#include "select.h" #include "select.h"
#include "esphome/core/log.h" #include "esphome/core/log.h"
#include <cstring>
namespace esphome { namespace esphome {
namespace select { namespace select {
@@ -35,7 +36,7 @@ size_t Select::size() const {
optional<size_t> Select::index_of(const std::string &option) const { optional<size_t> Select::index_of(const std::string &option) const {
const auto &options = traits.get_options(); const auto &options = traits.get_options();
for (size_t i = 0; i < options.size(); i++) { for (size_t i = 0; i < options.size(); i++) {
if (options[i] == option) { if (strcmp(options[i], option.c_str()) == 0) {
return i; return i;
} }
} }
@@ -53,11 +54,13 @@ optional<size_t> Select::active_index() const {
optional<std::string> Select::at(size_t index) const { optional<std::string> Select::at(size_t index) const {
if (this->has_index(index)) { if (this->has_index(index)) {
const auto &options = traits.get_options(); const auto &options = traits.get_options();
return options.at(index); return std::string(options.at(index));
} else { } else {
return {}; return {};
} }
} }
const char *Select::option_at(size_t index) const { return traits.get_options().at(index); }
} // namespace select } // namespace select
} // namespace esphome } // namespace esphome

View File

@@ -56,6 +56,9 @@ class Select : public EntityBase {
/// Return the (optional) option value at the provided index offset. /// Return the (optional) option value at the provided index offset.
optional<std::string> at(size_t index) const; optional<std::string> at(size_t index) const;
/// Return the option value at the provided index offset (as const char* from flash).
const char *option_at(size_t index) const;
void add_on_state_callback(std::function<void(std::string, size_t)> &&callback); void add_on_state_callback(std::function<void(std::string, size_t)> &&callback);
protected: protected:

View File

@@ -3,9 +3,16 @@
namespace esphome { namespace esphome {
namespace select { namespace select {
void SelectTraits::set_options(std::vector<std::string> options) { this->options_ = std::move(options); } void SelectTraits::set_options(const std::initializer_list<const char *> &options) { this->options_ = options; }
const std::vector<std::string> &SelectTraits::get_options() const { return this->options_; } void SelectTraits::set_options(const FixedVector<const char *> &options) {
this->options_.init(options.size());
for (size_t i = 0; i < options.size(); i++) {
this->options_[i] = options[i];
}
}
const FixedVector<const char *> &SelectTraits::get_options() const { return this->options_; }
} // namespace select } // namespace select
} // namespace esphome } // namespace esphome

View File

@@ -1,18 +1,19 @@
#pragma once #pragma once
#include <vector> #include "esphome/core/helpers.h"
#include <string> #include <initializer_list>
namespace esphome { namespace esphome {
namespace select { namespace select {
class SelectTraits { class SelectTraits {
public: public:
void set_options(std::vector<std::string> options); void set_options(const std::initializer_list<const char *> &options);
const std::vector<std::string> &get_options() const; void set_options(const FixedVector<const char *> &options);
const FixedVector<const char *> &get_options() const;
protected: protected:
std::vector<std::string> options_; FixedVector<const char *> options_;
}; };
} // namespace select } // namespace select

View File

@@ -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(

View File

@@ -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:

View File

@@ -27,7 +27,7 @@ void SNTPComponent::setup() {
esp_sntp_setoperatingmode(ESP_SNTP_OPMODE_POLL); esp_sntp_setoperatingmode(ESP_SNTP_OPMODE_POLL);
size_t i = 0; size_t i = 0;
for (auto &server : this->servers_) { for (auto &server : this->servers_) {
esp_sntp_setservername(i++, server.c_str()); esp_sntp_setservername(i++, server);
} }
esp_sntp_set_sync_interval(this->get_update_interval()); esp_sntp_set_sync_interval(this->get_update_interval());
esp_sntp_set_time_sync_notification_cb([](struct timeval *tv) { esp_sntp_set_time_sync_notification_cb([](struct timeval *tv) {
@@ -42,7 +42,7 @@ void SNTPComponent::setup() {
size_t i = 0; size_t i = 0;
for (auto &server : this->servers_) { for (auto &server : this->servers_) {
sntp_setservername(i++, server.c_str()); sntp_setservername(i++, server);
} }
#if defined(USE_ESP8266) #if defined(USE_ESP8266)
@@ -59,7 +59,7 @@ void SNTPComponent::dump_config() {
ESP_LOGCONFIG(TAG, "SNTP Time:"); ESP_LOGCONFIG(TAG, "SNTP Time:");
size_t i = 0; size_t i = 0;
for (auto &server : this->servers_) { for (auto &server : this->servers_) {
ESP_LOGCONFIG(TAG, " Server %zu: '%s'", i++, server.c_str()); ESP_LOGCONFIG(TAG, " Server %zu: '%s'", i++, server);
} }
} }
void SNTPComponent::update() { void SNTPComponent::update() {

View File

@@ -2,10 +2,14 @@
#include "esphome/core/component.h" #include "esphome/core/component.h"
#include "esphome/components/time/real_time_clock.h" #include "esphome/components/time/real_time_clock.h"
#include <array>
namespace esphome { namespace esphome {
namespace sntp { namespace sntp {
// Server count is calculated at compile time by Python codegen
// SNTP_SERVER_COUNT will always be defined
/// The SNTP component allows you to configure local timekeeping via Simple Network Time Protocol. /// The SNTP component allows you to configure local timekeeping via Simple Network Time Protocol.
/// ///
/// \note /// \note
@@ -14,10 +18,7 @@ namespace sntp {
/// \see https://www.gnu.org/software/libc/manual/html_node/TZ-Variable.html /// \see https://www.gnu.org/software/libc/manual/html_node/TZ-Variable.html
class SNTPComponent : public time::RealTimeClock { class SNTPComponent : public time::RealTimeClock {
public: public:
SNTPComponent(const std::vector<std::string> &servers) : servers_(servers) {} SNTPComponent(const std::array<const char *, SNTP_SERVER_COUNT> &servers) : servers_(servers) {}
// Note: set_servers() has been removed and replaced by a constructor - calling set_servers after setup would
// have had no effect anyway, and making the strings immutable avoids the need to strdup their contents.
void setup() override; void setup() override;
void dump_config() override; void dump_config() override;
@@ -29,7 +30,10 @@ class SNTPComponent : public time::RealTimeClock {
void time_synced(); void time_synced();
protected: protected:
std::vector<std::string> servers_; // Store const char pointers to string literals
// ESP8266: strings in rodata (RAM), but avoids std::string overhead (~24 bytes each)
// Other platforms: strings in flash
std::array<const char *, SNTP_SERVER_COUNT> servers_;
bool has_time_{false}; bool has_time_{false};
#if defined(USE_ESP32) #if defined(USE_ESP32)

View File

@@ -43,6 +43,11 @@ CONFIG_SCHEMA = cv.All(
async def to_code(config): async def to_code(config):
servers = config[CONF_SERVERS] servers = config[CONF_SERVERS]
# Define server count at compile time
cg.add_define("SNTP_SERVER_COUNT", len(servers))
# Pass string literals to constructor - stored in flash/rodata by compiler
var = cg.new_Pvariable(config[CONF_ID], servers) var = cg.new_Pvariable(config[CONF_ID], servers)
await cg.register_component(var, config) await cg.register_component(var, config)

View File

@@ -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

View File

@@ -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

View File

@@ -80,19 +80,12 @@ void TemplateAlarmControlPanel::dump_config() {
} }
void TemplateAlarmControlPanel::setup() { void TemplateAlarmControlPanel::setup() {
switch (this->restore_mode_) { this->current_state_ = ACP_STATE_DISARMED;
case ALARM_CONTROL_PANEL_ALWAYS_DISARMED: if (this->restore_mode_ == ALARM_CONTROL_PANEL_RESTORE_DEFAULT_DISARMED) {
this->current_state_ = ACP_STATE_DISARMED; uint8_t value;
break; this->pref_ = global_preferences->make_preference<uint8_t>(this->get_preference_hash());
case ALARM_CONTROL_PANEL_RESTORE_DEFAULT_DISARMED: { if (this->pref_.load(&value)) {
uint8_t value; this->current_state_ = static_cast<alarm_control_panel::AlarmControlPanelState>(value);
this->pref_ = global_preferences->make_preference<uint8_t>(this->get_preference_hash());
if (this->pref_.load(&value)) {
this->current_state_ = static_cast<alarm_control_panel::AlarmControlPanelState>(value);
} else {
this->current_state_ = ACP_STATE_DISARMED;
}
break;
} }
} }
this->desired_state_ = this->current_state_; this->desired_state_ = this->current_state_;
@@ -119,15 +112,15 @@ void TemplateAlarmControlPanel::loop() {
this->publish_state(ACP_STATE_TRIGGERED); this->publish_state(ACP_STATE_TRIGGERED);
return; return;
} }
auto future_state = this->current_state_; auto next_state = this->current_state_;
// reset triggered if all clear // reset triggered if all clear
if (this->current_state_ == ACP_STATE_TRIGGERED && this->trigger_time_ > 0 && if (this->current_state_ == ACP_STATE_TRIGGERED && this->trigger_time_ > 0 &&
(millis() - this->last_update_) > this->trigger_time_) { (millis() - this->last_update_) > this->trigger_time_) {
future_state = this->desired_state_; next_state = this->desired_state_;
} }
bool delayed_sensor_not_ready = false; bool delayed_sensor_faulted = false;
bool instant_sensor_not_ready = false; bool instant_sensor_faulted = false;
#ifdef USE_BINARY_SENSOR #ifdef USE_BINARY_SENSOR
// Test all of the sensors in the list regardless of the alarm panel state // Test all of the sensors in the list regardless of the alarm panel state
@@ -144,7 +137,7 @@ void TemplateAlarmControlPanel::loop() {
// Record the sensor state change // Record the sensor state change
this->sensor_data_[sensor_info.second.store_index].last_chime_state = sensor_info.first->state; this->sensor_data_[sensor_info.second.store_index].last_chime_state = sensor_info.first->state;
} }
// Check for triggered sensors // Check for faulted sensors
if (sensor_info.first->state) { // Sensor triggered? if (sensor_info.first->state) { // Sensor triggered?
// Skip if auto bypassed // Skip if auto bypassed
if (std::count(this->bypassed_sensor_indicies_.begin(), this->bypassed_sensor_indicies_.end(), if (std::count(this->bypassed_sensor_indicies_.begin(), this->bypassed_sensor_indicies_.end(),
@@ -163,42 +156,41 @@ void TemplateAlarmControlPanel::loop() {
} }
switch (sensor_info.second.type) { switch (sensor_info.second.type) {
case ALARM_SENSOR_TYPE_INSTANT:
instant_sensor_not_ready = true;
break;
case ALARM_SENSOR_TYPE_INSTANT_ALWAYS: case ALARM_SENSOR_TYPE_INSTANT_ALWAYS:
instant_sensor_not_ready = true; next_state = ACP_STATE_TRIGGERED;
future_state = ACP_STATE_TRIGGERED; [[fallthrough]];
case ALARM_SENSOR_TYPE_INSTANT:
instant_sensor_faulted = true;
break; break;
case ALARM_SENSOR_TYPE_DELAYED_FOLLOWER: case ALARM_SENSOR_TYPE_DELAYED_FOLLOWER:
// Look to see if we are in the pending state // Look to see if we are in the pending state
if (this->current_state_ == ACP_STATE_PENDING) { if (this->current_state_ == ACP_STATE_PENDING) {
delayed_sensor_not_ready = true; delayed_sensor_faulted = true;
} else { } else {
instant_sensor_not_ready = true; instant_sensor_faulted = true;
} }
break; break;
case ALARM_SENSOR_TYPE_DELAYED: case ALARM_SENSOR_TYPE_DELAYED:
default: default:
delayed_sensor_not_ready = true; delayed_sensor_faulted = true;
} }
} }
} }
// Update all sensors not ready flag // Update all sensors ready flag
this->sensors_ready_ = ((!instant_sensor_not_ready) && (!delayed_sensor_not_ready)); bool sensors_ready = !(instant_sensor_faulted || delayed_sensor_faulted);
// Call the ready state change callback if there was a change // Call the ready state change callback if there was a change
if (this->sensors_ready_ != this->sensors_ready_last_) { if (this->sensors_ready_ != sensors_ready) {
this->sensors_ready_ = sensors_ready;
this->ready_callback_.call(); this->ready_callback_.call();
this->sensors_ready_last_ = this->sensors_ready_;
} }
#endif #endif
if (this->is_state_armed(future_state) && (!this->sensors_ready_)) { if (this->is_state_armed(next_state) && (!this->sensors_ready_)) {
// Instant sensors // Instant sensors
if (instant_sensor_not_ready) { if (instant_sensor_faulted) {
this->publish_state(ACP_STATE_TRIGGERED); this->publish_state(ACP_STATE_TRIGGERED);
} else if (delayed_sensor_not_ready) { } else if (delayed_sensor_faulted) {
// Delayed sensors // Delayed sensors
if ((this->pending_time_ > 0) && (this->current_state_ != ACP_STATE_TRIGGERED)) { if ((this->pending_time_ > 0) && (this->current_state_ != ACP_STATE_TRIGGERED)) {
this->publish_state(ACP_STATE_PENDING); this->publish_state(ACP_STATE_PENDING);
@@ -206,8 +198,8 @@ void TemplateAlarmControlPanel::loop() {
this->publish_state(ACP_STATE_TRIGGERED); this->publish_state(ACP_STATE_TRIGGERED);
} }
} }
} else if (future_state != this->current_state_) { } else if (next_state != this->current_state_) {
this->publish_state(future_state); this->publish_state(next_state);
} }
} }
@@ -234,8 +226,6 @@ uint32_t TemplateAlarmControlPanel::get_supported_features() const {
return features; return features;
} }
bool TemplateAlarmControlPanel::get_requires_code() const { return !this->codes_.empty(); }
void TemplateAlarmControlPanel::arm_(optional<std::string> code, alarm_control_panel::AlarmControlPanelState state, void TemplateAlarmControlPanel::arm_(optional<std::string> code, alarm_control_panel::AlarmControlPanelState state,
uint32_t delay) { uint32_t delay) {
if (this->current_state_ != ACP_STATE_DISARMED) { if (this->current_state_ != ACP_STATE_DISARMED) {
@@ -258,9 +248,9 @@ void TemplateAlarmControlPanel::arm_(optional<std::string> code, alarm_control_p
void TemplateAlarmControlPanel::bypass_before_arming() { void TemplateAlarmControlPanel::bypass_before_arming() {
#ifdef USE_BINARY_SENSOR #ifdef USE_BINARY_SENSOR
for (auto sensor_info : this->sensor_map_) { for (auto sensor_info : this->sensor_map_) {
// Check for sensors left on and set to bypass automatically and remove them from monitoring // Check for faulted bypass_auto sensors and remove them from monitoring
if ((sensor_info.second.flags & BINARY_SENSOR_MODE_BYPASS_AUTO) && (sensor_info.first->state)) { if ((sensor_info.second.flags & BINARY_SENSOR_MODE_BYPASS_AUTO) && (sensor_info.first->state)) {
ESP_LOGW(TAG, "'%s' is left on and will be automatically bypassed", sensor_info.first->get_name().c_str()); ESP_LOGW(TAG, "'%s' is faulted and will be automatically bypassed", sensor_info.first->get_name().c_str());
this->bypassed_sensor_indicies_.push_back(sensor_info.second.store_index); this->bypassed_sensor_indicies_.push_back(sensor_info.second.store_index);
} }
} }

View File

@@ -56,7 +56,7 @@ class TemplateAlarmControlPanel : public alarm_control_panel::AlarmControlPanel,
void setup() override; void setup() override;
void loop() override; void loop() override;
uint32_t get_supported_features() const override; uint32_t get_supported_features() const override;
bool get_requires_code() const override; bool get_requires_code() const override { return !this->codes_.empty(); }
bool get_requires_code_to_arm() const override { return this->requires_code_to_arm_; } bool get_requires_code_to_arm() const override { return this->requires_code_to_arm_; }
bool get_all_sensors_ready() { return this->sensors_ready_; }; bool get_all_sensors_ready() { return this->sensors_ready_; };
void set_restore_mode(TemplateAlarmControlPanelRestoreMode restore_mode) { this->restore_mode_ = restore_mode; } void set_restore_mode(TemplateAlarmControlPanelRestoreMode restore_mode) { this->restore_mode_ = restore_mode; }
@@ -66,7 +66,8 @@ class TemplateAlarmControlPanel : public alarm_control_panel::AlarmControlPanel,
/** Add a binary_sensor to the alarm_panel. /** Add a binary_sensor to the alarm_panel.
* *
* @param sensor The BinarySensor instance. * @param sensor The BinarySensor instance.
* @param ignore_when_home if this should be ignored when armed_home mode * @param flags The OR of BinarySensorFlags for the sensor.
* @param type The sensor type which determines its triggering behaviour.
*/ */
void add_sensor(binary_sensor::BinarySensor *sensor, uint16_t flags = 0, void add_sensor(binary_sensor::BinarySensor *sensor, uint16_t flags = 0,
AlarmSensorType type = ALARM_SENSOR_TYPE_DELAYED); AlarmSensorType type = ALARM_SENSOR_TYPE_DELAYED);
@@ -121,7 +122,7 @@ class TemplateAlarmControlPanel : public alarm_control_panel::AlarmControlPanel,
protected: protected:
void control(const alarm_control_panel::AlarmControlPanelCall &call) override; void control(const alarm_control_panel::AlarmControlPanelCall &call) override;
#ifdef USE_BINARY_SENSOR #ifdef USE_BINARY_SENSOR
// This maps a binary sensor to its type and attribute bits // This maps a binary sensor to its alarm specific info
std::map<binary_sensor::BinarySensor *, SensorInfo> sensor_map_; std::map<binary_sensor::BinarySensor *, SensorInfo> sensor_map_;
// a list of automatically bypassed sensors // a list of automatically bypassed sensors
std::vector<uint8_t> bypassed_sensor_indicies_; std::vector<uint8_t> bypassed_sensor_indicies_;
@@ -147,7 +148,6 @@ class TemplateAlarmControlPanel : public alarm_control_panel::AlarmControlPanel,
bool supports_arm_home_ = false; bool supports_arm_home_ = false;
bool supports_arm_night_ = false; bool supports_arm_night_ = false;
bool sensors_ready_ = false; bool sensors_ready_ = false;
bool sensors_ready_last_ = false;
uint8_t next_store_index_ = 0; uint8_t next_store_index_ = 0;
// check if the code is valid // check if the code is valid
bool is_code_valid_(optional<std::string> code); bool is_code_valid_(optional<std::string> code);

View File

@@ -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_))

View File

@@ -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);
} }

View File

@@ -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_

View File

@@ -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; }

View File

@@ -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_;

Some files were not shown because too many files have changed in this diff Show More