From 48cdf9e036d402d84518ee0256f7db24306e6b3e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 20 Dec 2025 05:47:29 -1000 Subject: [PATCH 1/5] [tests] Fix race condition in alarm control panel state transitions test (#12581) --- ...t_alarm_control_panel_state_transitions.py | 26 +++++++++++++++---- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/tests/integration/test_alarm_control_panel_state_transitions.py b/tests/integration/test_alarm_control_panel_state_transitions.py index 2977ff56c2..09348f5bea 100644 --- a/tests/integration/test_alarm_control_panel_state_transitions.py +++ b/tests/integration/test_alarm_control_panel_state_transitions.py @@ -279,14 +279,30 @@ async def test_alarm_control_panel_state_transitions( except TimeoutError: pytest.fail(f"on_chime callback not fired. Log lines: {log_lines[-20:]}") - # Close the chime sensor + # Close the chime sensor and wait for alarm to become ready again + # We need to wait for this transition before testing door sensor, + # otherwise there's a race where the door sensor state change could + # arrive before the chime sensor state change, leaving the alarm in + # a continuous "not ready" state with no on_ready callback fired. + ready_after_chime_close: asyncio.Future[bool] = loop.create_future() + ready_futures.append(ready_after_chime_close) + client.switch_command(chime_switch_info.key, False) - # ===== Test ready state changes ===== - # Opening/closing sensors while disarmed affects ready state - # The on_ready callback fires when sensors_ready changes + # Wait for alarm to become ready again (chime sensor closed) + try: + await asyncio.wait_for(ready_after_chime_close, timeout=2.0) + except TimeoutError: + pytest.fail( + f"on_ready callback not fired when chime sensor closed. " + f"Log lines: {log_lines[-20:]}" + ) - # Set up futures for ready state changes + # ===== Test ready state changes ===== + # Now the alarm is confirmed ready. Opening/closing door sensor + # should trigger on_ready callbacks. + + # Set up futures for door sensor state changes ready_future_1: asyncio.Future[bool] = loop.create_future() ready_future_2: asyncio.Future[bool] = loop.create_future() ready_futures.extend([ready_future_1, ready_future_2]) From 121375ff392260487ed9dfcc2872ffccb15f6da3 Mon Sep 17 00:00:00 2001 From: Eduard Llull Date: Sat, 20 Dec 2025 16:59:14 +0100 Subject: [PATCH 2/5] [display_menu_base] Call on_value_ after updating the select (#12584) --- esphome/components/display_menu_base/menu_item.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/esphome/components/display_menu_base/menu_item.cpp b/esphome/components/display_menu_base/menu_item.cpp index 8224adf3fe..08f758045e 100644 --- a/esphome/components/display_menu_base/menu_item.cpp +++ b/esphome/components/display_menu_base/menu_item.cpp @@ -54,6 +54,7 @@ bool MenuItemSelect::select_next() { if (this->select_var_ != nullptr) { this->select_var_->make_call().select_next(true).perform(); + this->on_value_(); changed = true; } @@ -65,6 +66,7 @@ bool MenuItemSelect::select_prev() { if (this->select_var_ != nullptr) { this->select_var_->make_call().select_previous(true).perform(); + this->on_value_(); changed = true; } From 64269334ce9e1c5ae70268f679c550c7d62686de Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 20 Dec 2025 06:46:13 -1000 Subject: [PATCH 3/5] [text_sensor] Avoid string copies in callbacks by passing const ref (#12503) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> --- esphome/components/text_sensor/text_sensor.cpp | 4 ++-- esphome/components/text_sensor/text_sensor.h | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/esphome/components/text_sensor/text_sensor.cpp b/esphome/components/text_sensor/text_sensor.cpp index 76c1acf56c..ad1dc0f521 100644 --- a/esphome/components/text_sensor/text_sensor.cpp +++ b/esphome/components/text_sensor/text_sensor.cpp @@ -71,10 +71,10 @@ void TextSensor::clear_filters() { this->filter_list_ = nullptr; } -void TextSensor::add_on_state_callback(std::function callback) { +void TextSensor::add_on_state_callback(std::function callback) { this->callback_.add(std::move(callback)); } -void TextSensor::add_on_raw_state_callback(std::function callback) { +void TextSensor::add_on_raw_state_callback(std::function callback) { this->raw_callback_.add(std::move(callback)); } diff --git a/esphome/components/text_sensor/text_sensor.h b/esphome/components/text_sensor/text_sensor.h index f926f171a7..919bf81c8c 100644 --- a/esphome/components/text_sensor/text_sensor.h +++ b/esphome/components/text_sensor/text_sensor.h @@ -55,9 +55,9 @@ class TextSensor : public EntityBase, public EntityBase_DeviceClass { /// Clear the entire filter chain. void clear_filters(); - void add_on_state_callback(std::function callback); + void add_on_state_callback(std::function callback); /// Add a callback that will be called every time the sensor sends a raw value. - void add_on_raw_state_callback(std::function callback); + void add_on_raw_state_callback(std::function callback); // ========== INTERNAL METHODS ========== // (In most use cases you won't need these) @@ -65,8 +65,8 @@ class TextSensor : public EntityBase, public EntityBase_DeviceClass { void internal_send_state_to_frontend(const std::string &state); protected: - LazyCallbackManager raw_callback_; ///< Storage for raw state callbacks. - LazyCallbackManager callback_; ///< Storage for filtered state callbacks. + LazyCallbackManager raw_callback_; ///< Storage for raw state callbacks. + LazyCallbackManager callback_; ///< Storage for filtered state callbacks. Filter *filter_list_{nullptr}; ///< Store all active filters. }; From 40eb898814aec3fe077bf0cfc0b2101934970cf6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 20 Dec 2025 06:47:30 -1000 Subject: [PATCH 4/5] [api] Add zero-copy support for noise encryption key requests (#12405) --- esphome/components/api/api.proto | 2 +- esphome/components/api/api_connection.cpp | 4 ++-- esphome/components/api/api_pb2.cpp | 7 +++++-- esphome/components/api/api_pb2.h | 5 +++-- esphome/components/api/api_pb2_dump.cpp | 2 +- esphome/core/helpers.cpp | 12 ++++++++---- esphome/core/helpers.h | 1 + 7 files changed, 21 insertions(+), 12 deletions(-) diff --git a/esphome/components/api/api.proto b/esphome/components/api/api.proto index e8c900df26..5d44d7e549 100644 --- a/esphome/components/api/api.proto +++ b/esphome/components/api/api.proto @@ -747,7 +747,7 @@ message NoiseEncryptionSetKeyRequest { option (source) = SOURCE_CLIENT; option (ifdef) = "USE_API_NOISE"; - bytes key = 1; + bytes key = 1 [(pointer_to_buffer) = true]; } message NoiseEncryptionSetKeyResponse { diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 126d3cb220..0f551d1bc3 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -1666,13 +1666,13 @@ bool APIConnection::send_noise_encryption_set_key_response(const NoiseEncryption resp.success = false; psk_t psk{}; - if (msg.key.empty()) { + if (msg.key_len == 0) { if (this->parent_->clear_noise_psk(true)) { resp.success = true; } else { ESP_LOGW(TAG, "Failed to clear encryption key"); } - } else if (base64_decode(msg.key, psk.data(), psk.size()) != psk.size()) { + } else if (base64_decode(msg.key, msg.key_len, psk.data(), psk.size()) != psk.size()) { ESP_LOGW(TAG, "Invalid encryption key length"); } else if (!this->parent_->save_noise_psk(psk, true)) { ESP_LOGW(TAG, "Failed to save encryption key"); diff --git a/esphome/components/api/api_pb2.cpp b/esphome/components/api/api_pb2.cpp index 8bba13a4de..8b84f9651f 100644 --- a/esphome/components/api/api_pb2.cpp +++ b/esphome/components/api/api_pb2.cpp @@ -858,9 +858,12 @@ void SubscribeLogsResponse::calculate_size(ProtoSize &size) const { #ifdef USE_API_NOISE bool NoiseEncryptionSetKeyRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) { switch (field_id) { - case 1: - this->key = value.as_string(); + case 1: { + // Use raw data directly to avoid allocation + this->key = value.data(); + this->key_len = value.size(); break; + } default: return false; } diff --git a/esphome/components/api/api_pb2.h b/esphome/components/api/api_pb2.h index d3b91ac56b..668c0af461 100644 --- a/esphome/components/api/api_pb2.h +++ b/esphome/components/api/api_pb2.h @@ -1054,11 +1054,12 @@ class SubscribeLogsResponse final : public ProtoMessage { class NoiseEncryptionSetKeyRequest final : public ProtoDecodableMessage { public: static constexpr uint8_t MESSAGE_TYPE = 124; - static constexpr uint8_t ESTIMATED_SIZE = 9; + static constexpr uint8_t ESTIMATED_SIZE = 19; #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "noise_encryption_set_key_request"; } #endif - std::string key{}; + const uint8_t *key{nullptr}; + uint16_t key_len{0}; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif diff --git a/esphome/components/api/api_pb2_dump.cpp b/esphome/components/api/api_pb2_dump.cpp index d733e66a6d..38c3b473e6 100644 --- a/esphome/components/api/api_pb2_dump.cpp +++ b/esphome/components/api/api_pb2_dump.cpp @@ -1115,7 +1115,7 @@ void SubscribeLogsResponse::dump_to(std::string &out) const { void NoiseEncryptionSetKeyRequest::dump_to(std::string &out) const { MessageDumpHelper helper(out, "NoiseEncryptionSetKeyRequest"); out.append(" key: "); - out.append(format_hex_pretty(reinterpret_cast(this->key.data()), this->key.size())); + out.append(format_hex_pretty(this->key, this->key_len)); out.append("\n"); } void NoiseEncryptionSetKeyResponse::dump_to(std::string &out) const { dump_field(out, "success", this->success); } diff --git a/esphome/core/helpers.cpp b/esphome/core/helpers.cpp index bbe59e53f1..156f41a2dc 100644 --- a/esphome/core/helpers.cpp +++ b/esphome/core/helpers.cpp @@ -479,10 +479,14 @@ std::string base64_encode(const uint8_t *buf, size_t buf_len) { } size_t base64_decode(const std::string &encoded_string, uint8_t *buf, size_t buf_len) { - int in_len = encoded_string.size(); + return base64_decode(reinterpret_cast(encoded_string.data()), encoded_string.size(), buf, buf_len); +} + +size_t base64_decode(const uint8_t *encoded_data, size_t encoded_len, uint8_t *buf, size_t buf_len) { + size_t in_len = encoded_len; int i = 0; int j = 0; - int in = 0; + size_t in = 0; size_t out = 0; uint8_t char_array_4[4], char_array_3[3]; bool truncated = false; @@ -490,8 +494,8 @@ size_t base64_decode(const std::string &encoded_string, uint8_t *buf, size_t buf // SAFETY: The loop condition checks is_base64() before processing each character. // This ensures base64_find_char() is only called on valid base64 characters, // preventing the edge case where invalid chars would return 0 (same as 'A'). - while (in_len-- && (encoded_string[in] != '=') && is_base64(encoded_string[in])) { - char_array_4[i++] = encoded_string[in]; + while (in_len-- && (encoded_data[in] != '=') && is_base64(encoded_data[in])) { + char_array_4[i++] = encoded_data[in]; in++; if (i == 4) { for (i = 0; i < 4; i++) diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index 9ff2458a74..6028c93ce2 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -878,6 +878,7 @@ std::string base64_encode(const std::vector &buf); std::vector base64_decode(const std::string &encoded_string); size_t base64_decode(std::string const &encoded_string, uint8_t *buf, size_t buf_len); +size_t base64_decode(const uint8_t *encoded_data, size_t encoded_len, uint8_t *buf, size_t buf_len); ///@} From f470cf5c8732b1ac23142cddb1051be53ff81b87 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 20 Dec 2025 10:05:50 -1000 Subject: [PATCH 5/5] add missing USE_API guard --- esphome/components/zwave_proxy/zwave_proxy.cpp | 5 +++++ esphome/components/zwave_proxy/zwave_proxy.h | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/esphome/components/zwave_proxy/zwave_proxy.cpp b/esphome/components/zwave_proxy/zwave_proxy.cpp index e0ca5529b8..bd3f85772b 100644 --- a/esphome/components/zwave_proxy/zwave_proxy.cpp +++ b/esphome/components/zwave_proxy/zwave_proxy.cpp @@ -1,4 +1,7 @@ #include "zwave_proxy.h" + +#ifdef USE_API + #include "esphome/components/api/api_server.h" #include "esphome/core/application.h" #include "esphome/core/helpers.h" @@ -344,3 +347,5 @@ bool ZWaveProxy::response_handler_() { ZWaveProxy *global_zwave_proxy = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) } // namespace esphome::zwave_proxy + +#endif // USE_API diff --git a/esphome/components/zwave_proxy/zwave_proxy.h b/esphome/components/zwave_proxy/zwave_proxy.h index e23e202bea..137a1206e3 100644 --- a/esphome/components/zwave_proxy/zwave_proxy.h +++ b/esphome/components/zwave_proxy/zwave_proxy.h @@ -1,5 +1,8 @@ #pragma once +#include "esphome/core/defines.h" +#ifdef USE_API + #include "esphome/components/api/api_connection.h" #include "esphome/components/api/api_pb2.h" #include "esphome/core/component.h" @@ -89,3 +92,5 @@ class ZWaveProxy : public uart::UARTDevice, public Component { extern ZWaveProxy *global_zwave_proxy; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) } // namespace esphome::zwave_proxy + +#endif // USE_API