From f3cddcee214fc8c510ecfc88dd3ef277c0d882df Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 20 Mar 2026 15:25:40 -1000 Subject: [PATCH 1/4] [core] Store parent pointers as members to enable inline Callback storage (#14923) --- esphome/components/cover/automation.h | 18 +++--- esphome/components/datetime/datetime_base.h | 7 ++- .../components/display_menu_base/automation.h | 35 ++++++++---- esphome/components/esp32_improv/automation.h | 55 ++++++++++++------- esphome/components/fan/automation.h | 49 ++++++++++------- .../graphical_display_menu.h | 7 ++- esphome/components/lock/automation.h | 9 ++- esphome/components/media_player/automation.h | 9 ++- esphome/components/mqtt/mqtt_fan.cpp | 3 +- esphome/components/valve/automation.h | 18 ++++-- 10 files changed, 134 insertions(+), 76 deletions(-) diff --git a/esphome/components/cover/automation.h b/esphome/components/cover/automation.h index 12ec46725d6..f121e5c2d67 100644 --- a/esphome/components/cover/automation.h +++ b/esphome/components/cover/automation.h @@ -105,17 +105,18 @@ template using CoverIsClosedCondition = CoverPositionCondition class CoverPositionTrigger : public Trigger<> { public: - CoverPositionTrigger(Cover *a_cover) { - a_cover->add_on_state_callback([this, a_cover]() { - if (a_cover->position != this->last_position_) { - this->last_position_ = a_cover->position; - if (a_cover->position == (OPEN ? COVER_OPEN : COVER_CLOSED)) + CoverPositionTrigger(Cover *a_cover) : cover_(a_cover) { + a_cover->add_on_state_callback([this]() { + if (this->cover_->position != this->last_position_) { + this->last_position_ = this->cover_->position; + if (this->cover_->position == (OPEN ? COVER_OPEN : COVER_CLOSED)) this->trigger(); } }); } protected: + Cover *cover_; float last_position_{NAN}; }; @@ -124,9 +125,9 @@ using CoverClosedTrigger = CoverPositionTrigger; template class CoverTrigger : public Trigger<> { public: - CoverTrigger(Cover *a_cover) { - a_cover->add_on_state_callback([this, a_cover]() { - auto current_op = a_cover->current_operation; + CoverTrigger(Cover *a_cover) : cover_(a_cover) { + a_cover->add_on_state_callback([this]() { + auto current_op = this->cover_->current_operation; if (current_op == OP) { if (!this->last_operation_.has_value() || this->last_operation_.value() != OP) { this->trigger(); @@ -137,6 +138,7 @@ template class CoverTrigger : public Trigger<> { } protected: + Cover *cover_; optional last_operation_{}; }; } // namespace esphome::cover diff --git a/esphome/components/datetime/datetime_base.h b/esphome/components/datetime/datetime_base.h index 98f23aa7137..6c0a33c8420 100644 --- a/esphome/components/datetime/datetime_base.h +++ b/esphome/components/datetime/datetime_base.h @@ -33,9 +33,12 @@ class DateTimeBase : public EntityBase { class DateTimeStateTrigger : public Trigger { public: - explicit DateTimeStateTrigger(DateTimeBase *parent) { - parent->add_on_state_callback([this, parent]() { this->trigger(parent->state_as_esptime()); }); + explicit DateTimeStateTrigger(DateTimeBase *parent) : parent_(parent) { + parent->add_on_state_callback([this]() { this->trigger(this->parent_->state_as_esptime()); }); } + + protected: + DateTimeBase *parent_; }; } // namespace esphome::datetime diff --git a/esphome/components/display_menu_base/automation.h b/esphome/components/display_menu_base/automation.h index 9c64794cefe..50c26c344cd 100644 --- a/esphome/components/display_menu_base/automation.h +++ b/esphome/components/display_menu_base/automation.h @@ -96,37 +96,52 @@ template class IsActiveCondition : public Condition { class DisplayMenuOnEnterTrigger : public Trigger { public: - explicit DisplayMenuOnEnterTrigger(MenuItem *parent) { - parent->add_on_enter_callback([this, parent]() { this->trigger(parent); }); + explicit DisplayMenuOnEnterTrigger(MenuItem *parent) : parent_(parent) { + parent->add_on_enter_callback([this]() { this->trigger(this->parent_); }); } + + protected: + MenuItem *parent_; }; class DisplayMenuOnLeaveTrigger : public Trigger { public: - explicit DisplayMenuOnLeaveTrigger(MenuItem *parent) { - parent->add_on_leave_callback([this, parent]() { this->trigger(parent); }); + explicit DisplayMenuOnLeaveTrigger(MenuItem *parent) : parent_(parent) { + parent->add_on_leave_callback([this]() { this->trigger(this->parent_); }); } + + protected: + MenuItem *parent_; }; class DisplayMenuOnValueTrigger : public Trigger { public: - explicit DisplayMenuOnValueTrigger(MenuItem *parent) { - parent->add_on_value_callback([this, parent]() { this->trigger(parent); }); + explicit DisplayMenuOnValueTrigger(MenuItem *parent) : parent_(parent) { + parent->add_on_value_callback([this]() { this->trigger(this->parent_); }); } + + protected: + MenuItem *parent_; }; class DisplayMenuOnNextTrigger : public Trigger { public: - explicit DisplayMenuOnNextTrigger(MenuItemCustom *parent) { - parent->add_on_next_callback([this, parent]() { this->trigger(parent); }); + explicit DisplayMenuOnNextTrigger(MenuItemCustom *parent) : parent_(parent) { + parent->add_on_next_callback([this]() { this->trigger(this->parent_); }); } + + protected: + MenuItemCustom *parent_; }; class DisplayMenuOnPrevTrigger : public Trigger { public: - explicit DisplayMenuOnPrevTrigger(MenuItemCustom *parent) { - parent->add_on_prev_callback([this, parent]() { this->trigger(parent); }); + explicit DisplayMenuOnPrevTrigger(MenuItemCustom *parent) : parent_(parent) { + parent->add_on_prev_callback([this]() { this->trigger(this->parent_); }); } + + protected: + MenuItemCustom *parent_; }; } // namespace display_menu_base diff --git a/esphome/components/esp32_improv/automation.h b/esphome/components/esp32_improv/automation.h index 52c5da125b7..cd2bd84c30c 100644 --- a/esphome/components/esp32_improv/automation.h +++ b/esphome/components/esp32_improv/automation.h @@ -12,58 +12,73 @@ namespace esp32_improv { class ESP32ImprovProvisionedTrigger : public Trigger<> { public: - explicit ESP32ImprovProvisionedTrigger(ESP32ImprovComponent *parent) { - parent->add_on_state_callback([this, parent](improv::State state, improv::Error error) { - if (state == improv::STATE_PROVISIONED && !parent->is_failed()) { - trigger(); + explicit ESP32ImprovProvisionedTrigger(ESP32ImprovComponent *parent) : parent_(parent) { + parent->add_on_state_callback([this](improv::State state, improv::Error error) { + if (state == improv::STATE_PROVISIONED && !this->parent_->is_failed()) { + this->trigger(); } }); } + + protected: + ESP32ImprovComponent *parent_; }; class ESP32ImprovProvisioningTrigger : public Trigger<> { public: - explicit ESP32ImprovProvisioningTrigger(ESP32ImprovComponent *parent) { - parent->add_on_state_callback([this, parent](improv::State state, improv::Error error) { - if (state == improv::STATE_PROVISIONING && !parent->is_failed()) { - trigger(); + explicit ESP32ImprovProvisioningTrigger(ESP32ImprovComponent *parent) : parent_(parent) { + parent->add_on_state_callback([this](improv::State state, improv::Error error) { + if (state == improv::STATE_PROVISIONING && !this->parent_->is_failed()) { + this->trigger(); } }); } + + protected: + ESP32ImprovComponent *parent_; }; class ESP32ImprovStartTrigger : public Trigger<> { public: - explicit ESP32ImprovStartTrigger(ESP32ImprovComponent *parent) { - parent->add_on_state_callback([this, parent](improv::State state, improv::Error error) { + explicit ESP32ImprovStartTrigger(ESP32ImprovComponent *parent) : parent_(parent) { + parent->add_on_state_callback([this](improv::State state, improv::Error error) { if ((state == improv::STATE_AUTHORIZED || state == improv::STATE_AWAITING_AUTHORIZATION) && - !parent->is_failed()) { - trigger(); + !this->parent_->is_failed()) { + this->trigger(); } }); } + + protected: + ESP32ImprovComponent *parent_; }; class ESP32ImprovStateTrigger : public Trigger { public: - explicit ESP32ImprovStateTrigger(ESP32ImprovComponent *parent) { - parent->add_on_state_callback([this, parent](improv::State state, improv::Error error) { - if (!parent->is_failed()) { - trigger(state, error); + explicit ESP32ImprovStateTrigger(ESP32ImprovComponent *parent) : parent_(parent) { + parent->add_on_state_callback([this](improv::State state, improv::Error error) { + if (!this->parent_->is_failed()) { + this->trigger(state, error); } }); } + + protected: + ESP32ImprovComponent *parent_; }; class ESP32ImprovStoppedTrigger : public Trigger<> { public: - explicit ESP32ImprovStoppedTrigger(ESP32ImprovComponent *parent) { - parent->add_on_state_callback([this, parent](improv::State state, improv::Error error) { - if (state == improv::STATE_STOPPED && !parent->is_failed()) { - trigger(); + explicit ESP32ImprovStoppedTrigger(ESP32ImprovComponent *parent) : parent_(parent) { + parent->add_on_state_callback([this](improv::State state, improv::Error error) { + if (state == improv::STATE_STOPPED && !this->parent_->is_failed()) { + this->trigger(); } }); } + + protected: + ESP32ImprovComponent *parent_; }; } // namespace esp32_improv diff --git a/esphome/components/fan/automation.h b/esphome/components/fan/automation.h index 3c3b0ce519e..3ee6f89e557 100644 --- a/esphome/components/fan/automation.h +++ b/esphome/components/fan/automation.h @@ -113,16 +113,19 @@ template class FanIsOffCondition : public Condition { class FanStateTrigger : public Trigger { public: - FanStateTrigger(Fan *state) { - state->add_on_state_callback([this, state]() { this->trigger(state); }); + FanStateTrigger(Fan *state) : fan_(state) { + state->add_on_state_callback([this]() { this->trigger(this->fan_); }); } + + protected: + Fan *fan_; }; class FanTurnOnTrigger : public Trigger<> { public: - FanTurnOnTrigger(Fan *state) { - state->add_on_state_callback([this, state]() { - auto is_on = state->state; + FanTurnOnTrigger(Fan *state) : fan_(state) { + state->add_on_state_callback([this]() { + auto is_on = this->fan_->state; auto should_trigger = is_on && !this->last_on_; this->last_on_ = is_on; if (should_trigger) { @@ -133,14 +136,15 @@ class FanTurnOnTrigger : public Trigger<> { } protected: + Fan *fan_; bool last_on_; }; class FanTurnOffTrigger : public Trigger<> { public: - FanTurnOffTrigger(Fan *state) { - state->add_on_state_callback([this, state]() { - auto is_on = state->state; + FanTurnOffTrigger(Fan *state) : fan_(state) { + state->add_on_state_callback([this]() { + auto is_on = this->fan_->state; auto should_trigger = !is_on && this->last_on_; this->last_on_ = is_on; if (should_trigger) { @@ -151,14 +155,15 @@ class FanTurnOffTrigger : public Trigger<> { } protected: + Fan *fan_; bool last_on_; }; class FanDirectionSetTrigger : public Trigger { public: - FanDirectionSetTrigger(Fan *state) { - state->add_on_state_callback([this, state]() { - auto direction = state->direction; + FanDirectionSetTrigger(Fan *state) : fan_(state) { + state->add_on_state_callback([this]() { + auto direction = this->fan_->direction; auto should_trigger = direction != this->last_direction_; this->last_direction_ = direction; if (should_trigger) { @@ -169,14 +174,15 @@ class FanDirectionSetTrigger : public Trigger { } protected: + Fan *fan_; FanDirection last_direction_; }; class FanOscillatingSetTrigger : public Trigger { public: - FanOscillatingSetTrigger(Fan *state) { - state->add_on_state_callback([this, state]() { - auto oscillating = state->oscillating; + FanOscillatingSetTrigger(Fan *state) : fan_(state) { + state->add_on_state_callback([this]() { + auto oscillating = this->fan_->oscillating; auto should_trigger = oscillating != this->last_oscillating_; this->last_oscillating_ = oscillating; if (should_trigger) { @@ -187,14 +193,15 @@ class FanOscillatingSetTrigger : public Trigger { } protected: + Fan *fan_; bool last_oscillating_; }; class FanSpeedSetTrigger : public Trigger { public: - FanSpeedSetTrigger(Fan *state) { - state->add_on_state_callback([this, state]() { - auto speed = state->speed; + FanSpeedSetTrigger(Fan *state) : fan_(state) { + state->add_on_state_callback([this]() { + auto speed = this->fan_->speed; auto should_trigger = speed != this->last_speed_; this->last_speed_ = speed; if (should_trigger) { @@ -205,14 +212,15 @@ class FanSpeedSetTrigger : public Trigger { } protected: + Fan *fan_; int last_speed_; }; class FanPresetSetTrigger : public Trigger { public: - FanPresetSetTrigger(Fan *state) { - state->add_on_state_callback([this, state]() { - auto preset_mode = state->get_preset_mode(); + FanPresetSetTrigger(Fan *state) : fan_(state) { + state->add_on_state_callback([this]() { + auto preset_mode = this->fan_->get_preset_mode(); auto should_trigger = preset_mode != this->last_preset_mode_; this->last_preset_mode_ = preset_mode; if (should_trigger) { @@ -223,6 +231,7 @@ class FanPresetSetTrigger : public Trigger { } protected: + Fan *fan_; StringRef last_preset_mode_{}; }; diff --git a/esphome/components/graphical_display_menu/graphical_display_menu.h b/esphome/components/graphical_display_menu/graphical_display_menu.h index 007889557dd..ce1db185251 100644 --- a/esphome/components/graphical_display_menu/graphical_display_menu.h +++ b/esphome/components/graphical_display_menu/graphical_display_menu.h @@ -75,9 +75,12 @@ class GraphicalDisplayMenu : public display_menu_base::DisplayMenuComponent { class GraphicalDisplayMenuOnRedrawTrigger : public Trigger { public: - explicit GraphicalDisplayMenuOnRedrawTrigger(GraphicalDisplayMenu *parent) { - parent->add_on_redraw_callback([this, parent]() { this->trigger(parent); }); + explicit GraphicalDisplayMenuOnRedrawTrigger(GraphicalDisplayMenu *parent) : parent_(parent) { + parent->add_on_redraw_callback([this]() { this->trigger(this->parent_); }); } + + protected: + GraphicalDisplayMenu *parent_; }; } // namespace graphical_display_menu diff --git a/esphome/components/lock/automation.h b/esphome/components/lock/automation.h index 011c6cc6afc..6f3c4226933 100644 --- a/esphome/components/lock/automation.h +++ b/esphome/components/lock/automation.h @@ -51,13 +51,16 @@ template class LockCondition : public Condition { template class LockStateTrigger : public Trigger<> { public: - explicit LockStateTrigger(Lock *a_lock) { - a_lock->add_on_state_callback([this, a_lock]() { - if (a_lock->state == State) { + explicit LockStateTrigger(Lock *a_lock) : lock_(a_lock) { + a_lock->add_on_state_callback([this]() { + if (this->lock_->state == State) { this->trigger(); } }); } + + protected: + Lock *lock_; }; using LockLockTrigger = LockStateTrigger; diff --git a/esphome/components/media_player/automation.h b/esphome/components/media_player/automation.h index 90e7bf75b5c..031f6657f49 100644 --- a/esphome/components/media_player/automation.h +++ b/esphome/components/media_player/automation.h @@ -80,12 +80,15 @@ class StateTrigger : public Trigger<> { template class MediaPlayerStateTrigger : public Trigger<> { public: - explicit MediaPlayerStateTrigger(MediaPlayer *player) { - player->add_on_state_callback([this, player]() { - if (player->state == State) + explicit MediaPlayerStateTrigger(MediaPlayer *player) : player_(player) { + player->add_on_state_callback([this]() { + if (this->player_->state == State) this->trigger(); }); } + + protected: + MediaPlayer *player_; }; using IdleTrigger = MediaPlayerStateTrigger; diff --git a/esphome/components/mqtt/mqtt_fan.cpp b/esphome/components/mqtt/mqtt_fan.cpp index ae2b8c46002..3a8658a55fe 100644 --- a/esphome/components/mqtt/mqtt_fan.cpp +++ b/esphome/components/mqtt/mqtt_fan.cpp @@ -121,8 +121,7 @@ void MQTTFanComponent::setup() { }); } - auto f = std::bind(&MQTTFanComponent::publish_state, this); - this->state_->add_on_state_callback([this, f]() { this->defer("send", f); }); + this->state_->add_on_state_callback([this]() { this->defer("send", [this]() { this->publish_state(); }); }); } void MQTTFanComponent::dump_config() { diff --git a/esphome/components/valve/automation.h b/esphome/components/valve/automation.h index 87e9cde0885..a064f375f72 100644 --- a/esphome/components/valve/automation.h +++ b/esphome/components/valve/automation.h @@ -87,24 +87,30 @@ template class ValveIsClosedCondition : public Condition class ValveOpenTrigger : public Trigger<> { public: - ValveOpenTrigger(Valve *a_valve) { - a_valve->add_on_state_callback([this, a_valve]() { - if (a_valve->is_fully_open()) { + ValveOpenTrigger(Valve *a_valve) : valve_(a_valve) { + a_valve->add_on_state_callback([this]() { + if (this->valve_->is_fully_open()) { this->trigger(); } }); } + + protected: + Valve *valve_; }; class ValveClosedTrigger : public Trigger<> { public: - ValveClosedTrigger(Valve *a_valve) { - a_valve->add_on_state_callback([this, a_valve]() { - if (a_valve->is_fully_closed()) { + ValveClosedTrigger(Valve *a_valve) : valve_(a_valve) { + a_valve->add_on_state_callback([this]() { + if (this->valve_->is_fully_closed()) { this->trigger(); } }); } + + protected: + Valve *valve_; }; } // namespace valve From 95dea59382d422f8d1a44020a64d79935bd6ac19 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 20 Mar 2026 15:25:54 -1000 Subject: [PATCH 2/4] [core] Use SplitMix32 PRNG for random_uint32() (#14984) --- esphome/components/esp32/helpers.cpp | 1 - esphome/components/host/helpers.cpp | 9 -------- esphome/components/libretiny/helpers.cpp | 2 -- esphome/components/rp2040/helpers.cpp | 9 -------- esphome/components/zephyr/__init__.py | 2 ++ esphome/components/zephyr/core.cpp | 1 - esphome/core/helpers.cpp | 26 ++++++++++++++++++++++++ esphome/core/helpers.h | 7 ++++++- 8 files changed, 34 insertions(+), 23 deletions(-) diff --git a/esphome/components/esp32/helpers.cpp b/esphome/components/esp32/helpers.cpp index 76f1c59c739..654a35b4735 100644 --- a/esphome/components/esp32/helpers.cpp +++ b/esphome/components/esp32/helpers.cpp @@ -14,7 +14,6 @@ namespace esphome { -uint32_t random_uint32() { return esp_random(); } bool random_bytes(uint8_t *data, size_t len) { esp_fill_random(data, len); return true; diff --git a/esphome/components/host/helpers.cpp b/esphome/components/host/helpers.cpp index fdad4f5cb69..7e8849b3e19 100644 --- a/esphome/components/host/helpers.cpp +++ b/esphome/components/host/helpers.cpp @@ -8,8 +8,6 @@ #include #endif #include -#include -#include #include "esphome/core/defines.h" #include "esphome/core/log.h" @@ -18,13 +16,6 @@ namespace esphome { static const char *const TAG = "helpers.host"; -uint32_t random_uint32() { - std::random_device dev; - std::mt19937 rng(dev()); - std::uniform_int_distribution dist(0, std::numeric_limits::max()); - return dist(rng); -} - bool random_bytes(uint8_t *data, size_t len) { FILE *fp = fopen("/dev/urandom", "r"); if (fp == nullptr) { diff --git a/esphome/components/libretiny/helpers.cpp b/esphome/components/libretiny/helpers.cpp index ffbd181c54c..52332ef53d8 100644 --- a/esphome/components/libretiny/helpers.cpp +++ b/esphome/components/libretiny/helpers.cpp @@ -8,8 +8,6 @@ namespace esphome { -uint32_t random_uint32() { return rand(); } - bool random_bytes(uint8_t *data, size_t len) { lt_rand_bytes(data, len); return true; diff --git a/esphome/components/rp2040/helpers.cpp b/esphome/components/rp2040/helpers.cpp index e360b45ebbb..8cb5f7c18df 100644 --- a/esphome/components/rp2040/helpers.cpp +++ b/esphome/components/rp2040/helpers.cpp @@ -17,15 +17,6 @@ namespace esphome { -uint32_t random_uint32() { - uint32_t result = 0; - for (uint8_t i = 0; i < 32; i++) { - result <<= 1; - result |= rosc_hw->randombit; - } - return result; -} - bool random_bytes(uint8_t *data, size_t len) { while (len-- != 0) { uint8_t result = 0; diff --git a/esphome/components/zephyr/__init__.py b/esphome/components/zephyr/__init__.py index b8a091feb92..348e7a3cf2d 100644 --- a/esphome/components/zephyr/__init__.py +++ b/esphome/components/zephyr/__init__.py @@ -122,6 +122,8 @@ def zephyr_to_code(config: ConfigType) -> None: zephyr_add_prj_conf("FPU", True) zephyr_add_prj_conf("NEWLIB_LIBC_FLOAT_PRINTF", True) zephyr_add_prj_conf("STD_CPP20", True) + # random_bytes() uses sys_rand_get() which requires the entropy subsystem + zephyr_add_prj_conf("ENTROPY_GENERATOR", True) # os: ***** USAGE FAULT ***** # os: Illegal load of EXC_RETURN into PC diff --git a/esphome/components/zephyr/core.cpp b/esphome/components/zephyr/core.cpp index d7c77fdd2ca..a3b0471ebcc 100644 --- a/esphome/components/zephyr/core.cpp +++ b/esphome/components/zephyr/core.cpp @@ -75,7 +75,6 @@ IRAM_ATTR InterruptLock::~InterruptLock() { irq_unlock(state_); } // Zephyr LwIPLock is defined inline as a no-op in helpers.h -uint32_t random_uint32() { return rand(); } // NOLINT(cert-msc30-c, cert-msc50-cpp) bool random_bytes(uint8_t *data, size_t len) { sys_rand_get(data, len); return true; diff --git a/esphome/core/helpers.cpp b/esphome/core/helpers.cpp index 00b447ebf29..ee99c541963 100644 --- a/esphome/core/helpers.cpp +++ b/esphome/core/helpers.cpp @@ -156,6 +156,32 @@ uint32_t fnv1_hash(const char *str) { return hash; } +// SplitMix32 — a fast, non-cryptographic PRNG from the SplitMix family +// (Steele et al., 2014). Uses a Weyl sequence with golden-ratio increment +// and the MurmurHash3 32-bit finalizer as output mixing function. +// Reference: https://doi.org/10.1145/2714064.2660195 +// Test results: https://lemire.me/blog/2017/08/22/testing-non-cryptographic-random-number-generators-my-results/ +// Seeded lazily from the platform's secure RNG via random_bytes(). +// ESP8266 uses os_random() instead (defined in esp8266/helpers.cpp). +#ifndef USE_ESP8266 +static uint32_t splitmix32_state; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) + +uint32_t random_uint32() { + // State of 0 means unseeded. The state will wrap back to 0 after 2^32 calls, + // triggering one extra random_bytes() call — an acceptable trade-off vs. adding + // a separate bool flag (4 bytes BSS + branch on every call). + if (splitmix32_state == 0) { + random_bytes(reinterpret_cast(&splitmix32_state), sizeof(splitmix32_state)); + splitmix32_state |= 1; // ensure non-zero seed + } + splitmix32_state += 0x9e3779b9u; + uint32_t z = splitmix32_state; + z = (z ^ (z >> 16)) * 0x85ebca6bu; + z = (z ^ (z >> 13)) * 0xc2b2ae35u; + return z ^ (z >> 16); +} +#endif + float random_float() { return static_cast(random_uint32()) / static_cast(UINT32_MAX); } // Strings diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index a703b5a5f37..43431299dea 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -842,10 +842,15 @@ template inline constexpr ESPHOME_ALWAYS_INLINE Ret } /// Return a random 32-bit unsigned integer. +/// Not thread-safe. Must only be called from the main loop. +/// Not suitable for cryptographic use; use random_bytes() instead. uint32_t random_uint32(); /// Return a random float between 0 and 1. +/// Not thread-safe. Must only be called from the main loop. +/// Not suitable for cryptographic use; use random_bytes() instead. float random_float(); -/// Generate \p len number of random bytes. +/// Generate \p len random bytes using the platform's secure RNG (hardware RNG or OS CSPRNG). +/// Thread-safe. Suitable for cryptographic use. bool random_bytes(uint8_t *data, size_t len); ///@} From cc58cffb7b8f26e783b3fbd581ed17a0e70f98f3 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 20 Mar 2026 16:58:34 -1000 Subject: [PATCH 3/4] [api] Enable HAVE_WEAK_SYMBOLS and HAVE_INLINE_ASM for libsodium Without these flags, libsodium's sodium_memzero() falls through to the slowest fallback: a volatile byte-by-byte zeroing loop. With them, it uses memset() guarded by a weak-symbol call and inline asm memory clobber, which is significantly faster. This also improves sodium_memcmp() and sodium_compare() codegen by allowing non-volatile pointer access with weak barriers, and enables a timing-safe comparison optimization in crypto_verify. Both flags are safe for all ESPHome targets: __attribute__((weak)) and __asm__ are core GCC features supported by all toolchains (Xtensa, RISC-V, ARM, x86_64). --- esphome/components/api/__init__.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/esphome/components/api/__init__.py b/esphome/components/api/__init__.py index 4c3cf81927d..84589d540d3 100644 --- a/esphome/components/api/__init__.py +++ b/esphome/components/api/__init__.py @@ -455,6 +455,9 @@ async def to_code(config: ConfigType) -> None: cg.add_define("USE_API_PLAINTEXT") cg.add_define("USE_API_NOISE") cg.add_library("esphome/noise-c", "0.1.11") + # Enable optimized memzero/memcmp in libsodium instead of volatile byte loops + cg.add_build_flag("-DHAVE_WEAK_SYMBOLS=1") + cg.add_build_flag("-DHAVE_INLINE_ASM=1") else: cg.add_define("USE_API_PLAINTEXT") From 1920d8a887407a9d9f2f23f8c1d9f83480a497a5 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 20 Mar 2026 17:35:17 -1000 Subject: [PATCH 4/4] [benchmark] Add noise encryption benchmarks (#15037) --- script/setup_codspeed_lib.py | 8 +- tests/benchmarks/components/api/__init__.py | 7 + .../components/api/bench_noise_encrypt.cpp | 177 ++++++++++++++++++ .../benchmarks/components/api/benchmark.yaml | 1 + tests/benchmarks/components/mdns/__init__.py | 5 + .../benchmarks/components/network/__init__.py | 5 + .../benchmarks/components/socket/__init__.py | 7 + 7 files changed, 209 insertions(+), 1 deletion(-) create mode 100644 tests/benchmarks/components/api/__init__.py create mode 100644 tests/benchmarks/components/api/bench_noise_encrypt.cpp create mode 100644 tests/benchmarks/components/mdns/__init__.py create mode 100644 tests/benchmarks/components/network/__init__.py create mode 100644 tests/benchmarks/components/socket/__init__.py diff --git a/script/setup_codspeed_lib.py b/script/setup_codspeed_lib.py index 959c89d05b6..4f5d1bff24b 100755 --- a/script/setup_codspeed_lib.py +++ b/script/setup_codspeed_lib.py @@ -205,7 +205,13 @@ def setup_codspeed_lib(output_dir: Path) -> None: if hooks_dist_c.exists(): _copy_if_missing(hooks_dist_c, lib_src / "instrument_hooks.c") - # 4. Write library.json + # 4. Copy instrument-hooks headers (core.h, callgrind.h, valgrind.h) next to + # measurement.hpp so they are found before any same-named headers from + # other libraries (e.g. libsodium's core.h). + for header in hooks_include.glob("*.h"): + _copy_if_missing(header, core_include / header.name) + + # 5. Write library.json version = _read_codspeed_version(output_dir / CORE_CMAKE) _write_library_json( benchmark_dir, core_include, hooks_include, version, project_root diff --git a/tests/benchmarks/components/api/__init__.py b/tests/benchmarks/components/api/__init__.py new file mode 100644 index 00000000000..0687c3f87fb --- /dev/null +++ b/tests/benchmarks/components/api/__init__.py @@ -0,0 +1,7 @@ +from tests.testing_helpers import ComponentManifestOverride + + +def override_manifest(manifest: ComponentManifestOverride) -> None: + # api must run its to_code to define USE_API, USE_API_PLAINTEXT, + # and add the noise-c library dependency. + manifest.enable_codegen() diff --git a/tests/benchmarks/components/api/bench_noise_encrypt.cpp b/tests/benchmarks/components/api/bench_noise_encrypt.cpp new file mode 100644 index 00000000000..223e6ada0d0 --- /dev/null +++ b/tests/benchmarks/components/api/bench_noise_encrypt.cpp @@ -0,0 +1,177 @@ +#include "esphome/core/defines.h" +#ifdef USE_API_NOISE + +#include +#include +#include + +#include "noise/protocol.h" + +namespace esphome::api::benchmarks { + +static constexpr int kInnerIterations = 2000; + +// Helper to create and initialize a NoiseCipherState with ChaChaPoly. +// Returns nullptr on failure. +static NoiseCipherState *create_cipher() { + NoiseCipherState *cipher = nullptr; + int err = noise_cipherstate_new_by_id(&cipher, NOISE_CIPHER_CHACHAPOLY); + if (err != NOISE_ERROR_NONE || cipher == nullptr) + return nullptr; + + // Initialize with a dummy 32-byte key (same pattern as handshake split produces) + uint8_t key[32]; + memset(key, 0xAB, sizeof(key)); + err = noise_cipherstate_init_key(cipher, key, sizeof(key)); + if (err != NOISE_ERROR_NONE) { + noise_cipherstate_free(cipher); + return nullptr; + } + return cipher; +} + +// Benchmark helper matching the exact pattern from +// APINoiseFrameHelper::write_protobuf_messages: +// - noise_buffer_init + noise_buffer_set_inout (same as production) +// - No explicit set_nonce (production relies on internal nonce increment) +// - Error checking on encrypt return +static void noise_encrypt_bench(benchmark::State &state, size_t plaintext_size) { + NoiseCipherState *cipher = create_cipher(); + if (cipher == nullptr) { + state.SkipWithError("Failed to create cipher state"); + return; + } + + size_t mac_len = noise_cipherstate_get_mac_length(cipher); + size_t buf_capacity = plaintext_size + mac_len; + auto buffer = std::make_unique(buf_capacity); + memset(buffer.get(), 0x42, plaintext_size); + + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + // Match production: init buffer, set inout, encrypt + NoiseBuffer mbuf; + noise_buffer_init(mbuf); + noise_buffer_set_inout(mbuf, buffer.get(), plaintext_size, buf_capacity); + + int err = noise_cipherstate_encrypt(cipher, &mbuf); + if (err != NOISE_ERROR_NONE) { + state.SkipWithError("noise_cipherstate_encrypt failed"); + noise_cipherstate_free(cipher); + return; + } + } + benchmark::DoNotOptimize(buffer[0]); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); + + noise_cipherstate_free(cipher); +} + +// --- Encrypt a typical sensor state message (small payload ~14 bytes) --- +// This is the most common message encrypted on every sensor update. +// 4 bytes type+len header + ~10 bytes payload. + +static void NoiseEncrypt_SmallMessage(benchmark::State &state) { noise_encrypt_bench(state, 14); } +BENCHMARK(NoiseEncrypt_SmallMessage); + +// --- Encrypt a medium message (~128 bytes, typical for LightStateResponse) --- + +static void NoiseEncrypt_MediumMessage(benchmark::State &state) { noise_encrypt_bench(state, 128); } +BENCHMARK(NoiseEncrypt_MediumMessage); + +// --- Encrypt a large message (~1024 bytes, typical for DeviceInfoResponse) --- + +static void NoiseEncrypt_LargeMessage(benchmark::State &state) { noise_encrypt_bench(state, 1024); } +BENCHMARK(NoiseEncrypt_LargeMessage); + +// Benchmark helper matching the exact pattern from +// APINoiseFrameHelper::read_packet: +// - noise_buffer_init + noise_buffer_set_inout with capacity == size (decrypt shrinks) +// - Error checking on decrypt return +// +// Pre-encrypts kInnerIterations messages with sequential nonces before the +// timed loop. Each outer iteration re-inits the decrypt key to reset the +// nonce back to 0, then decrypts all pre-encrypted messages in sequence. +// The init_key cost is amortized over kInnerIterations decrypts. +static void noise_decrypt_bench(benchmark::State &state, size_t plaintext_size) { + NoiseCipherState *encrypt_cipher = create_cipher(); + NoiseCipherState *decrypt_cipher = create_cipher(); + if (encrypt_cipher == nullptr || decrypt_cipher == nullptr) { + state.SkipWithError("Failed to create cipher state"); + if (encrypt_cipher) + noise_cipherstate_free(encrypt_cipher); + if (decrypt_cipher) + noise_cipherstate_free(decrypt_cipher); + return; + } + + size_t mac_len = noise_cipherstate_get_mac_length(encrypt_cipher); + size_t encrypted_size = plaintext_size + mac_len; + + // Pre-encrypt kInnerIterations messages with sequential nonces (0..N-1). + auto ciphertexts = std::make_unique(encrypted_size * kInnerIterations); + for (int i = 0; i < kInnerIterations; i++) { + uint8_t *ct = ciphertexts.get() + i * encrypted_size; + memset(ct, 0x42, plaintext_size); + NoiseBuffer enc_buf; + noise_buffer_init(enc_buf); + noise_buffer_set_inout(enc_buf, ct, plaintext_size, encrypted_size); + int err = noise_cipherstate_encrypt(encrypt_cipher, &enc_buf); + if (err != NOISE_ERROR_NONE) { + state.SkipWithError("Pre-encrypt failed"); + noise_cipherstate_free(encrypt_cipher); + noise_cipherstate_free(decrypt_cipher); + return; + } + } + + // Working buffer — decrypt modifies in place + auto buffer = std::make_unique(encrypted_size); + static constexpr uint8_t KEY[32] = {0xAB, 0xAB, 0xAB, 0xAB, 0xAB, 0xAB, 0xAB, 0xAB, 0xAB, 0xAB, 0xAB, + 0xAB, 0xAB, 0xAB, 0xAB, 0xAB, 0xAB, 0xAB, 0xAB, 0xAB, 0xAB, 0xAB, + 0xAB, 0xAB, 0xAB, 0xAB, 0xAB, 0xAB, 0xAB, 0xAB, 0xAB, 0xAB}; + + for (auto _ : state) { + // Reset nonce to 0 by re-initing the key (amortized over kInnerIterations) + noise_cipherstate_init_key(decrypt_cipher, KEY, sizeof(KEY)); + + for (int i = 0; i < kInnerIterations; i++) { + // Copy ciphertext into working buffer (decrypt modifies in place) + memcpy(buffer.get(), ciphertexts.get() + i * encrypted_size, encrypted_size); + + // Decrypt matching production pattern + NoiseBuffer mbuf; + noise_buffer_init(mbuf); + noise_buffer_set_inout(mbuf, buffer.get(), encrypted_size, encrypted_size); + + int err = noise_cipherstate_decrypt(decrypt_cipher, &mbuf); + if (err != NOISE_ERROR_NONE) { + state.SkipWithError("noise_cipherstate_decrypt failed"); + noise_cipherstate_free(encrypt_cipher); + noise_cipherstate_free(decrypt_cipher); + return; + } + } + benchmark::DoNotOptimize(buffer[0]); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); + + noise_cipherstate_free(encrypt_cipher); + noise_cipherstate_free(decrypt_cipher); +} + +// --- Decrypt benchmarks (matching read_packet path) --- + +static void NoiseDecrypt_SmallMessage(benchmark::State &state) { noise_decrypt_bench(state, 14); } +BENCHMARK(NoiseDecrypt_SmallMessage); + +static void NoiseDecrypt_MediumMessage(benchmark::State &state) { noise_decrypt_bench(state, 128); } +BENCHMARK(NoiseDecrypt_MediumMessage); + +static void NoiseDecrypt_LargeMessage(benchmark::State &state) { noise_decrypt_bench(state, 1024); } +BENCHMARK(NoiseDecrypt_LargeMessage); + +} // namespace esphome::api::benchmarks + +#endif // USE_API_NOISE diff --git a/tests/benchmarks/components/api/benchmark.yaml b/tests/benchmarks/components/api/benchmark.yaml index bfc24d7440d..e57276ea660 100644 --- a/tests/benchmarks/components/api/benchmark.yaml +++ b/tests/benchmarks/components/api/benchmark.yaml @@ -108,6 +108,7 @@ esphome: area_id: area_20 api: + encryption: sensor: binary_sensor: light: diff --git a/tests/benchmarks/components/mdns/__init__.py b/tests/benchmarks/components/mdns/__init__.py new file mode 100644 index 00000000000..b08f67a0956 --- /dev/null +++ b/tests/benchmarks/components/mdns/__init__.py @@ -0,0 +1,5 @@ +from tests.testing_helpers import ComponentManifestOverride + + +def override_manifest(manifest: ComponentManifestOverride) -> None: + manifest.enable_codegen() diff --git a/tests/benchmarks/components/network/__init__.py b/tests/benchmarks/components/network/__init__.py new file mode 100644 index 00000000000..b08f67a0956 --- /dev/null +++ b/tests/benchmarks/components/network/__init__.py @@ -0,0 +1,5 @@ +from tests.testing_helpers import ComponentManifestOverride + + +def override_manifest(manifest: ComponentManifestOverride) -> None: + manifest.enable_codegen() diff --git a/tests/benchmarks/components/socket/__init__.py b/tests/benchmarks/components/socket/__init__.py new file mode 100644 index 00000000000..7a20f9f2300 --- /dev/null +++ b/tests/benchmarks/components/socket/__init__.py @@ -0,0 +1,7 @@ +from tests.testing_helpers import ComponentManifestOverride + + +def override_manifest(manifest: ComponentManifestOverride) -> None: + # socket must run its to_code to define USE_SOCKET_IMPL_BSD_SOCKETS + # which is needed by the api frame helper benchmarks. + manifest.enable_codegen()