Compare commits

...
Author SHA1 Message Date
J. Nick Koston f162fce638 Add host unit tests for the noise component 2026-08-23 09:41:15 -05:00
J. Nick Koston 3730f9137c Refactor the api noise handshake onto the shared responder 2026-08-23 09:41:15 -05:00
J. Nick Koston 3a350484c1 Share the noise wire constants and reject formatter 2026-08-23 09:41:15 -05:00
J. Nick Koston 04e2977609 Move encryption key validation into the noise component 2026-08-23 09:41:15 -05:00
J. Nick Koston 08231c91d4 Add shared noise component and move noise-c primitives out of api 2026-08-23 09:41:15 -05:00
J. Nick KostonandGitHub cf31c08a5c [core] Skip copying entity automation and filter sources when unused (#18602) 2026-08-23 09:05:04 -05:00
J. Nick KostonandGitHub e7574a574b [ota] Restore lazy flash erase for ESP32 OTA with 64 KiB block erase (#18580) 2026-08-23 09:04:48 -05:00
J. Nick KostonandGitHub 33484108a9 [core] Replace a damaged existing file in write_file_if_changed (#18665) 2026-08-23 09:04:24 -05:00
J. Nick KostonandGitHub e697a40fda [core] Register the OTA component in dummy_main like its siblings (#18666) 2026-08-23 09:04:06 -05:00
J. Nick KostonandGitHub d1f065671e [http_request] Abort OTA backend when update fails before first write (#18581) 2026-08-22 22:21:04 -05:00
J. Nick KostonandGitHub 02da5c6484 [ethernet] Remove deprecated get_eth_mac_address_pretty() (#18379) 2026-08-22 22:02:41 -05:00
J. Nick KostonandGitHub b2440cb655 [modbus] Remove deprecated waiting_for_response() (#18381) 2026-08-22 22:02:22 -05:00
J. Nick KostonandGitHub 160d8b8f0c [web_server_idf] Remove deprecated AsyncWebServerRequest::url() (#18382) 2026-08-22 22:02:05 -05:00
J. Nick KostonandGitHub 8899713ef9 [core] Remove deprecated gamma_correct and gamma_uncorrect (#18376) 2026-08-22 22:00:53 -05:00
J. Nick KostonandGitHub f3cdefce21 [wifi] Remove deprecated wifi_ssid() (#18378) 2026-08-22 22:00:38 -05:00
J. Nick KostonandGitHub b115813fbe [esp32] Report abort and task watchdog panics correctly in crash handler (#18575) 2026-08-22 22:00:17 -05:00
J. Nick KostonandGitHub ab45ab316a [core] Remove deprecated entity_base getters (#18375) 2026-08-22 22:00:02 -05:00
J. Nick KostonandGitHub 5b3a6c05bf [core] Remove deprecated esp_log_vprintf_ flash-string overload (#18377) 2026-08-22 21:59:47 -05:00
J. Nick KostonandGitHub cd53681787 [wifi] Inline the remaining trivial WiFiAP and WiFiComponent accessors (#18617) 2026-08-23 02:51:39 +00:00
69 changed files with 1293 additions and 552 deletions
+1
View File
@@ -381,6 +381,7 @@ esphome/components/nextion/switch/* @senexcrenshaw
esphome/components/nextion/text_sensor/* @senexcrenshaw
esphome/components/nfc/* @jesserockz @kbx81
esphome/components/noblex/* @AGalfra
esphome/components/noise/* @esphome/core
esphome/components/npi19/* @bakerkj
esphome/components/nrf52/* @tomaszduda23
esphome/components/number/* @esphome/core
+22 -34
View File
@@ -1,4 +1,3 @@
import base64
import logging
from typing import Any
@@ -6,6 +5,15 @@ from esphome import automation
from esphome.automation import Condition
import esphome.codegen as cg
from esphome.components.logger import request_log_listener
# ENCRYPTION_SCHEMA and validate_encryption_key are re-exported for external
# components and downstream consumers that import them from api
from esphome.components.noise import ( # noqa: F401
ENCRYPTION_SCHEMA,
decode_encryption_key,
encryption_schema,
validate_encryption_key,
)
from esphome.config_helpers import get_logger_level
import esphome.config_validation as cv
from esphome.const import (
@@ -38,6 +46,10 @@ from esphome.core import CORE, ID, CoroPriority, EsphomeError, coroutine_with_pr
from esphome.cpp_generator import MockObj, TemplateArgsType
from esphome.types import ConfigFragmentType, ConfigType
# Compat alias: downstream consumers (e.g. device-builder) referenced the
# schema by its old private name before it moved to the noise component
_encryption_schema = encryption_schema
_LOGGER = logging.getLogger(__name__)
DOMAIN = "api"
@@ -46,9 +58,15 @@ CODEOWNERS = ["@esphome/core"]
def AUTO_LOAD(config: ConfigType) -> list[str]:
"""Conditionally auto-load json only when capture_response is used."""
"""Conditionally auto-load noise (encryption) and json (capture_response)."""
base = ["socket"]
# A falsy config is a tooling probe for the maximal set (None from
# dependency resolution, {} from the components-graph platform probe);
# a validated config always carries defaults, never empty
if not config or CONF_ENCRYPTION in config:
base = base + ["noise"]
# Check if any homeassistant.action/homeassistant.service has capture_response: true
# This flag is set during config validation in _validate_response_config
if not config or CORE.data.get(DOMAIN, {}).get(CONF_CAPTURE_RESPONSE, False):
@@ -130,20 +148,6 @@ def _register_provisioning_source(config: ConfigType) -> ConfigType:
return config
def validate_encryption_key(value: Any) -> str:
value = cv.string_strict(value)
try:
decoded = base64.b64decode(value, validate=True)
except ValueError as err:
raise cv.Invalid("Invalid key format, please check it's using base64") from err
if len(decoded) != 32:
raise cv.Invalid("Encryption key must be base64 and 32 bytes long")
# Return original data for roundtrip conversion
return value
CONF_SUPPORTS_RESPONSE = "supports_response"
# Enum values in api::enums namespace
@@ -250,18 +254,6 @@ ACTIONS_SCHEMA = automation.validate_automation(
),
)
ENCRYPTION_SCHEMA = cv.Schema(
{
cv.Optional(CONF_KEY): cv.sensitive(validate_encryption_key),
}
)
def _encryption_schema(config: ConfigType | None) -> ConfigType:
if config is None:
config = {}
return ENCRYPTION_SCHEMA(config)
def _consume_api_sockets(config: ConfigType) -> ConfigType:
"""Register socket needs for API component."""
@@ -297,7 +289,7 @@ CONFIG_SCHEMA = cv.All(
CONF_SERVICES, group_of_exclusion=CONF_ACTIONS
): ACTIONS_SCHEMA,
cv.Exclusive(CONF_ACTIONS, group_of_exclusion=CONF_ACTIONS): ACTIONS_SCHEMA,
cv.Optional(CONF_ENCRYPTION): _encryption_schema,
cv.Optional(CONF_ENCRYPTION): encryption_schema,
cv.Optional(CONF_BATCH_DELAY, default="100ms"): cv.All(
cv.positive_time_period_milliseconds,
cv.Range(max=cv.TimePeriod(milliseconds=65535)),
@@ -484,7 +476,7 @@ async def to_code(config: ConfigType) -> None:
if (encryption_config := config.get(CONF_ENCRYPTION, None)) is not None:
if key := encryption_config.get(CONF_KEY):
decoded = base64.b64decode(key)
decoded = decode_encryption_key(key)
cg.add(var.set_noise_psk(list(decoded)))
cg.add_define("USE_API_NOISE_PSK_FROM_YAML")
else:
@@ -498,10 +490,6 @@ async def to_code(config: ConfigType) -> None:
# and plaintext disabled. Only a factory reset can remove it.
cg.add_define("USE_API_PLAINTEXT")
cg.add_define("USE_API_NOISE")
cg.add_library("esphome/noise-c", "0.1.21")
# 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")
+2 -2
View File
@@ -2130,7 +2130,7 @@ bool APIConnection::send_noise_encryption_set_key_response_(const NoiseEncryptio
}
#endif
psk_t psk{};
noise::psk_t psk{};
if (msg.key_len == 0) {
if (this->parent_->clear_noise_psk(true)) {
resp.success = true;
@@ -2139,7 +2139,7 @@ bool APIConnection::send_noise_encryption_set_key_response_(const NoiseEncryptio
}
} else if (base64_decode(msg.key, msg.key_len, psk.data(), psk.size()) != psk.size()) {
ESP_LOGW(TAG, "Invalid encryption key length");
} else if (APINoiseContext::is_all_zeros(psk)) {
} else if (noise::NoiseContext::is_all_zeros(psk)) {
// Accepting the reserved provisioning PSK would report success without
// enabling encryption (or silently clear an existing key)
ESP_LOGW(TAG, "Rejecting all-zero encryption key");
+55 -155
View File
@@ -2,9 +2,9 @@
#ifdef USE_API
#ifdef USE_API_NOISE
#include "api_connection.h" // For ClientInfo struct
#include "esphome/components/noise/noise.h"
#include "esphome/core/application.h"
#include "esphome/core/entity_base.h"
#include "esphome/core/hal.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include "proto.h"
@@ -17,6 +17,14 @@
namespace esphome::api {
using noise::noise_err_to_logstr;
// api_frame_helper.h keeps its own MAX_HANDSHAKE_SIZE because that header is
// also compiled in plaintext-only builds without the noise component; keep
// the two definitions from drifting apart.
static_assert(MAX_HANDSHAKE_SIZE == noise::MAX_HANDSHAKE_SIZE,
"api and noise component handshake size limits must match");
static const char *const TAG = "api.noise";
#ifdef USE_ESP8266
static constexpr char PROLOGUE_INIT[] PROGMEM = "NoiseAPIInit";
@@ -51,45 +59,6 @@ static constexpr size_t API_MAX_LOG_BYTES = 168;
#define LOG_PACKET_RECEIVED(buffer) ((void) 0)
#endif
/// Convert a noise error code to a readable error
const LogString *noise_err_to_logstr(int err) {
if (err == NOISE_ERROR_NO_MEMORY)
return LOG_STR("NO_MEMORY");
if (err == NOISE_ERROR_UNKNOWN_ID)
return LOG_STR("UNKNOWN_ID");
if (err == NOISE_ERROR_UNKNOWN_NAME)
return LOG_STR("UNKNOWN_NAME");
if (err == NOISE_ERROR_MAC_FAILURE)
return LOG_STR("MAC_FAILURE");
if (err == NOISE_ERROR_NOT_APPLICABLE)
return LOG_STR("NOT_APPLICABLE");
if (err == NOISE_ERROR_SYSTEM)
return LOG_STR("SYSTEM");
if (err == NOISE_ERROR_REMOTE_KEY_REQUIRED)
return LOG_STR("REMOTE_KEY_REQUIRED");
if (err == NOISE_ERROR_LOCAL_KEY_REQUIRED)
return LOG_STR("LOCAL_KEY_REQUIRED");
if (err == NOISE_ERROR_PSK_REQUIRED)
return LOG_STR("PSK_REQUIRED");
if (err == NOISE_ERROR_INVALID_LENGTH)
return LOG_STR("INVALID_LENGTH");
if (err == NOISE_ERROR_INVALID_PARAM)
return LOG_STR("INVALID_PARAM");
if (err == NOISE_ERROR_INVALID_STATE)
return LOG_STR("INVALID_STATE");
if (err == NOISE_ERROR_INVALID_NONCE)
return LOG_STR("INVALID_NONCE");
if (err == NOISE_ERROR_INVALID_PRIVATE_KEY)
return LOG_STR("INVALID_PRIVATE_KEY");
if (err == NOISE_ERROR_INVALID_PUBLIC_KEY)
return LOG_STR("INVALID_PUBLIC_KEY");
if (err == NOISE_ERROR_INVALID_FORMAT)
return LOG_STR("INVALID_FORMAT");
if (err == NOISE_ERROR_INVALID_SIGNATURE)
return LOG_STR("INVALID_SIGNATURE");
return LOG_STR("UNKNOWN");
}
/// Initialize the frame helper, returns OK if successful.
APIError APINoiseFrameHelper::init() {
APIError err = init_common_();
@@ -194,9 +163,9 @@ APIError APINoiseFrameHelper::loop() {
*/
APIError APINoiseFrameHelper::try_read_frame_() {
// read header
if (rx_header_buf_len_ < 3) {
if (rx_header_buf_len_ < noise::FRAME_HEADER_SIZE) {
// no header information yet
uint8_t to_read = 3 - rx_header_buf_len_;
uint8_t to_read = static_cast<uint8_t>(noise::FRAME_HEADER_SIZE) - rx_header_buf_len_;
ssize_t received = this->socket_->read(&rx_header_buf_[rx_header_buf_len_], to_read);
APIError err = handle_socket_read_result_(received);
if (err != APIError::OK) {
@@ -208,7 +177,7 @@ APIError APINoiseFrameHelper::try_read_frame_() {
return APIError::WOULD_BLOCK;
}
if (rx_header_buf_[0] != 0x01) {
if (rx_header_buf_[0] != noise::FRAME_INDICATOR) {
state_ = State::FAILED;
HELPER_LOG("Bad indicator byte %u", rx_header_buf_[0]);
return APIError::BAD_INDICATOR;
@@ -348,15 +317,15 @@ APIError APINoiseFrameHelper::state_action_server_hello_() {
return APIError::OK;
}
APIError APINoiseFrameHelper::state_action_handshake_() {
int action = noise_handshakestate_get_action(this->handshake_);
if (action == NOISE_ACTION_READ_MESSAGE) {
noise::NoiseResponderHandshake::Action action = this->handshake_.action();
if (action == noise::NoiseResponderHandshake::Action::ACTION_READ) {
return this->state_action_handshake_read_();
} else if (action == NOISE_ACTION_WRITE_MESSAGE) {
} else if (action == noise::NoiseResponderHandshake::Action::ACTION_WRITE) {
return this->state_action_handshake_write_();
}
// bad state for action
this->state_ = State::FAILED;
HELPER_LOG("Bad action for handshake: %d", action);
HELPER_LOG("Bad action for handshake: %d", (int) action);
return APIError::HANDSHAKESTATE_BAD_STATE;
}
APIError APINoiseFrameHelper::state_action_handshake_read_() {
@@ -368,20 +337,16 @@ APIError APINoiseFrameHelper::state_action_handshake_read_() {
if (this->rx_buf_.empty()) {
this->send_explicit_handshake_reject_(LOG_STR("Empty handshake message"));
return APIError::BAD_HANDSHAKE_ERROR_BYTE;
} else if (this->rx_buf_[0] != 0x00) {
} else if (this->rx_buf_[0] != noise::HANDSHAKE_STATUS_OK) {
HELPER_LOG("Bad handshake error byte: %u", this->rx_buf_[0]);
this->send_explicit_handshake_reject_(LOG_STR("Bad handshake error byte"));
return APIError::BAD_HANDSHAKE_ERROR_BYTE;
}
NoiseBuffer mbuf;
noise_buffer_init(mbuf);
noise_buffer_set_input(mbuf, this->rx_buf_.data() + 1, this->rx_buf_.size() - 1);
int err = noise_handshakestate_read_message(this->handshake_, &mbuf, nullptr);
int err = this->handshake_.read_message(this->rx_buf_.data() + 1, this->rx_buf_.size() - 1);
if (err != 0) {
// Special handling for MAC failure
this->send_explicit_handshake_reject_(err == NOISE_ERROR_MAC_FAILURE ? LOG_STR("Handshake MAC failure")
: LOG_STR("Handshake error"));
this->send_explicit_handshake_reject_(noise::reject_reason_for(err));
return this->handle_noise_error_(err, LOG_STR("noise_handshakestate_read_message"),
APIError::HANDSHAKESTATE_READ_FAILED);
}
@@ -390,18 +355,16 @@ APIError APINoiseFrameHelper::state_action_handshake_read_() {
}
APIError APINoiseFrameHelper::state_action_handshake_write_() {
uint8_t buffer[65];
NoiseBuffer mbuf;
noise_buffer_init(mbuf);
noise_buffer_set_output(mbuf, buffer + 1, sizeof(buffer) - 1);
size_t msg_len = 0;
int err = noise_handshakestate_write_message(this->handshake_, &mbuf, nullptr);
int err = this->handshake_.write_message(buffer + 1, sizeof(buffer) - 1, msg_len);
APIError aerr = this->handle_noise_error_(err, LOG_STR("noise_handshakestate_write_message"),
APIError::HANDSHAKESTATE_WRITE_FAILED);
if (aerr != APIError::OK)
return aerr;
buffer[0] = 0x00; // success
buffer[0] = noise::HANDSHAKE_STATUS_OK;
aerr = this->write_frame_(buffer, mbuf.size + 1);
aerr = this->write_frame_(buffer, msg_len + 1);
if (aerr != APIError::OK)
return aerr;
return this->check_handshake_finished_();
@@ -409,33 +372,22 @@ APIError APINoiseFrameHelper::state_action_handshake_write_() {
void APINoiseFrameHelper::send_explicit_handshake_reject_(const LogString *reason) {
// Max reject message: "Bad handshake packet len" (24) + 1 (failure byte) = 25 bytes
uint8_t data[32];
data[0] = 0x01; // failure
#ifdef USE_STORE_LOG_STR_IN_FLASH
// On ESP8266 with flash strings, we need to use PROGMEM-aware functions
size_t reason_len = strlen_P(reinterpret_cast<PGM_P>(reason));
reason_len = std::min(reason_len, sizeof(data) - 1);
if (reason_len > 0) {
memcpy_P(data + 1, reinterpret_cast<PGM_P>(reason), reason_len);
}
#else
// Normal memory access
const char *reason_str = LOG_STR_ARG(reason);
size_t reason_len = strlen(reason_str);
reason_len = std::min(reason_len, sizeof(data) - 1);
if (reason_len > 0) {
// NOLINTNEXTLINE(bugprone-not-null-terminated-result) - binary protocol, not a C string
std::memcpy(data + 1, reason_str, reason_len);
}
#endif
size_t data_size = reason_len + 1;
static_assert(sizeof(data) >= noise::MAC_FAILURE_PAYLOAD_SIZE,
"reject buffer must fit the MAC failure wire contract");
size_t data_size = noise::format_reject_payload(data, sizeof(data), reason);
// temporarily remove failed state
auto orig_state = state_;
state_ = State::EXPLICIT_REJECT;
write_frame_(data, data_size);
state_ = orig_state;
APIError aerr = write_frame_(data, data_size);
if (aerr != APIError::OK) {
// Best effort; the reject reason is a diagnosis aid, not a protocol step
ESP_LOGW(TAG, "Sending handshake reject failed: %d", (int) aerr);
}
if (state_ == State::EXPLICIT_REJECT) {
// write_frame_ may have moved the state to FAILED; keep that decision
state_ = orig_state;
}
}
APIError APINoiseFrameHelper::read_packet(ReadPacketBuffer *buffer) {
APIError aerr = this->check_data_state_();
@@ -492,12 +444,10 @@ APIError APINoiseFrameHelper::read_packet(ReadPacketBuffer *buffer) {
// Returns APIError::OK on success.
APIError APINoiseFrameHelper::encrypt_noise_message_(uint8_t *buf_start, uint16_t payload_size, uint8_t message_type,
uint16_t &encrypted_len_out) {
// Write noise header
buf_start[0] = 0x01; // indicator
// buf_start[1], buf_start[2] to be set after encryption
// The noise frame header is written after encryption, when the size is known
// Write message header (to be encrypted)
constexpr uint8_t msg_offset = 3;
constexpr uint8_t msg_offset = noise::FRAME_HEADER_SIZE;
buf_start[msg_offset] = static_cast<uint8_t>(message_type >> 8); // type high byte
buf_start[msg_offset + 1] = static_cast<uint8_t>(message_type); // type low byte
buf_start[msg_offset + 2] = static_cast<uint8_t>(payload_size >> 8); // data_len high byte
@@ -515,11 +465,10 @@ APIError APINoiseFrameHelper::encrypt_noise_message_(uint8_t *buf_start, uint16_
if (aerr != APIError::OK)
return aerr;
// Fill in the encrypted size
buf_start[1] = static_cast<uint8_t>(mbuf.size >> 8);
buf_start[2] = static_cast<uint8_t>(mbuf.size);
// Fill in the frame header now that the encrypted size is known
noise::write_frame_header(buf_start, static_cast<uint16_t>(mbuf.size));
encrypted_len_out = static_cast<uint16_t>(3 + mbuf.size); // indicator + size + encrypted data
encrypted_len_out = static_cast<uint16_t>(noise::FRAME_HEADER_SIZE + mbuf.size);
return APIError::OK;
}
@@ -568,21 +517,19 @@ APIError APINoiseFrameHelper::write_protobuf_messages(ProtoWriteBuffer buffer, s
}
APIError APINoiseFrameHelper::write_frame_(const uint8_t *data, uint16_t len) {
uint8_t header[3];
header[0] = 0x01; // indicator
header[1] = (uint8_t) (len >> 8);
header[2] = (uint8_t) len;
uint8_t header[noise::FRAME_HEADER_SIZE];
noise::write_frame_header(header, len);
if (len == 0) {
return this->write_raw_buf_(header, 3);
return this->write_raw_buf_(header, noise::FRAME_HEADER_SIZE);
}
struct iovec iov[2];
iov[0].iov_base = header;
iov[0].iov_len = 3;
iov[0].iov_len = noise::FRAME_HEADER_SIZE;
iov[1].iov_base = const_cast<uint8_t *>(data);
iov[1].iov_len = len;
return this->write_raw_iov_(iov, 2, 3 + len);
return this->write_raw_iov_(iov, 2, noise::FRAME_HEADER_SIZE + len);
}
/** Initiate the data structures for the handshake.
@@ -590,45 +537,12 @@ APIError APINoiseFrameHelper::write_frame_(const uint8_t *data, uint16_t len) {
* @return 0 on success, -1 on error (check errno)
*/
APIError APINoiseFrameHelper::init_handshake_() {
int err;
// Noise_NNpsk0_25519_ChaChaPoly_SHA256, built on the stack:
// noise_handshakestate_new_by_id copies it, so a member would waste
// 104 bytes per connection, and a static const would sit in RAM on
// ESP8266 (.rodata is DRAM there).
const NoiseProtocolId nid = {
.prefix_id = NOISE_PREFIX_STANDARD,
.pattern_id = NOISE_PATTERN_NN,
.modifier_ids = {NOISE_MODIFIER_PSK0},
.dh_id = NOISE_DH_CURVE25519,
.cipher_id = NOISE_CIPHER_CHACHAPOLY,
.hash_id = NOISE_HASH_SHA256,
.hybrid_id = NOISE_DH_NONE,
};
err = noise_handshakestate_new_by_id(&handshake_, &nid, NOISE_ROLE_RESPONDER);
APIError aerr =
handle_noise_error_(err, LOG_STR("noise_handshakestate_new_by_id"), APIError::HANDSHAKESTATE_SETUP_FAILED);
int err = this->handshake_.init(this->ctx_.get_psk(), prologue_.data(), prologue_.size());
APIError aerr = handle_noise_error_(err, LOG_STR("noise_handshake_init"), APIError::HANDSHAKESTATE_SETUP_FAILED);
if (aerr != APIError::OK)
return aerr;
const auto &psk = this->ctx_.get_psk();
err = noise_handshakestate_set_pre_shared_key(handshake_, psk.data(), psk.size());
aerr = handle_noise_error_(err, LOG_STR("noise_handshakestate_set_pre_shared_key"),
APIError::HANDSHAKESTATE_SETUP_FAILED);
if (aerr != APIError::OK)
return aerr;
err = noise_handshakestate_set_prologue(handshake_, prologue_.data(), prologue_.size());
aerr = handle_noise_error_(err, LOG_STR("noise_handshakestate_set_prologue"), APIError::HANDSHAKESTATE_SETUP_FAILED);
if (aerr != APIError::OK)
return aerr;
// set_prologue copies it into handshakestate, so we can get rid of it now
// init copies the prologue into the handshakestate, so we can get rid of it now
prologue_.release();
err = noise_handshakestate_start(handshake_);
aerr = handle_noise_error_(err, LOG_STR("noise_handshakestate_start"), APIError::HANDSHAKESTATE_SETUP_FAILED);
if (aerr != APIError::OK)
return aerr;
return APIError::OK;
}
@@ -637,15 +551,17 @@ APIError APINoiseFrameHelper::check_handshake_finished_() {
assert(state_ == State::HANDSHAKE);
#endif
int action = noise_handshakestate_get_action(handshake_);
if (action == NOISE_ACTION_READ_MESSAGE || action == NOISE_ACTION_WRITE_MESSAGE)
noise::NoiseResponderHandshake::Action action = this->handshake_.action();
if (action == noise::NoiseResponderHandshake::Action::ACTION_READ ||
action == noise::NoiseResponderHandshake::Action::ACTION_WRITE)
return APIError::OK;
if (action != NOISE_ACTION_SPLIT) {
if (action != noise::NoiseResponderHandshake::Action::ACTION_SPLIT) {
state_ = State::FAILED;
HELPER_LOG("Bad action for handshake: %d", action);
HELPER_LOG("Bad action for handshake: %d", (int) action);
return APIError::HANDSHAKESTATE_BAD_STATE;
}
int err = noise_handshakestate_split(handshake_, &send_cipher_, &recv_cipher_);
// split() also frees the handshake state
int err = this->handshake_.split(send_cipher_, recv_cipher_);
APIError aerr =
handle_noise_error_(err, LOG_STR("noise_handshakestate_split"), APIError::HANDSHAKESTATE_SPLIT_FAILED);
if (aerr != APIError::OK)
@@ -654,17 +570,11 @@ APIError APINoiseFrameHelper::check_handshake_finished_() {
this->frame_footer_size_ = noise_cipherstate_get_mac_length(send_cipher_);
HELPER_LOG("Handshake complete!");
noise_handshakestate_free(handshake_);
handshake_ = nullptr;
state_ = State::DATA;
return APIError::OK;
}
APINoiseFrameHelper::~APINoiseFrameHelper() {
if (handshake_ != nullptr) {
noise_handshakestate_free(handshake_);
handshake_ = nullptr;
}
if (send_cipher_ != nullptr) {
noise_cipherstate_free(send_cipher_);
send_cipher_ = nullptr;
@@ -675,16 +585,6 @@ APINoiseFrameHelper::~APINoiseFrameHelper() {
}
}
extern "C" {
// declare how noise generates random bytes (here with a good HWRNG based on the RF system)
void noise_rand_bytes(void *output, size_t len) {
if (!esphome::random_bytes(reinterpret_cast<uint8_t *>(output), len)) {
ESP_LOGE(TAG, "Acquiring random bytes failed; rebooting");
arch_restart();
}
}
}
} // namespace esphome::api
#endif // USE_API_NOISE
#endif // USE_API
@@ -3,7 +3,7 @@
#ifdef USE_API
#ifdef USE_API_NOISE
#include "noise/protocol.h"
#include "api_noise_context.h"
#include "esphome/components/noise/noise_handshake.h"
namespace esphome::api {
@@ -14,9 +14,9 @@ class APINoiseFrameHelper final : public APIFrameHelper {
// Pos 1-2: encrypted payload size (16-bit big-endian)
// Pos 3-6: encrypted type (16-bit) + data_len (16-bit)
// Pos 7+: actual payload data
static constexpr uint8_t HEADER_PADDING = 1 + 2 + 2 + 2; // indicator + size + type + data_len
static constexpr uint8_t HEADER_PADDING = noise::FRAME_HEADER_SIZE + 2 + 2; // frame header + type + data_len
APINoiseFrameHelper(std::unique_ptr<socket::Socket> socket, APINoiseContext &ctx)
APINoiseFrameHelper(std::unique_ptr<socket::Socket> socket, noise::NoiseContext &ctx)
: APIFrameHelper(std::move(socket)), ctx_(ctx) {
frame_header_padding_ = HEADER_PADDING;
}
@@ -52,13 +52,13 @@ class APINoiseFrameHelper final : public APIFrameHelper {
APIError handle_handshake_frame_error_(APIError aerr);
APIError handle_noise_error_(int err, const LogString *func_name, APIError api_err);
// Pointers first (4 bytes each)
NoiseHandshakeState *handshake_{nullptr};
// Pointers first (4 bytes each; the handshake wrapper holds one pointer)
noise::NoiseResponderHandshake handshake_;
NoiseCipherState *send_cipher_{nullptr};
NoiseCipherState *recv_cipher_{nullptr};
// Reference to noise context (4 bytes on 32-bit)
APINoiseContext &ctx_;
noise::NoiseContext &ctx_;
// Buffer for noise handshake prologue (released after handshake)
APIBuffer prologue_;
@@ -67,7 +67,7 @@ class APINoiseFrameHelper final : public APIFrameHelper {
// Fixed-size header buffer for noise protocol:
// 1 byte for indicator + 2 bytes for message size (16-bit value, not varint)
// Note: Maximum message size is UINT16_MAX (65535), with a limit of 128 bytes during handshake phase
uint8_t rx_header_buf_[3];
uint8_t rx_header_buf_[noise::FRAME_HEADER_SIZE];
uint8_t rx_header_buf_len_ = 0;
// 4 bytes total, no padding
};
@@ -1,37 +0,0 @@
#pragma once
#include <array>
#include <cstdint>
#include "esphome/core/defines.h"
namespace esphome::api {
#ifdef USE_API_NOISE
using psk_t = std::array<uint8_t, 32>;
class APINoiseContext {
public:
// The all-zeros PSK is reserved: it marks the device as unprovisioned and
// doubles as the well-known provisioning PSK that unprovisioned devices
// accept for Noise handshakes (passive-sniffing protection only, no
// authentication). It is never a valid real key.
static bool is_all_zeros(const psk_t &psk) {
uint8_t acc = 0;
for (uint8_t b : psk) {
acc |= b;
}
return acc == 0;
}
void set_psk(psk_t psk) {
this->psk_ = psk;
this->has_psk_ = !is_all_zeros(psk);
}
const psk_t &get_psk() const { return this->psk_; }
bool has_psk() const { return this->has_psk_; }
protected:
psk_t psk_{};
bool has_psk_{false};
};
#endif // USE_API_NOISE
} // namespace esphome::api
+1 -1
View File
@@ -588,7 +588,7 @@ bool APIServer::load_and_apply_noise_psk_() {
return true;
}
bool APIServer::save_noise_psk(psk_t psk, bool make_active) {
bool APIServer::save_noise_psk(noise::psk_t psk, bool make_active) {
#ifdef USE_API_NOISE_PSK_FROM_YAML
// When PSK is set from YAML, this function should never be called
// but if it is, reject the change
+9 -6
View File
@@ -5,7 +5,10 @@
#include "api_buffer.h"
// Must precede clients_ so APIConnection is complete for default_delete (libc++).
#include "api_connection.h"
#include "api_noise_context.h"
#ifdef USE_API_NOISE
// Only present in the build when the noise component is loaded
#include "esphome/components/noise/noise.h"
#endif
#include "api_pb2.h"
#include "api_pb2_service.h"
#include "esphome/components/socket/socket.h"
@@ -37,7 +40,7 @@ class UserServiceDescriptor;
#ifdef USE_API_NOISE
struct SavedNoisePsk {
psk_t psk;
noise::psk_t psk;
} PACKED; // NOLINT
#endif
@@ -73,10 +76,10 @@ class APIServer final : public Component,
APIBuffer &get_shared_buffer_ref() { return shared_write_buffer_; }
#ifdef USE_API_NOISE
bool save_noise_psk(psk_t psk, bool make_active = true);
bool save_noise_psk(noise::psk_t psk, bool make_active = true);
bool clear_noise_psk(bool make_active = true);
void set_noise_psk(psk_t psk) { this->noise_ctx_.set_psk(psk); }
APINoiseContext &get_noise_ctx() { return this->noise_ctx_; }
void set_noise_psk(noise::psk_t psk) { this->noise_ctx_.set_psk(psk); }
noise::NoiseContext &get_noise_ctx() { return this->noise_ctx_; }
#endif // USE_API_NOISE
void handle_disconnect(APIConnection *conn);
@@ -354,7 +357,7 @@ class APIServer final : public Component,
#endif
#ifdef USE_API_NOISE
APINoiseContext noise_ctx_;
noise::NoiseContext noise_ctx_;
ESPPreferenceObject noise_pref_;
#endif // USE_API_NOISE
};
@@ -5,6 +5,7 @@ from esphome.automation import Condition, maybe_simple_id
import esphome.codegen as cg
from esphome.components import mqtt, web_server, zigbee
from esphome.components.const import CONF_ON_STATE_CHANGE
from esphome.config_helpers import filter_source_files_from_defines
import esphome.config_validation as cv
from esphome.const import (
CONF_DELAY,
@@ -560,6 +561,11 @@ _CALLBACK_AUTOMATIONS = (
async def _build_binary_sensor_automations(var, config):
await automation.build_callback_automations(var, config, _CALLBACK_AUTOMATIONS)
if config.get(CONF_ON_CLICK) or config.get(CONF_ON_DOUBLE_CLICK):
cg.add_define("USE_BINARY_SENSOR_CLICK_TRIGGER")
if config.get(CONF_ON_MULTI_CLICK):
cg.add_define("USE_BINARY_SENSOR_MULTI_CLICK_TRIGGER")
for conf in config.get(CONF_ON_CLICK, []):
trigger = cg.new_Pvariable(
conf[CONF_TRIGGER_ID], var, conf[CONF_MIN_LENGTH], conf[CONF_MAX_LENGTH]
@@ -673,3 +679,15 @@ async def to_code(config):
async def binary_sensor_invalidate_state_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
return cg.new_Pvariable(action_id, template_arg, paren)
# automation.cpp only implements the click/double_click/multi_click triggers
FILTER_SOURCE_FILES = filter_source_files_from_defines(
{
"automation.cpp": (
"USE_BINARY_SENSOR_CLICK_TRIGGER",
"USE_BINARY_SENSOR_MULTI_CLICK_TRIGGER",
),
"filter.cpp": "USE_BINARY_SENSOR_FILTER",
}
)
@@ -1,8 +1,13 @@
#include "esphome/core/defines.h"
#if defined(USE_BINARY_SENSOR_CLICK_TRIGGER) || defined(USE_BINARY_SENSOR_MULTI_CLICK_TRIGGER)
#include "automation.h"
#include "esphome/core/log.h"
namespace esphome::binary_sensor {
#ifdef USE_BINARY_SENSOR_MULTI_CLICK_TRIGGER
static const char *const TAG = "binary_sensor.automation";
// MultiClickTrigger timeout IDs.
@@ -120,6 +125,9 @@ void MultiClickTriggerBase::trigger_() {
this->trigger();
}
#endif // USE_BINARY_SENSOR_MULTI_CLICK_TRIGGER
#ifdef USE_BINARY_SENSOR_CLICK_TRIGGER
bool match_interval(uint32_t min_length, uint32_t max_length, uint32_t length) {
if (max_length == 0) {
return length >= min_length;
@@ -127,4 +135,8 @@ bool match_interval(uint32_t min_length, uint32_t max_length, uint32_t length) {
return length >= min_length && length <= max_length;
}
}
#endif // USE_BINARY_SENSOR_CLICK_TRIGGER
} // namespace esphome::binary_sensor
#endif // USE_BINARY_SENSOR_CLICK_TRIGGER || USE_BINARY_SENSOR_MULTI_CLICK_TRIGGER
+8
View File
@@ -12,6 +12,7 @@ from typing import Any
from esphome import yaml_util
import esphome.codegen as cg
from esphome.components.const import CONF_ENABLE_OTA_DOWNGRADE_PROTECTION
from esphome.config_helpers import filter_source_files_from_defines
import esphome.config_validation as cv
from esphome.const import (
CONF_ADVANCED,
@@ -3451,3 +3452,10 @@ def process_stacktrace(config, line, backtrace_state):
_decode_pc(config, addr.group())
return backtrace_state
# gpio.cpp only implements ESP32InternalGPIOPin and its ISR helpers, which
# are instantiated solely by the pin schema codegen (esp32_pin_to_code)
FILTER_SOURCE_FILES = filter_source_files_from_defines(
{"gpio.cpp": "USE_ESP32_INTERNAL_GPIO"}
)
+54 -7
View File
@@ -124,6 +124,15 @@ static uint8_t IRAM_ATTR capture_riscv_backtrace(RvExcFrame *frame, uint32_t *ou
// Version is uint32_t because it would be padded to 4 bytes anyway before the next
// uint32_t field, so we use the full width rather than wasting 3 bytes of padding.
static constexpr uint32_t CRASH_DATA_VERSION = 4;
#if CONFIG_IDF_TARGET_ARCH_XTENSA
// EXCCAUSE is a 6-bit register; larger recorded values mean the frame's
// cause/vaddr slots were never written (not a real exception frame).
static constexpr uint32_t XTENSA_EXCCAUSE_COUNT = XCHAL_EXCCAUSE_NUM;
#elif CONFIG_IDF_TARGET_ARCH_RISCV
// Synchronous mcause exception codes are small and have no interrupt bit;
// anything else in a non-pseudo record is a stale slot.
static constexpr uint32_t RISCV_EXCEPTION_CAUSE_COUNT = 32;
#endif
struct RawCrashData {
uint32_t version;
uint32_t magic;
@@ -198,10 +207,28 @@ void crash_handler_clear() {
s_raw_crash_data.magic = 0;
}
// Whether the cause slot was written by a real exception frame.
static bool cause_slot_was_written() {
#if CONFIG_IDF_TARGET_ARCH_XTENSA
return s_raw_crash_data.cause < XTENSA_EXCCAUSE_COUNT;
#else
return s_raw_crash_data.cause < RISCV_EXCEPTION_CAUSE_COUNT;
#endif
}
// Look up the exception cause as a human-readable string.
// Tables mirror ESP-IDF's panic_arch_fill_info() which uses local static arrays
// not exposed via any public API.
static const char *get_exception_reason() {
uint8_t exception = s_raw_crash_data.exception;
if (exception == PANIC_EXCEPTION_ABORT || exception == PANIC_EXCEPTION_TWDT) {
// Abort-class panics carry no cause register
return nullptr;
}
if (!cause_slot_was_written()) {
// Garbage from old-build or corrupt records; report just the type
return nullptr;
}
#if CONFIG_IDF_TARGET_ARCH_XTENSA
if (s_raw_crash_data.pseudo_excause) {
// SoC-level panic: watchdog, cache error, etc.
@@ -354,10 +381,11 @@ static const char *const FAULT_ADDR_REG = "MTVAL";
static const char *const FAULT_ADDR_REG_LOWER = "mtval";
#endif
// Whether the fault address is meaningful real CPU faults only, not
// aborts/watchdogs or SoC-level pseudo exceptions.
// Whether the fault address is meaningful: real CPU faults with a validly
// written frame only.
static bool has_fault_addr() {
return s_raw_crash_data.exception == PANIC_EXCEPTION_FAULT && !s_raw_crash_data.pseudo_excause;
return s_raw_crash_data.exception == PANIC_EXCEPTION_FAULT && !s_raw_crash_data.pseudo_excause &&
cause_slot_was_written();
}
// The record was captured by a different firmware build (it survives soft
@@ -458,6 +486,10 @@ void crash_handler_log() {
// into NOINIT memory before the normal panic handler runs.
//
extern "C" {
// Set by IDF's task watchdog (task_wdt.c, no header) before it simulates an
// abort; weak so builds without the task watchdog still link.
extern bool g_twdt_isr __attribute__((weak));
// NOLINTBEGIN(bugprone-reserved-identifier,cert-dcl37-c,cert-dcl51-cpp,readability-identifier-naming)
// Names are mandated by the --wrap linker mechanism
extern void __real_esp_panic_handler(panic_info_t *info);
@@ -470,6 +502,14 @@ void IRAM_ATTR __wrap_esp_panic_handler(panic_info_t *info) {
s_raw_crash_data.exception = (uint8_t) info->exception;
s_raw_crash_data.pseudo_excause = info->pseudo_excause ? 1 : 0;
s_raw_crash_data.crashed_core = (uint8_t) info->core;
if (g_panic_abort) {
// IDF reclassifies to ABORT only inside esp_panic_handler(), after this
// wrapper captured info->exception; correct it here. TWDT is our own
// distinction (IDF never assigns PANIC_EXCEPTION_TWDT). The abort text is
// not stored; the symbolized backtrace already identifies the site.
bool is_twdt = &g_twdt_isr != nullptr && g_twdt_isr;
s_raw_crash_data.exception = (uint8_t) (is_twdt ? PANIC_EXCEPTION_TWDT : PANIC_EXCEPTION_ABORT);
}
// Zero unconditionally so a null frame doesn't leave stale .noinit data from a previous boot
s_raw_crash_data.cause = 0;
s_raw_crash_data.fault_addr = 0;
@@ -487,8 +527,12 @@ void IRAM_ATTR __wrap_esp_panic_handler(panic_info_t *info) {
// Xtensa: walk the backtrace using the public API
if (info->frame != nullptr) {
auto *xt_frame = (XtExcFrame *) info->frame;
s_raw_crash_data.cause = xt_frame->exccause;
s_raw_crash_data.fault_addr = xt_frame->excvaddr;
if (!g_panic_abort) {
// Abort-class frames carry no useful cause/vaddr: TWDT task snapshots
// never wrote them and abort() traps describe only the synthetic trap.
s_raw_crash_data.cause = xt_frame->exccause;
s_raw_crash_data.fault_addr = xt_frame->excvaddr;
}
s_raw_crash_data.backtrace_count = walk_xtensa_backtrace(xt_frame, s_raw_crash_data.backtrace, MAX_BACKTRACE);
}
@@ -510,8 +554,11 @@ void IRAM_ATTR __wrap_esp_panic_handler(panic_info_t *info) {
// RISC-V: capture MEPC + RA, then scan stack for code addresses
if (info->frame != nullptr) {
auto *rv_frame = (RvExcFrame *) info->frame;
s_raw_crash_data.cause = rv_frame->mcause;
s_raw_crash_data.fault_addr = rv_frame->mtval;
if (!g_panic_abort) {
// See the Xtensa branch: abort-class frames carry no valid cause/vaddr.
s_raw_crash_data.cause = rv_frame->mcause;
s_raw_crash_data.fault_addr = rv_frame->mtval;
}
s_raw_crash_data.backtrace_count =
capture_riscv_backtrace(rv_frame, s_raw_crash_data.backtrace, MAX_BACKTRACE, &s_raw_crash_data.reg_frame_count);
}
+5 -2
View File
@@ -1,4 +1,7 @@
#ifdef USE_ESP32
#include "esphome/core/defines.h"
// Also defines the core ISRInternalGPIOPin methods; those are only reachable
// via ESP32InternalGPIOPin::to_isr(), so the same define gates both safely.
#if defined(USE_ESP32) && defined(USE_ESP32_INTERNAL_GPIO)
#include "gpio.h"
#include "esphome/core/log.h"
@@ -204,4 +207,4 @@ void IRAM_ATTR ISRInternalGPIOPin::pin_mode(gpio::Flags flags) {
} // namespace esphome
#endif // USE_ESP32
#endif // USE_ESP32 && USE_ESP32_INTERNAL_GPIO
+1
View File
@@ -257,6 +257,7 @@ ESP32_PIN_SCHEMA = cv.All(
@pins.PIN_SCHEMA_REGISTRY.register(PLATFORM_ESP32, ESP32_PIN_SCHEMA)
async def esp32_pin_to_code(config):
cg.add_define("USE_ESP32_INTERNAL_GPIO")
var = cg.new_Pvariable(config[CONF_ID])
num = config[CONF_NUMBER]
cg.add(var.set_pin(getattr(gpio_num_t, f"GPIO_NUM_{num}")))
@@ -398,7 +398,7 @@ void ESPHomeOTAComponent::handle_data_() {
this->notify_state_(ota::OTA_STARTED, 0.0f, 0);
#endif
// begin() may block for a few seconds while it locks flash.
// begin() returns quickly; flash sectors are erased incrementally during write().
error_code = this->backend_->begin(ota_size, ota_type);
if (error_code != ota::OTA_RESPONSE_OK)
goto error; // NOLINT(cppcoreguidelines-avoid-goto)
@@ -159,9 +159,6 @@ class EthernetComponent final : public Component {
const char *get_use_address() const { return this->use_address_; }
void set_use_address(const char *use_address) { this->use_address_ = use_address; }
void get_eth_mac_address_raw(uint8_t *mac);
// Remove before 2026.9.0
ESPDEPRECATED("Use get_eth_mac_address_pretty_into_buffer() instead. Removed in 2026.9.0", "2026.3.0")
std::string get_eth_mac_address_pretty();
const char *get_eth_mac_address_pretty_into_buffer(std::span<char, MAC_ADDRESS_PRETTY_BUFFER_SIZE> buf);
eth_duplex_t get_duplex_mode();
eth_speed_t get_link_speed();
@@ -928,11 +928,6 @@ void EthernetComponent::get_eth_mac_address_raw(uint8_t *mac) {
ESPHL_ERROR_CHECK(err, "ETH_CMD_G_MAC error");
}
std::string EthernetComponent::get_eth_mac_address_pretty() {
char buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
return std::string(this->get_eth_mac_address_pretty_into_buffer(buf));
}
const char *EthernetComponent::get_eth_mac_address_pretty_into_buffer(
std::span<char, MAC_ADDRESS_PRETTY_BUFFER_SIZE> buf) {
uint8_t mac[MAC_ADDRESS_SIZE];
@@ -249,11 +249,6 @@ void EthernetComponent::get_eth_mac_address_raw(uint8_t *mac) {
}
}
std::string EthernetComponent::get_eth_mac_address_pretty() {
char buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
return std::string(this->get_eth_mac_address_pretty_into_buffer(buf));
}
const char *EthernetComponent::get_eth_mac_address_pretty_into_buffer(
std::span<char, MAC_ADDRESS_PRETTY_BUFFER_SIZE> buf) {
uint8_t mac[MAC_ADDRESS_SIZE];
@@ -64,8 +64,9 @@ void OtaHttpRequestComponent::flash() {
}
}
void OtaHttpRequestComponent::cleanup_(ota::OTABackendPtr backend, const std::shared_ptr<HttpContainer> &container) {
if (this->update_started_) {
void OtaHttpRequestComponent::cleanup_(ota::OTABackendPtr backend, const std::shared_ptr<HttpContainer> &container,
bool abort_backend) {
if (abort_backend) {
ESP_LOGV(TAG, "Aborting OTA backend");
backend->abort();
}
@@ -106,7 +107,8 @@ uint8_t OtaHttpRequestComponent::do_ota_() {
auto error_code = backend->begin(container->content_length);
if (error_code != ota::OTA_RESPONSE_OK) {
ESP_LOGW(TAG, "backend->begin error: %d", error_code);
this->cleanup_(std::move(backend), container);
// Nothing to abort: begin() failed, so no OTA handle was opened
this->cleanup_(std::move(backend), container, /*abort_backend=*/false);
return error_code;
}
@@ -140,7 +142,7 @@ uint8_t OtaHttpRequestComponent::do_ota_() {
} else {
ESP_LOGE(TAG, "Error reading data: %d", bufsize_or_error);
}
this->cleanup_(std::move(backend), container);
this->cleanup_(std::move(backend), container, /*abort_backend=*/true);
return OTA_CONNECTION_ERROR;
}
@@ -150,14 +152,13 @@ uint8_t OtaHttpRequestComponent::do_ota_() {
md5_receive.add(buf, bufsize_or_error);
// write bytes to OTA backend
this->update_started_ = true;
error_code = backend->write(buf, bufsize_or_error);
if (error_code != ota::OTA_RESPONSE_OK) {
// error code explanation available at
// https://github.com/esphome/esphome/blob/dev/esphome/components/ota/ota_backend.h
ESP_LOGE(TAG, "Error code (%02X) writing binary data to flash at offset %d and size %d", error_code,
container->get_bytes_read() - bufsize_or_error, container->content_length);
this->cleanup_(std::move(backend), container);
this->cleanup_(std::move(backend), container, /*abort_backend=*/true);
return error_code;
}
}
@@ -181,7 +182,7 @@ uint8_t OtaHttpRequestComponent::do_ota_() {
this->md5_computed_ = md5_receive_str;
if (strncmp(this->md5_computed_.c_str(), this->md5_expected_.c_str(), MD5_SIZE) != 0) {
ESP_LOGE(TAG, "MD5 computed: %s - Aborting due to MD5 mismatch", this->md5_computed_.c_str());
this->cleanup_(std::move(backend), container);
this->cleanup_(std::move(backend), container, /*abort_backend=*/true);
return ota::OTA_RESPONSE_ERROR_MD5_MISMATCH;
} else {
backend->set_update_md5(md5_receive_str);
@@ -197,7 +198,7 @@ uint8_t OtaHttpRequestComponent::do_ota_() {
error_code = backend->end();
if (error_code != ota::OTA_RESPONSE_OK) {
ESP_LOGW(TAG, "Error ending update! error_code: %d", error_code);
this->cleanup_(std::move(backend), container);
this->cleanup_(std::move(backend), container, /*abort_backend=*/true);
return error_code;
}
@@ -38,7 +38,7 @@ class OtaHttpRequestComponent final : public ota::OTAComponent, public Parented<
void flash();
protected:
void cleanup_(ota::OTABackendPtr backend, const std::shared_ptr<HttpContainer> &container);
void cleanup_(ota::OTABackendPtr backend, const std::shared_ptr<HttpContainer> &container, bool abort_backend);
uint8_t do_ota_();
std::string get_url_with_auth_(const std::string &url);
bool http_get_md5_();
@@ -51,7 +51,6 @@ class OtaHttpRequestComponent final : public ota::OTAComponent, public Parented<
std::string username_{};
std::string url_{};
int status_ = -1;
bool update_started_ = false;
static const uint16_t HTTP_RECV_BUFFER = 256; // the firmware GET chunk size
};
-3
View File
@@ -618,9 +618,6 @@ class ModbusClientDevice {
inline void clear_tx_queue_for_address() { this->parent_->clear_tx_queue_for_address(this->address_); }
inline void clear_tx_queue_for_device() { this->parent_->clear_tx_queue_for_device(this); }
// If more than one device is connected block sending a new command before a response is received
ESPDEPRECATED("Use ready_for_immediate_send() instead. Removed in 2026.9.0", "2026.3.0")
bool waiting_for_response() { return !this->ready_for_immediate_send(); }
bool ready_for_immediate_send() { return this->parent_->tx_buffer_empty() && !this->parent_->tx_blocked(); }
protected:
+69
View File
@@ -0,0 +1,69 @@
import base64
import binascii
from typing import Any
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.const import CONF_KEY
from esphome.types import ConfigType
CODEOWNERS = ["@esphome/core"]
noise_ns = cg.esphome_ns.namespace("noise")
CONFIG_SCHEMA = cv.Schema({})
def validate_encryption_key(value: Any) -> str:
value = cv.string_strict(value)
try:
decoded = base64.b64decode(value, validate=True)
except ValueError as err:
raise cv.Invalid("Invalid key format, please check it's using base64") from err
if len(decoded) != 32:
raise cv.Invalid("Encryption key must be base64 and 32 bytes long")
# Return original data for roundtrip conversion
return value
def decode_encryption_key(value: str) -> bytes:
"""Decode a base64 encryption key to its 32 raw bytes.
a2b_base64 matches the decode the clients use (aioesphomeapi
decode_noise_psk), so both ends derive the same bytes. The length is
re-checked so a caller cannot turn an unvalidated short decode into a
zero-padded PSK.
"""
try:
decoded = binascii.a2b_base64(value)
except ValueError as err:
raise cv.Invalid("Invalid key format, please check it's using base64") from err
if len(decoded) != 32:
raise cv.Invalid("Encryption key must be base64 and 32 bytes long")
return decoded
ENCRYPTION_SCHEMA = cv.Schema(
{
cv.Optional(CONF_KEY): cv.sensitive(validate_encryption_key),
}
)
def encryption_schema(config: ConfigType | None) -> ConfigType:
# A bare `encryption:` block is valid; a missing key means the consumer
# falls back to its keyless behavior (api provisioning, ota inheriting
# the api key).
if config is None:
config = {}
return ENCRYPTION_SCHEMA(config)
async def to_code(config: ConfigType) -> None:
cg.add_define("USE_NOISE")
cg.add_library("esphome/noise-c", "0.1.21")
# 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")
+88
View File
@@ -0,0 +1,88 @@
#include "noise.h"
#ifdef USE_NOISE
#include "esphome/core/log.h"
#include <algorithm>
#include <cstring>
#include <noise/protocol.h>
#ifdef USE_ESP8266
#include <pgmspace.h>
#endif
namespace esphome::noise {
static const char *const TAG = "noise";
const LogString *noise_err_to_logstr(int err) {
if (err == NOISE_ERROR_NO_MEMORY)
return LOG_STR("NO_MEMORY");
if (err == NOISE_ERROR_UNKNOWN_ID)
return LOG_STR("UNKNOWN_ID");
if (err == NOISE_ERROR_UNKNOWN_NAME)
return LOG_STR("UNKNOWN_NAME");
if (err == NOISE_ERROR_MAC_FAILURE)
return LOG_STR("MAC_FAILURE");
if (err == NOISE_ERROR_NOT_APPLICABLE)
return LOG_STR("NOT_APPLICABLE");
if (err == NOISE_ERROR_SYSTEM)
return LOG_STR("SYSTEM");
if (err == NOISE_ERROR_REMOTE_KEY_REQUIRED)
return LOG_STR("REMOTE_KEY_REQUIRED");
if (err == NOISE_ERROR_LOCAL_KEY_REQUIRED)
return LOG_STR("LOCAL_KEY_REQUIRED");
if (err == NOISE_ERROR_PSK_REQUIRED)
return LOG_STR("PSK_REQUIRED");
if (err == NOISE_ERROR_INVALID_LENGTH)
return LOG_STR("INVALID_LENGTH");
if (err == NOISE_ERROR_INVALID_PARAM)
return LOG_STR("INVALID_PARAM");
if (err == NOISE_ERROR_INVALID_STATE)
return LOG_STR("INVALID_STATE");
if (err == NOISE_ERROR_INVALID_NONCE)
return LOG_STR("INVALID_NONCE");
if (err == NOISE_ERROR_INVALID_PRIVATE_KEY)
return LOG_STR("INVALID_PRIVATE_KEY");
if (err == NOISE_ERROR_INVALID_PUBLIC_KEY)
return LOG_STR("INVALID_PUBLIC_KEY");
if (err == NOISE_ERROR_INVALID_FORMAT)
return LOG_STR("INVALID_FORMAT");
if (err == NOISE_ERROR_INVALID_SIGNATURE)
return LOG_STR("INVALID_SIGNATURE");
return LOG_STR("UNKNOWN");
}
const LogString *reject_reason_for(int err) {
return err == NOISE_ERROR_MAC_FAILURE ? LOG_STR("Handshake MAC failure") : LOG_STR("Handshake error");
}
size_t format_reject_payload(uint8_t *buf, size_t capacity, const LogString *reason) {
if (capacity == 0) {
// A caller bug; the MAC_FAILURE_PAYLOAD_SIZE static_asserts at the call
// sites make this unreachable, kept as cheap memory safety
ESP_LOGVV(TAG, "Reject buffer has no capacity");
return 0;
}
buf[0] = HANDSHAKE_STATUS_REJECT;
#ifdef USE_STORE_LOG_STR_IN_FLASH
// On ESP8266 with flash strings, we need to use PROGMEM-aware functions
size_t reason_len = strlen_P(reinterpret_cast<PGM_P>(reason));
reason_len = std::min(reason_len, capacity - 1);
if (reason_len > 0) {
memcpy_P(buf + 1, reinterpret_cast<PGM_P>(reason), reason_len);
}
#else
const char *reason_str = LOG_STR_ARG(reason);
size_t reason_len = strlen(reason_str);
reason_len = std::min(reason_len, capacity - 1);
if (reason_len > 0) {
// NOLINTNEXTLINE(bugprone-not-null-terminated-result) - binary protocol, not a C string
std::memcpy(buf + 1, reason_str, reason_len);
}
#endif
return reason_len + 1;
}
} // namespace esphome::noise
#endif // USE_NOISE
+73
View File
@@ -0,0 +1,73 @@
#pragma once
#include "esphome/core/defines.h"
#ifdef USE_NOISE
#include <array>
#include <cstdint>
#include "esphome/core/log.h"
namespace esphome::noise {
using psk_t = std::array<uint8_t, 32>;
class NoiseContext {
public:
// The all-zeros PSK is reserved: it marks the device as unprovisioned and
// doubles as the well-known provisioning PSK that unprovisioned devices
// accept for Noise handshakes (passive-sniffing protection only, no
// authentication). It is never a valid real key.
static bool is_all_zeros(const psk_t &psk) {
uint8_t acc = 0;
for (uint8_t b : psk) {
acc |= b;
}
return acc == 0;
}
void set_psk(psk_t psk) {
this->psk_ = psk;
this->has_psk_ = !is_all_zeros(psk);
}
const psk_t &get_psk() const { return this->psk_; }
bool has_psk() const { return this->has_psk_; }
protected:
psk_t psk_{};
bool has_psk_{false};
};
/// Convert a noise error code to a readable error
const LogString *noise_err_to_logstr(int err);
// Shared wire format for the noise transports (api and ota): every frame is
// FRAME_INDICATOR, a 16-bit big-endian payload length, then the payload.
// Handshake payloads start with a status byte; transport payloads end with
// the ChaCha20-Poly1305 MAC.
static constexpr uint8_t FRAME_INDICATOR = 0x01;
static constexpr size_t FRAME_HEADER_SIZE = 3;
static constexpr size_t MAC_SIZE = 16;
static constexpr size_t MAX_HANDSHAKE_SIZE = 128;
static constexpr uint8_t HANDSHAKE_STATUS_OK = 0x00;
static constexpr uint8_t HANDSHAKE_STATUS_REJECT = 0x01;
inline void write_frame_header(uint8_t *buf, uint16_t payload_len) {
buf[0] = FRAME_INDICATOR;
buf[1] = (uint8_t) (payload_len >> 8);
buf[2] = (uint8_t) payload_len;
}
/// Fill buf with a handshake reject payload (status byte plus the reason
/// text, PROGMEM aware); returns the payload length. buf needs capacity for
/// the status byte plus the truncated reason.
size_t format_reject_payload(uint8_t *buf, size_t capacity, const LogString *reason);
/// Reject reason for a failed handshake read. The MAC failure string is a
/// wire contract: clients match it to report a wrong key.
const LogString *reject_reason_for(int err);
/// Payload size of the MAC failure reject, the one reason string that is a
/// wire contract (sizeof's NUL stands in for the status byte). static_assert
/// reject buffers against this so a wrong key report can never truncate;
/// longer caller-supplied reasons are informational and sized by the caller.
static constexpr size_t MAC_FAILURE_PAYLOAD_SIZE = sizeof("Handshake MAC failure");
} // namespace esphome::noise
#endif // USE_NOISE
@@ -0,0 +1,139 @@
#include "noise_handshake.h"
#ifdef USE_NOISE
#include "esphome/core/hal.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
namespace esphome::noise {
static const char *const TAG = "noise";
// Log the failing noise-c call at the same verbosity the api helper used
// before this class existed; callers only see one collapsed error code.
#define HANDSHAKE_STEP_LOG(func_name, err_code) \
ESP_LOGVV(TAG, "%s failed: %s", LOG_STR_ARG(LOG_STR(func_name)), LOG_STR_ARG(noise_err_to_logstr(err_code)))
NoiseResponderHandshake::~NoiseResponderHandshake() {
if (this->handshake_ != nullptr) {
noise_handshakestate_free(this->handshake_);
this->handshake_ = nullptr;
}
}
int NoiseResponderHandshake::init(const psk_t &psk, const uint8_t *prologue, size_t prologue_len) {
if (this->handshake_ != nullptr) {
noise_handshakestate_free(this->handshake_);
this->handshake_ = nullptr;
}
// Noise_NNpsk0_25519_ChaChaPoly_SHA256, built on the stack:
// noise_handshakestate_new_by_id copies it, so a member would waste
// 104 bytes per connection, and a static const would sit in RAM on
// ESP8266 (.rodata is DRAM there).
const NoiseProtocolId nid = {
.prefix_id = NOISE_PREFIX_STANDARD,
.pattern_id = NOISE_PATTERN_NN,
.modifier_ids = {NOISE_MODIFIER_PSK0},
.dh_id = NOISE_DH_CURVE25519,
.cipher_id = NOISE_CIPHER_CHACHAPOLY,
.hash_id = NOISE_HASH_SHA256,
.hybrid_id = NOISE_DH_NONE,
};
int err = noise_handshakestate_new_by_id(&this->handshake_, &nid, NOISE_ROLE_RESPONDER);
if (err != 0) {
HANDSHAKE_STEP_LOG("noise_handshakestate_new_by_id", err);
return err;
}
err = noise_handshakestate_set_pre_shared_key(this->handshake_, psk.data(), psk.size());
if (err != 0) {
HANDSHAKE_STEP_LOG("noise_handshakestate_set_pre_shared_key", err);
return this->fail_init_(err);
}
err = noise_handshakestate_set_prologue(this->handshake_, prologue, prologue_len);
if (err != 0) {
HANDSHAKE_STEP_LOG("noise_handshakestate_set_prologue", err);
return this->fail_init_(err);
}
err = noise_handshakestate_start(this->handshake_);
if (err != 0) {
HANDSHAKE_STEP_LOG("noise_handshakestate_start", err);
return this->fail_init_(err);
}
return 0;
}
/// Release a half-initialized state so a failed init() leaves the object as
/// if init() was never called.
int NoiseResponderHandshake::fail_init_(int err) {
noise_handshakestate_free(this->handshake_);
this->handshake_ = nullptr;
return err;
}
NoiseResponderHandshake::Action NoiseResponderHandshake::action() const {
if (this->handshake_ == nullptr) {
// A caller bug: init() was never called, or split() already released the state
ESP_LOGVV(TAG, "action() on uninitialized or split handshake");
return Action::ACTION_FAILED;
}
int raw = noise_handshakestate_get_action(this->handshake_);
switch (raw) {
case NOISE_ACTION_READ_MESSAGE:
return Action::ACTION_READ;
case NOISE_ACTION_WRITE_MESSAGE:
return Action::ACTION_WRITE;
case NOISE_ACTION_SPLIT:
return Action::ACTION_SPLIT;
default:
// Preserve the raw code in debug logs; callers only see the collapsed enum
ESP_LOGVV(TAG, "Unexpected noise action %d", raw);
return Action::ACTION_FAILED;
}
}
int NoiseResponderHandshake::read_message(uint8_t *data, size_t len) {
NoiseBuffer mbuf;
noise_buffer_init(mbuf);
noise_buffer_set_input(mbuf, data, len);
return noise_handshakestate_read_message(this->handshake_, &mbuf, nullptr);
}
int NoiseResponderHandshake::write_message(uint8_t *out, size_t capacity, size_t &out_len) {
out_len = 0;
NoiseBuffer mbuf;
noise_buffer_init(mbuf);
noise_buffer_set_output(mbuf, out, capacity);
int err = noise_handshakestate_write_message(this->handshake_, &mbuf, nullptr);
if (err == 0)
out_len = mbuf.size;
return err;
}
int NoiseResponderHandshake::split(NoiseCipherState *&send_cipher, NoiseCipherState *&recv_cipher) {
// Defined error postcondition: noise-c leaves the out-params unwritten on
// its early error returns, so a caller passing uninitialized locals must
// never see garbage to free
send_cipher = nullptr;
recv_cipher = nullptr;
int err = noise_handshakestate_split(this->handshake_, &send_cipher, &recv_cipher);
if (err != 0)
return err;
noise_handshakestate_free(this->handshake_);
this->handshake_ = nullptr;
return 0;
}
extern "C" {
// noise-c's only randomness source (the vendored library compiles no rand of
// its own); HWRNG backed. Lives in this TU so every handshake consumer links
// it and the definition can never be dropped from the archive.
void noise_rand_bytes(void *output, size_t len) {
if (!esphome::random_bytes(reinterpret_cast<uint8_t *>(output), len)) {
ESP_LOGE(TAG, "Acquiring random bytes failed; rebooting");
arch_restart();
}
}
}
} // namespace esphome::noise
#endif // USE_NOISE
@@ -0,0 +1,63 @@
#pragma once
#include "esphome/core/defines.h"
#ifdef USE_NOISE
#include <cstddef>
#include <cstdint>
#include <noise/protocol.h>
#include "noise.h"
namespace esphome::noise {
/** Sans-IO responder side of a Noise_NNpsk0_25519_ChaChaPoly_SHA256 handshake.
*
* Owns only the noise-c handshake state; the caller moves the raw handshake
* messages (no framing) over its own transport, driven by action():
* read_message() while READ, write_message() while WRITE, then split() to
* take ownership of the transport ciphers. All methods return a noise-c
* error code, 0 on success. Called outside their action() step (before
* init(), after split()) the message methods return a noise-c error rather
* than crashing; the library checks its state argument.
*
* Methods are deliberately small separate functions so callers on tight
* stacks (RP2040 core0 scratch bank) never pay for more than one branch;
* the curve25519 step alone needs ~2KB of stack.
*/
class NoiseResponderHandshake {
public:
// The ACTION_ prefix is macro-collision safety: SDK headers #define bare
// names like READ/WRITE, and macros expand even inside an enum class.
enum class Action : uint8_t { ACTION_READ, ACTION_WRITE, ACTION_SPLIT, ACTION_FAILED };
NoiseResponderHandshake() = default;
~NoiseResponderHandshake();
// Owns a raw noise-c handshake state; copying would double free it
NoiseResponderHandshake(const NoiseResponderHandshake &) = delete;
NoiseResponderHandshake &operator=(const NoiseResponderHandshake &) = delete;
/// Create and start the handshake with the given PSK and prologue. A
/// repeated call frees the previous handshake state and starts over.
[[nodiscard]] int init(const psk_t &psk, const uint8_t *prologue, size_t prologue_len);
/// ACTION_FAILED is the catch-all: returned before init(), after split()
/// has released the state, and when noise-c reports a failed handshake.
[[nodiscard]] Action action() const;
/// Process one received handshake message. The buffer is consumed in
/// place: noise-c decrypts into it and zeroes it before returning.
[[nodiscard]] int read_message(uint8_t *data, size_t len);
/// Produce the next handshake message into out; out_len receives its size
/// and is zero on error.
[[nodiscard]] int write_message(uint8_t *out, size_t capacity, size_t &out_len);
/// Hand out the transport ciphers and free the handshake state. The caller
/// owns both cipher states and must free them with noise_cipherstate_free();
/// both are set to nullptr on error.
[[nodiscard]] int split(NoiseCipherState *&send_cipher, NoiseCipherState *&recv_cipher);
protected:
int fail_init_(int err);
NoiseHandshakeState *handshake_{nullptr};
};
} // namespace esphome::noise
#endif // USE_NOISE
+17 -21
View File
@@ -1,6 +1,9 @@
from esphome import automation
import esphome.codegen as cg
from esphome.config_helpers import filter_source_files_from_platform
from esphome.config_helpers import (
filter_source_files_from_defines,
filter_source_files_from_platform,
)
import esphome.config_validation as cv
from esphome.const import (
CONF_ESPHOME,
@@ -171,24 +174,17 @@ _filter_backend_source_files = filter_source_files_from_platform(
)
# USE_OTA_SIGNED_VERIFICATION_MULTI_KEY is set only on ESP32/IDF;
# USE_OTA_PARTITIONS is set by the esphome OTA platform when
# allow_partition_access is enabled.
_filter_define_source_files = filter_source_files_from_defines(
{
"ota_signature_esp_idf.cpp": "USE_OTA_SIGNED_VERIFICATION_MULTI_KEY",
"ota_bootloader_esp_idf.cpp": "USE_OTA_PARTITIONS",
"ota_partitions_esp_idf.cpp": "USE_OTA_PARTITIONS",
}
)
def FILTER_SOURCE_FILES() -> list[str]:
files = _filter_backend_source_files()
# ota_signature_esp_idf.cpp implements multi-key OTA signature verification,
# compiled only when the esp32 component enables it (external RSA signed
# OTA sets USE_OTA_SIGNED_VERIFICATION_MULTI_KEY). The define is set only on
# ESP32/IDF, so this also excludes the file on every other platform. Filter
# it out otherwise so the (otherwise fully #ifdef'd-out) file isn't opened
# and parsed on every build.
if not any(
define.name == "USE_OTA_SIGNED_VERIFICATION_MULTI_KEY"
for define in CORE.defines
):
files.append("ota_signature_esp_idf.cpp")
# ota_bootloader_esp_idf.cpp and ota_partitions_esp_idf.cpp are fully
# #ifdef'd on USE_OTA_PARTITIONS (set by the esphome OTA platform when
# allow_partition_access is enabled). Filter them out otherwise for the
# same reason as above.
if not any(define.name == "USE_OTA_PARTITIONS" for define in CORE.defines):
files.append("ota_bootloader_esp_idf.cpp")
files.append("ota_partitions_esp_idf.cpp")
return files
return _filter_backend_source_files() + _filter_define_source_files()
+13
View File
@@ -66,6 +66,19 @@ enum OTAResponseTypes {
*/
bool version_is_older(const char *candidate, const char *reference);
// 64 KiB flash block; the erase granularity the ESP-IDF backend erases ahead with.
static constexpr size_t OTA_BLOCK_ERASE_SIZE = 64 * 1024;
/** Target erased watermark for lazy block erase-ahead.
*
* Rounds the write end offset up to a block boundary, clamped to the partition
* size. Platform-independent so the arithmetic is host-testable.
*/
constexpr size_t next_erase_end(size_t write_end, size_t partition_size) {
const size_t rounded = (write_end + OTA_BLOCK_ERASE_SIZE - 1) & ~(OTA_BLOCK_ERASE_SIZE - 1);
return rounded < partition_size ? rounded : partition_size;
}
enum OTAState {
OTA_COMPLETED = 0,
OTA_STARTED,
+69 -17
View File
@@ -7,7 +7,7 @@
#include "esphome/core/log.h"
#include <esp_ota_ops.h>
#include <esp_task_wdt.h>
#include <sdkconfig.h>
#include <spi_flash_mmap.h>
#ifdef USE_OTA_DOWNGRADE_PROTECTION
#include <esp_app_desc.h>
@@ -60,27 +60,38 @@ OTAResponseTypes IDFOTABackend::begin(size_t image_size, ota::OTAType ota_type)
return OTA_RESPONSE_ERROR_NO_UPDATE_PARTITION;
}
// esp_ota_begin() erases the destination region, which blocks loopTask and
// scales with the erase size -- a fixed watchdog overruns on large OTA slots.
// An unknown size (0, e.g. web_server uploads) erases the whole partition, so
// budget against the bytes actually erased. ~10ms/KiB (conservative
// ~100 KiB/s erase) over a 15s floor; panic stays on so a stuck erase still
// resets rather than hanging forever.
size_t erase_size = image_size;
if (erase_size == 0 || erase_size > this->partition_->size) {
erase_size = this->partition_->size;
// Both lazy-erase paths below replace esp_ota_begin()'s blocking full erase.
// Size check replaces the one that erase performed (0 = unknown size,
// e.g. web_server uploads).
if (image_size != 0 && image_size > this->partition_->size) {
return OTA_RESPONSE_ERROR_ESP32_NOT_ENOUGH_SPACE;
}
const uint32_t erase_budget_ms = 15000 + (erase_size >> 10) * 10;
watchdog::WatchdogManager watchdog(erase_budget_ms);
esp_err_t err = esp_ota_begin(this->partition_, image_size, &this->update_handle_);
this->written_ = 0;
esp_err_t err;
#ifdef USE_OTA_BLOCK_ERASE_AHEAD
this->erased_end_ = 0;
// Unlike esp_ota_begin(), esp_ota_resume() does not reject a running app in
// ESP_OTA_IMG_PENDING_VERIFY; that state is unreachable here because the app
// was marked valid at boot (esp32/hal.cpp) or just above under USE_OTA_ROLLBACK.
// erase_size 0 (!= OTA_WITH_SEQUENTIAL_WRITES) means no erase; erase_ahead_() handles it
err = esp_ota_resume(this->partition_, 0, 0, &this->update_handle_);
#if defined(CONFIG_BOOTLOADER_APP_ROLLBACK_ENABLE) && ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 5, 0)
// esp_ota_begin() does this on IDF 5.5+; esp_ota_resume() does not. Prevents
// booting a half-written slot after a crash mid-OTA. Not available on the
// 5.3.3/5.4.2 backports, whose esp_ota_begin() did not invalidate either.
if (err == ESP_OK) {
esp_ota_invalidate_inactive_ota_data_slot();
}
#endif
#else
err = esp_ota_begin(this->partition_, OTA_WITH_SEQUENTIAL_WRITES, &this->update_handle_);
#endif
if (err != ESP_OK) {
ESP_LOGE(TAG, "esp_ota_begin failed (err=0x%X)", err);
ESP_LOGE(TAG, "OTA begin failed (err=0x%X)", err);
esp_ota_abort(this->update_handle_);
this->update_handle_ = 0;
if (err == ESP_ERR_INVALID_SIZE) {
return OTA_RESPONSE_ERROR_ESP32_NOT_ENOUGH_SPACE;
} else if (err == ESP_ERR_FLASH_OP_TIMEOUT || err == ESP_ERR_FLASH_OP_FAIL) {
if (err == ESP_ERR_FLASH_OP_TIMEOUT || err == ESP_ERR_FLASH_OP_FAIL) {
return OTA_RESPONSE_ERROR_WRITING_FLASH;
} else if (err == ESP_ERR_OTA_PARTITION_CONFLICT) {
// This error appears with 1 factory and 1 ota partition
@@ -120,6 +131,17 @@ OTAResponseTypes IDFOTABackend::write(uint8_t *data, size_t len) {
if (!this->is_app_or_bootloader_update_()) {
return OTA_RESPONSE_ERROR_UNSUPPORTED_OTA_TYPE;
}
#endif
// Overflow can only happen on unknown-size uploads (web_server); known
// sizes were rejected in begin().
if (this->written_ + len > this->partition_->size) {
return OTA_RESPONSE_ERROR_ESP32_NOT_ENOUGH_SPACE;
}
#ifdef USE_OTA_BLOCK_ERASE_AHEAD
OTAResponseTypes erase_result = this->erase_ahead_(len);
if (erase_result != OTA_RESPONSE_OK) {
return erase_result;
}
#endif
esp_err_t err = esp_ota_write(this->update_handle_, data, len);
this->md5_.add(data, len);
@@ -127,14 +149,40 @@ OTAResponseTypes IDFOTABackend::write(uint8_t *data, size_t len) {
ESP_LOGE(TAG, "esp_ota_write failed (err=0x%X)", err);
if (err == ESP_ERR_OTA_VALIDATE_FAILED) {
return OTA_RESPONSE_ERROR_MAGIC;
} else if (err == ESP_ERR_INVALID_SIZE) {
// Sequential-writes fallback: IDF's lazy erase reports overflow here
return OTA_RESPONSE_ERROR_ESP32_NOT_ENOUGH_SPACE;
} else if (err == ESP_ERR_FLASH_OP_TIMEOUT || err == ESP_ERR_FLASH_OP_FAIL) {
return OTA_RESPONSE_ERROR_WRITING_FLASH;
}
return OTA_RESPONSE_ERROR_UNKNOWN;
}
this->written_ += len;
return OTA_RESPONSE_OK;
}
#ifdef USE_OTA_BLOCK_ERASE_AHEAD
OTAResponseTypes IDFOTABackend::erase_ahead_(size_t len) {
const size_t end = this->written_ + len;
if (this->erased_end_ >= end) {
return OTA_RESPONSE_OK;
}
// Round up to a block boundary, clamped to the partition end; IDF splits the
// range into 64 KiB block erases where aligned, sector erases elsewhere.
const size_t erase_to = next_erase_end(end, this->partition_->size);
// A block erase is one uninterruptible flash op (typically ~150 ms, seconds
// on aged flash) and the transfer loop may not have fed the WDT for ~1s.
watchdog::WatchdogManager watchdog(15000);
esp_err_t err = esp_partition_erase_range(this->partition_, this->erased_end_, erase_to - this->erased_end_);
if (err != ESP_OK) {
ESP_LOGE(TAG, "esp_partition_erase_range failed (err=0x%X)", err);
return err == ESP_ERR_INVALID_SIZE ? OTA_RESPONSE_ERROR_ESP32_NOT_ENOUGH_SPACE : OTA_RESPONSE_ERROR_WRITING_FLASH;
}
this->erased_end_ = erase_to;
return OTA_RESPONSE_OK;
}
#endif
OTAResponseTypes IDFOTABackend::end() {
if (this->md5_set_) {
this->md5_.calculate();
@@ -226,6 +274,10 @@ void IDFOTABackend::abort() {
// or not an update is in flight.
esp_ota_abort(this->update_handle_);
this->update_handle_ = 0;
this->written_ = 0;
#ifdef USE_OTA_BLOCK_ERASE_AHEAD
this->erased_end_ = 0;
#endif
}
} // namespace esphome::ota
+18 -1
View File
@@ -5,8 +5,18 @@
#include "esphome/components/md5/md5.h"
#include "esphome/core/defines.h"
#include <esp_idf_version.h>
#include <esp_ota_ops.h>
// esp_ota_resume() (IDF 5.4.2+, backported to 5.3.3) provides a no-erase OTA
// handle, letting write() block-erase 64 KiB ahead of the write cursor
// (~4x faster than the per-sector lazy erase of OTA_WITH_SEQUENTIAL_WRITES,
// used as fallback on older IDF).
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 4, 2) || \
(ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 3, 3) && ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 4, 0))
#define USE_OTA_BLOCK_ERASE_AHEAD
#endif
namespace esphome::ota {
#ifdef USE_OTA_PARTITIONS
@@ -54,6 +64,9 @@ class IDFOTABackend final {
#endif
private:
#ifdef USE_OTA_BLOCK_ERASE_AHEAD
OTAResponseTypes erase_ahead_(size_t len);
#endif
#ifdef USE_OTA_SIGNED_VERIFICATION_MULTI_KEY
// Accept an image signed by any key the running app trusts (up to 3 blocks),
// so rotation and backup keys work. Fails closed. Covers app and bootloader.
@@ -62,7 +75,11 @@ class IDFOTABackend final {
// Keep md5_ first since its digest_ is alignas(32) on DMA-SHA variants; md5_set_ stays last so buf_ packs tightly.
md5::MD5Digest md5_{};
esp_ota_handle_t update_handle_{0};
const esp_partition_t *partition_;
const esp_partition_t *partition_{nullptr};
size_t written_{0}; // Bytes handed to esp_ota_write()
#ifdef USE_OTA_BLOCK_ERASE_AHEAD
size_t erased_end_{0}; // Erased up to this partition offset; must stay >= written_
#endif
char expected_bin_md5_[32];
bool md5_set_{false};
#ifdef USE_OTA_PARTITIONS
@@ -1,6 +1,7 @@
#ifdef USE_ESP32
#include "ota_backend_esp_idf.h"
#include "esphome/components/watchdog/watchdog.h"
#include "esphome/core/defines.h"
#ifdef USE_OTA_PARTITIONS
@@ -69,12 +70,20 @@ OTAResponseTypes IDFOTABackend::setup_bootloader_staging_() {
return OTA_RESPONSE_ERROR_BOOTLOADER_VERIFY;
}
// Erase full size of the bootloader partition in the staging partition
// to avoid copying old data to the bootloader partition later
// to avoid copying old data to the bootloader partition later. Up to
// ESP_BOOTLOADER_SIZE of blocking erase; widen the WDT for its duration.
watchdog::WatchdogManager watchdog(15000);
esp_err_t err = esp_partition_erase_range(this->partition_, 0, this->bootloader_part_->size);
if (err != ESP_OK) {
ESP_LOGW(TAG, "esp_partition_erase_range failed (err=0x%X)", err);
// No critical error, don't return
}
#ifdef USE_OTA_BLOCK_ERASE_AHEAD
if (err == ESP_OK) {
// Skip re-erasing the pre-erased staging region in erase_ahead_()
this->erased_end_ = this->bootloader_part_->size;
}
#endif
err = esp_ota_set_final_partition(this->update_handle_, this->bootloader_part_, false);
if (err != ESP_OK) {
esp_ota_abort(this->update_handle_);
@@ -211,7 +211,7 @@ bool rsa_pss_verify(uint8_t *block, const uint8_t *digest) {
bool IDFOTABackend::verify_signed_image_(const esp_partition_t *incoming) {
// Verification re-hashes the full image (after esp_ota_end already did one
// pass), which can approach the task WDT budget on a large app. Extend it for
// the duration, mirroring the erase budget in begin().
// the duration, scaled to the image size over a 15 s floor.
const uint32_t verify_budget_ms = 15000 + (incoming->size >> 10) * 10;
watchdog::WatchdogManager watchdog(verify_budget_ms);
+6
View File
@@ -5,6 +5,7 @@ from esphome import automation
import esphome.codegen as cg
from esphome.components import mqtt, web_server, zigbee
from esphome.components.const import CONF_B_CONSTANT
from esphome.config_helpers import filter_source_files_from_defines
import esphome.config_validation as cv
from esphome.const import (
CONF_ABOVE,
@@ -1303,3 +1304,8 @@ def _lstsq(a, b):
@coroutine_with_priority(CoroPriority.CORE)
async def to_code(config):
cg.add_global(sensor_ns.using)
FILTER_SOURCE_FILES = filter_source_files_from_defines(
{"filter.cpp": "USE_SENSOR_FILTER"}
)
@@ -1,6 +1,7 @@
from esphome import automation
import esphome.codegen as cg
from esphome.components import mqtt, web_server
from esphome.config_helpers import filter_source_files_from_defines
import esphome.config_validation as cv
from esphome.const import (
CONF_DEVICE_CLASS,
@@ -256,3 +257,8 @@ async def text_sensor_state_to_code(config, condition_id, template_arg, args):
templ = await cg.templatable(config[CONF_STATE], args, cg.std_string)
cg.add(var.set_state(templ))
return var
FILTER_SOURCE_FILES = filter_source_files_from_defines(
{"filter.cpp": "USE_TEXT_SENSOR_FILTER"}
)
+4 -7
View File
@@ -1,5 +1,6 @@
import esphome.codegen as cg
from esphome.components import sensor, time
from esphome.config_helpers import filter_source_files_from_defines
import esphome.config_validation as cv
from esphome.const import (
CONF_TIME_ID,
@@ -10,7 +11,6 @@ from esphome.const import (
STATE_CLASS_TOTAL_INCREASING,
UNIT_SECOND,
)
from esphome.core import CORE
uptime_ns = cg.esphome_ns.namespace("uptime")
UptimeSecondsSensor = uptime_ns.class_(
@@ -62,9 +62,6 @@ async def to_code(config):
cg.add(var.set_time(time_id))
def FILTER_SOURCE_FILES() -> list[str]:
# uptime_timestamp_sensor.cpp is fully #ifdef'd on USE_TIME; skip it
# when no time component is configured.
if not any(define.name == "USE_TIME" for define in CORE.defines):
return ["uptime_timestamp_sensor.cpp"]
return []
FILTER_SOURCE_FILES = filter_source_files_from_defines(
{"uptime_timestamp_sensor.cpp": "USE_TIME"}
)
@@ -117,12 +117,6 @@ class AsyncWebServerRequest {
/// Write URL (without query string) to buffer, returns StringRef pointing to buffer.
/// URL is decoded (e.g., %20 -> space).
StringRef url_to(std::span<char, URL_BUF_SIZE> buffer) const;
// Remove before 2026.9.0
ESPDEPRECATED("Use url_to() instead. Removed in 2026.9.0", "2026.3.0")
std::string url() const {
char buffer[URL_BUF_SIZE];
return std::string(this->url_to(buffer));
}
// NOLINTNEXTLINE(readability-identifier-naming)
size_t contentLength() const { return this->req_->content_len; }
@@ -618,8 +618,6 @@ static const char *eap_phase2_to_str(esp_eap_ttls_phase2_types type) {
}
#endif
float WiFiComponent::get_setup_priority() const { return setup_priority::WIFI; }
void WiFiComponent::setup() {
this->wifi_pre_setup_();
@@ -931,10 +929,6 @@ void WiFiComponent::loop() {
WiFiComponent::WiFiComponent() { global_wifi_component = this; }
#ifdef USE_WIFI_11KV_SUPPORT
void WiFiComponent::set_btm(bool btm) { this->btm_ = btm; }
void WiFiComponent::set_rrm(bool rrm) { this->rrm_ = rrm; }
#endif
network::IPAddresses WiFiComponent::get_ip_addresses() {
if (this->has_sta())
return this->wifi_sta_ip_addresses();
@@ -1327,8 +1321,6 @@ void WiFiComponent::disable() {
this->wifi_mode_(false, false);
}
bool WiFiComponent::is_disabled() { return this->state_ == WIFI_COMPONENT_STATE_DISABLED; }
void WiFiComponent::start_scanning() {
this->action_started_ = millis();
ESP_LOGD(TAG, "Starting scan");
@@ -2196,7 +2188,6 @@ void WiFiComponent::retry_connect() {
}
}
void WiFiComponent::set_reboot_timeout(uint32_t reboot_timeout) { this->reboot_timeout_ = reboot_timeout; }
void WiFiComponent::set_power_save_mode(WiFiPowerSaveMode power_save) {
this->power_save_ = power_save;
#if defined(USE_ESP32) && defined(USE_WIFI_RUNTIME_POWER_SAVE)
@@ -2204,8 +2195,6 @@ void WiFiComponent::set_power_save_mode(WiFiPowerSaveMode power_save) {
#endif
}
void WiFiComponent::set_passive_scan(bool passive) { this->passive_scan_ = passive; }
bool WiFiComponent::is_captive_portal_active_() {
#ifdef USE_CAPTIVE_PORTAL
return captive_portal::global_captive_portal != nullptr && captive_portal::global_captive_portal->is_active();
@@ -2324,33 +2313,6 @@ void WiFiComponent::save_fast_connect_settings_(const bssid_t &bssid, uint8_t ch
}
#endif
void WiFiAP::set_ssid(const std::string &ssid) { this->ssid_ = CompactString(ssid.c_str(), ssid.size()); }
void WiFiAP::set_ssid(const char *ssid) { this->ssid_ = CompactString(ssid, strlen(ssid)); }
void WiFiAP::set_bssid(const bssid_t &bssid) { this->bssid_ = bssid; }
void WiFiAP::clear_bssid() { this->bssid_ = {}; }
void WiFiAP::set_password(const std::string &password) {
this->password_ = CompactString(password.c_str(), password.size());
}
void WiFiAP::set_password(const char *password) { this->password_ = CompactString(password, strlen(password)); }
#ifdef USE_WIFI_WPA2_EAP
void WiFiAP::set_eap(optional<EAPAuth> eap_auth) { this->eap_ = std::move(eap_auth); }
#endif
void WiFiAP::set_channel(uint8_t channel) { this->channel_ = channel; }
void WiFiAP::clear_channel() { this->channel_ = 0; }
#ifdef USE_WIFI_MANUAL_IP
void WiFiAP::set_manual_ip(optional<ManualIP> manual_ip) { this->manual_ip_ = manual_ip; }
#endif
void WiFiAP::set_hidden(bool hidden) { this->hidden_ = hidden; }
const bssid_t &WiFiAP::get_bssid() const { return this->bssid_; }
bool WiFiAP::has_bssid() const { return this->bssid_ != bssid_t{}; }
#ifdef USE_WIFI_WPA2_EAP
const optional<EAPAuth> &WiFiAP::get_eap() const { return this->eap_; }
#endif
#ifdef USE_WIFI_MANUAL_IP
const optional<ManualIP> &WiFiAP::get_manual_ip() const { return this->manual_ip_; }
#endif
bool WiFiAP::get_hidden() const { return this->hidden_; }
WiFiScanResult::WiFiScanResult(const bssid_t &bssid, const char *ssid, size_t ssid_len, uint8_t channel, int8_t rssi,
bool with_auth, bool is_hidden)
: bssid_(bssid),
+24 -25
View File
@@ -21,6 +21,7 @@
#include <span>
#include <string>
#include <type_traits>
#include <utility>
#include <vector>
#ifdef USE_LIBRETINY
@@ -261,38 +262,38 @@ class WiFiAP {
friend class WiFiScanResult;
public:
void set_ssid(const std::string &ssid);
void set_ssid(const char *ssid);
void set_ssid(const std::string &ssid) { this->ssid_ = CompactString(ssid.c_str(), ssid.size()); }
void set_ssid(const char *ssid) { this->set_ssid(StringRef(ssid)); }
void set_ssid(StringRef ssid) { this->ssid_ = CompactString(ssid.c_str(), ssid.size()); }
void set_bssid(const bssid_t &bssid);
void clear_bssid();
void set_password(const std::string &password);
void set_password(const char *password);
void set_bssid(const bssid_t &bssid) { this->bssid_ = bssid; }
void clear_bssid() { this->bssid_ = {}; }
void set_password(const std::string &password) { this->password_ = CompactString(password.c_str(), password.size()); }
void set_password(const char *password) { this->set_password(StringRef(password)); }
void set_password(StringRef password) { this->password_ = CompactString(password.c_str(), password.size()); }
#ifdef USE_WIFI_WPA2_EAP
void set_eap(optional<EAPAuth> eap_auth);
void set_eap(optional<EAPAuth> eap_auth) { this->eap_ = std::move(eap_auth); }
#endif // USE_WIFI_WPA2_EAP
void set_channel(uint8_t channel);
void clear_channel();
void set_channel(uint8_t channel) { this->channel_ = channel; }
void clear_channel() { this->channel_ = 0; }
void set_priority(int8_t priority) { priority_ = priority; }
#ifdef USE_WIFI_MANUAL_IP
void set_manual_ip(optional<ManualIP> manual_ip);
void set_manual_ip(optional<ManualIP> manual_ip) { this->manual_ip_ = manual_ip; }
#endif
void set_hidden(bool hidden);
void set_hidden(bool hidden) { this->hidden_ = hidden; }
StringRef get_ssid() const { return this->ssid_.ref(); }
StringRef get_password() const { return this->password_.ref(); }
const bssid_t &get_bssid() const;
bool has_bssid() const;
const bssid_t &get_bssid() const { return this->bssid_; }
bool has_bssid() const { return this->bssid_ != bssid_t{}; }
#ifdef USE_WIFI_WPA2_EAP
const optional<EAPAuth> &get_eap() const;
const optional<EAPAuth> &get_eap() const { return this->eap_; }
#endif // USE_WIFI_WPA2_EAP
uint8_t get_channel() const { return this->channel_; }
bool has_channel() const { return this->channel_ != 0; }
int8_t get_priority() const { return priority_; }
#ifdef USE_WIFI_MANUAL_IP
const optional<ManualIP> &get_manual_ip() const;
const optional<ManualIP> &get_manual_ip() const { return this->manual_ip_; }
#endif
bool get_hidden() const;
bool get_hidden() const { return this->hidden_; }
protected:
CompactString ssid_;
@@ -442,6 +443,7 @@ class WiFiComponent final : public Component {
void set_sta(const WiFiAP &ap);
// Returns a copy of the currently selected AP configuration
WiFiAP get_sta() const;
// init_sta/add_sta kept out of line: inlining them into the generated setup() grows flash
void init_sta(size_t count);
void add_sta(const WiFiAP &ap);
void clear_sta();
@@ -461,7 +463,7 @@ class WiFiComponent final : public Component {
void enable();
void disable();
bool is_disabled();
bool is_disabled() { return this->state_ == WIFI_COMPONENT_STATE_DISABLED; }
void start_scanning();
void check_scanning_finished();
void start_connecting(const WiFiAP &ap);
@@ -472,7 +474,7 @@ class WiFiComponent final : public Component {
void retry_connect();
void set_reboot_timeout(uint32_t reboot_timeout);
void set_reboot_timeout(uint32_t reboot_timeout) { this->reboot_timeout_ = reboot_timeout; }
bool is_connected() const { return this->connected_; }
@@ -492,7 +494,7 @@ class WiFiComponent final : public Component {
void set_phy_mode(WiFi8266PhyMode phy_mode) { this->phy_mode_ = phy_mode; }
#endif
void set_passive_scan(bool passive);
void set_passive_scan(bool passive) { this->passive_scan_ = passive; }
void save_wifi_sta(const std::string &ssid, const std::string &password);
void save_wifi_sta(const char *ssid, const char *password);
@@ -506,7 +508,7 @@ class WiFiComponent final : public Component {
void dump_config() override;
void restart_adapter();
/// WIFI setup_priority.
float get_setup_priority() const override;
float get_setup_priority() const override { return setup_priority::WIFI; }
/// Reconnect WiFi if required.
void loop() override;
@@ -515,8 +517,8 @@ class WiFiComponent final : public Component {
bool is_ap_active() const { return this->ap_started_; }
#ifdef USE_WIFI_11KV_SUPPORT
void set_btm(bool btm);
void set_rrm(bool rrm);
void set_btm(bool btm) { this->btm_ = btm; }
void set_rrm(bool rrm) { this->rrm_ = rrm; }
#endif
network::IPAddress get_dns_address(int num);
@@ -550,9 +552,6 @@ class WiFiComponent final : public Component {
void set_sta_priority(bssid_t bssid, int8_t priority);
network::IPAddresses wifi_sta_ip_addresses();
// Remove before 2026.9.0
ESPDEPRECATED("Use wifi_ssid_to() instead. Removed in 2026.9.0", "2026.3.0")
std::string wifi_ssid();
/// Write SSID to buffer without heap allocation.
/// Returns pointer to buffer, or empty string if not connected.
const char *wifi_ssid_to(std::span<char, SSID_BUFFER_SIZE> buffer);
@@ -944,16 +944,6 @@ bssid_t WiFiComponent::wifi_bssid() {
}
return bssid;
}
std::string WiFiComponent::wifi_ssid() {
struct station_config conf {};
if (!wifi_station_get_config(&conf)) {
return "";
}
// conf.ssid is uint8[32], not null-terminated if full
auto *ssid_s = reinterpret_cast<const char *>(conf.ssid);
size_t len = strnlen(ssid_s, sizeof(conf.ssid));
return {ssid_s, len};
}
const char *WiFiComponent::wifi_ssid_to(std::span<char, SSID_BUFFER_SIZE> buffer) {
struct station_config conf {};
if (!wifi_station_get_config(&conf)) {
@@ -1237,18 +1237,6 @@ bssid_t WiFiComponent::wifi_bssid() {
std::copy(info.bssid, info.bssid + 6, bssid.begin());
return bssid;
}
std::string WiFiComponent::wifi_ssid() {
wifi_ap_record_t info{};
esp_err_t err = esp_wifi_sta_get_ap_info(&info);
if (err != ESP_OK) {
// Very verbose only: this is expected during dump_config() before connection is established (PR #9823)
ESP_LOGVV(TAG, "esp_wifi_sta_get_ap_info failed: %s", esp_err_to_name(err));
return "";
}
auto *ssid_s = reinterpret_cast<const char *>(info.ssid);
size_t len = strnlen(ssid_s, sizeof(info.ssid));
return {ssid_s, len};
}
const char *WiFiComponent::wifi_ssid_to(std::span<char, SSID_BUFFER_SIZE> buffer) {
wifi_ap_record_t info{};
esp_err_t err = esp_wifi_sta_get_ap_info(&info);
@@ -762,7 +762,6 @@ bssid_t WiFiComponent::wifi_bssid() {
}
return bssid;
}
std::string WiFiComponent::wifi_ssid() { return WiFi.SSID().c_str(); }
const char *WiFiComponent::wifi_ssid_to(std::span<char, SSID_BUFFER_SIZE> buffer) {
#ifdef USE_BK72XX
LinkStatusTypeDef link_status{};
@@ -265,7 +265,6 @@ bssid_t WiFiComponent::wifi_bssid() {
bssid[i] = raw_bssid[i];
return bssid;
}
std::string WiFiComponent::wifi_ssid() { return WiFi.SSID().c_str(); }
const char *WiFiComponent::wifi_ssid_to(std::span<char, SSID_BUFFER_SIZE> buffer) {
// TODO: Find direct CYW43 API to avoid Arduino String allocation
String ssid = WiFi.SSID();
+25
View File
@@ -151,6 +151,31 @@ def filter_source_files_from_platform(
return filter_source_files
def filter_source_files_from_defines(
files_map: dict[str, str | tuple[str, ...]],
) -> Callable[[], list[str]]:
"""Helper to build a FILTER_SOURCE_FILES function from a define mapping.
Args:
files_map: Dict mapping filename to the define name (or tuple of
define names) that keeps the file in the build; the file is
excluded when none of its defines is set for the current config.
Returns:
Function that returns the files to exclude for the current config.
"""
def filter_source_files() -> list[str]:
defines = {define.name for define in CORE.defines}
return [
filename
for filename, needed in files_map.items()
if defines.isdisjoint((needed,) if isinstance(needed, str) else needed)
]
return filter_source_files
def get_logger_level() -> str:
"""Get the configured logger level.
+4
View File
@@ -43,7 +43,9 @@
#define USE_ALARM_CONTROL_PANEL
#define USE_AREAS
#define USE_BINARY_SENSOR
#define USE_BINARY_SENSOR_CLICK_TRIGGER
#define USE_BINARY_SENSOR_FILTER
#define USE_BINARY_SENSOR_MULTI_CLICK_TRIGGER
#define USE_BLE_DEVICE_IRK
#define USE_BUTTON
#define USE_CAMERA
@@ -220,6 +222,7 @@
#define API_MAX_SEND_QUEUE 8
#define MAX_API_CONNECTIONS 6
#define USE_MD5
#define USE_NOISE
#define USE_SHA256
#ifndef USE_RP2 // no MQTT backend or esp_wireguard library on RP2
#define USE_MQTT
@@ -281,6 +284,7 @@
// ESP32-specific feature flags
#ifdef USE_ESP32
#define USE_ESP32_CRASH_HANDLER
#define USE_ESP32_INTERNAL_GPIO
#define USE_MQTT_IDF_ENQUEUE
#define USE_ESPHOME_TASK_LOG_BUFFER
#define ESPHOME_TASK_LOG_BUFFER_SIZE 768
-40
View File
@@ -80,24 +80,6 @@ const char *EntityBase::get_device_class_to([[maybe_unused]] std::span<char, MAX
#endif
}
#ifndef USE_ESP8266
// Deprecated device class accessors — not available on ESP8266 (rodata is RAM)
StringRef EntityBase::get_device_class_ref() const {
#ifdef USE_ENTITY_DEVICE_CLASS
return StringRef(entity_device_class_lookup(this->device_class_idx_));
#else
return StringRef(entity_device_class_lookup(0));
#endif
}
std::string EntityBase::get_device_class() const {
#ifdef USE_ENTITY_DEVICE_CLASS
return std::string(entity_device_class_lookup(this->device_class_idx_));
#else
return std::string(entity_device_class_lookup(0));
#endif
}
#endif // !USE_ESP8266
// Entity unit of measurement (from index)
StringRef EntityBase::get_unit_of_measurement_ref() const {
#ifdef USE_ENTITY_UNIT_OF_MEASUREMENT
@@ -106,10 +88,6 @@ StringRef EntityBase::get_unit_of_measurement_ref() const {
return StringRef(entity_uom_lookup(0));
#endif
}
std::string EntityBase::get_unit_of_measurement() const {
return std::string(this->get_unit_of_measurement_ref().c_str());
}
// Entity icon — buffer-based API for PROGMEM safety on ESP8266
const char *EntityBase::get_icon_to([[maybe_unused]] std::span<char, MAX_ICON_LENGTH> buffer) const {
#ifdef USE_ENTITY_ICON
@@ -129,24 +107,6 @@ const char *EntityBase::get_icon_to([[maybe_unused]] std::span<char, MAX_ICON_LE
#endif
}
#ifndef USE_ESP8266
// Deprecated icon accessors — not available on ESP8266 (rodata is RAM)
StringRef EntityBase::get_icon_ref() const {
#ifdef USE_ENTITY_ICON
return StringRef(entity_icon_lookup(this->icon_idx_));
#else
return StringRef(entity_icon_lookup(0));
#endif
}
std::string EntityBase::get_icon() const {
#ifdef USE_ENTITY_ICON
return std::string(entity_icon_lookup(this->icon_idx_));
#else
return std::string(entity_icon_lookup(0));
#endif
}
#endif // !USE_ESP8266
// Calculate Object ID Hash directly from name using snake_case + sanitize
void EntityBase::calc_object_id_() {
this->object_id_hash_ = fnv1_hash_object_id(this->name_.c_str(), this->name_.size());
-46
View File
@@ -109,60 +109,14 @@ class EntityBase {
// On ESP8266: copies from PROGMEM to buffer, returns buffer pointer.
const char *get_device_class_to(std::span<char, MAX_DEVICE_CLASS_LENGTH> buffer) const;
#ifdef USE_ESP8266
// On ESP8266, rodata is RAM. Device classes are in PROGMEM and cannot be accessed
// directly as const char*. Use get_device_class_to() with a stack buffer instead.
template<typename T = int> StringRef get_device_class_ref() const {
static_assert(sizeof(T) == 0, "get_device_class_ref() unavailable on ESP8266 (rodata is RAM). "
"Use get_device_class_to() with a stack buffer.");
return StringRef("");
}
template<typename T = int> std::string get_device_class() const {
static_assert(sizeof(T) == 0, "get_device_class() unavailable on ESP8266 (rodata is RAM). "
"Use get_device_class_to() with a stack buffer.");
return "";
}
#else
// Deprecated: use get_device_class_to() instead. Device classes are in PROGMEM.
ESPDEPRECATED("Use get_device_class_to() instead. Will be removed in ESPHome 2026.9.0", "2026.3.0")
StringRef get_device_class_ref() const;
ESPDEPRECATED("Use get_device_class_to() instead. Will be removed in ESPHome 2026.9.0", "2026.3.0")
std::string get_device_class() const;
#endif
// Get unit of measurement as StringRef (from packed index)
StringRef get_unit_of_measurement_ref() const;
/// Get the unit of measurement as std::string (deprecated, prefer get_unit_of_measurement_ref())
ESPDEPRECATED("Use get_unit_of_measurement_ref() instead for better performance (avoids string copy). Will be "
"removed in ESPHome 2026.9.0",
"2026.3.0")
std::string get_unit_of_measurement() const;
// Get this entity's icon into a stack buffer.
// On ESP32: returns pointer to PROGMEM string directly (buffer unused).
// On ESP8266: copies from PROGMEM to buffer, returns buffer pointer.
const char *get_icon_to(std::span<char, MAX_ICON_LENGTH> buffer) const;
#ifdef USE_ESP8266
// On ESP8266, rodata is RAM. Icons are in PROGMEM and cannot be accessed
// directly as const char*. Use get_icon_to() with a stack buffer instead.
template<typename T = int> StringRef get_icon_ref() const {
static_assert(sizeof(T) == 0,
"get_icon_ref() unavailable on ESP8266 (rodata is RAM). Use get_icon_to() with a stack buffer.");
return StringRef("");
}
template<typename T = int> std::string get_icon() const {
static_assert(sizeof(T) == 0,
"get_icon() unavailable on ESP8266 (rodata is RAM). Use get_icon_to() with a stack buffer.");
return "";
}
#else
// Deprecated: use get_icon_to() instead. Icons are in PROGMEM.
ESPDEPRECATED("Use get_icon_to() instead. Will be removed in ESPHome 2026.9.0", "2026.3.0")
StringRef get_icon_ref() const;
ESPDEPRECATED("Use get_icon_to() instead. Will be removed in ESPHome 2026.9.0", "2026.3.0")
std::string get_icon() const;
#endif
#ifdef USE_DEVICES
// Get this entity's device id
uint32_t get_device_id() const {
-17
View File
@@ -723,23 +723,6 @@ bool base64_decode_int32_vector(const std::string &base64, std::vector<int32_t>
// Colors
float gamma_correct(float value, float gamma) {
if (value <= 0.0f)
return 0.0f;
if (gamma <= 0.0f)
return value;
return powf(value, gamma); // NOLINT - deprecated, removal 2026.9.0
}
float gamma_uncorrect(float value, float gamma) {
if (value <= 0.0f)
return 0.0f;
if (gamma <= 0.0f)
return value;
return powf(value, 1 / gamma); // NOLINT - deprecated, removal 2026.9.0
}
void rgb_to_hsv(float red, float green, float blue, int &hue, float &saturation, float &value) {
float max_color_value = std::max({red, green, blue});
float min_color_value = std::min({red, green, blue});
-9
View File
@@ -1646,15 +1646,6 @@ bool base64_decode_int32_vector(const std::string &base64, std::vector<int32_t>
/// @name Colors
///@{
/// Applies gamma correction of \p gamma to \p value.
// Remove before 2026.9.0
ESPDEPRECATED("Use LightState::gamma_correct_lut() instead. Removed in 2026.9.0.", "2026.3.0")
float gamma_correct(float value, float gamma);
/// Reverts gamma correction of \p gamma to \p value.
// Remove before 2026.9.0
ESPDEPRECATED("Use LightState::gamma_uncorrect_lut() instead. Removed in 2026.9.0.", "2026.3.0")
float gamma_uncorrect(float value, float gamma);
/// Convert \p red, \p green and \p blue (all 0-1) values to \p hue (0-360), \p saturation (0-1) and \p value (0-1).
void rgb_to_hsv(float red, float green, float blue, int &hue, float &saturation, float &value);
/// Convert \p hue (0-360), \p saturation (0-1) and \p value (0-1) to \p red, \p green and \p blue (all 0-1).
-10
View File
@@ -60,16 +60,6 @@ void HOT esp_log_vprintf_(int level, const char *tag, int line, const char *form
#endif
}
#ifdef USE_STORE_LOG_STR_IN_FLASH
// Remove before 2026.9.0
void HOT esp_log_vprintf_(int level, const char *tag, int line, const __FlashStringHelper *format, va_list args) {
#ifdef USE_LOGGER
ESPHOME_DEBUG_ASSERT(logger::global_logger != nullptr);
logger::global_logger->log_vprintf_(static_cast<uint8_t>(level), tag, line, format, args);
#endif
}
#endif
#ifdef USE_ESP32
int HOT esp_idf_log_vprintf_(const char *format, va_list args) { // NOLINT
#ifdef USE_LOGGER
-5
View File
@@ -68,11 +68,6 @@ void esp_log_printf_(int level, const char *tag, int line, const char *format, .
void esp_log_printf_(int level, const char *tag, int line, const __FlashStringHelper *format, ...);
#endif
void esp_log_vprintf_(int level, const char *tag, int line, const char *format, va_list args); // NOLINT
#ifdef USE_STORE_LOG_STR_IN_FLASH
// Remove before 2026.9.0
__attribute__((deprecated("Use esp_log_printf_() instead. Removed in 2026.9.0."))) void esp_log_vprintf_(
int level, const char *tag, int line, const __FlashStringHelper *format, va_list args);
#endif
#if defined(USE_ESP32)
int esp_idf_log_vprintf_(const char *format, va_list args); // NOLINT
#endif
+12 -1
View File
@@ -552,7 +552,18 @@ def write_file_if_changed(path: Path, text: str) -> bool:
"""
src_content = None
if path.is_file():
src_content = read_file(path)
try:
src_content = path.read_text(encoding="utf-8")
except UnicodeDecodeError as err:
# Replace a damaged file rather than abort the regeneration that
# fixes it; an OSError may hide an intact file, so it still raises
_LOGGER.warning("Replacing damaged file %s: %s", path, err)
with suppress(OSError):
path.unlink(missing_ok=True)
except OSError as err:
from esphome.core import EsphomeError
raise EsphomeError(f"Error reading file {path}: {err}") from err
if src_content == text:
return False
write_file(path, text)
+3 -3
View File
@@ -45,7 +45,7 @@ lib_deps_base =
lib_deps =
${common.lib_deps_base}
https://github.com/dudanov/MideaUART.git#eeea6c3e9b4474f067054592b435be1c4e466815 ; midea
esphome/noise-c@0.1.21 ; api
esphome/noise-c@0.1.21 ; noise (api, ota)
improv/Improv@1.2.6 ; improv_serial / esp32_improv
kikuchan98/pngle@1.1.0 ; online_image
; Using the repository directly, otherwise ESP-IDF can't use the library
@@ -244,7 +244,7 @@ lib_deps =
${common:idf-component-libs.lib_deps}
ESP32Async/ESPAsyncWebServer@3.9.6 ; web_server_base
droscy/esp_wireguard@0.4.5 ; wireguard
esphome/noise-c@0.1.21 ; api
esphome/noise-c@0.1.21 ; noise (api, ota)
ESP32Async/AsyncTCP@3.4.5 ; async_tcp
DNSServer ; captive_portal
heman/AsyncMqttClient-esphome@2.0.0 ; mqtt
@@ -641,7 +641,7 @@ build_unflags =
extends = common
platform = platformio/native
lib_deps =
esphome/noise-c@0.1.21 ; used by api
esphome/noise-c@0.1.21 ; used by noise (api, ota)
lvgl/lvgl@9.5.0 ; lvgl
build_flags =
${common.build_flags}
+3 -2
View File
@@ -3,8 +3,9 @@ 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.
# api must run its to_code to define USE_API and USE_API_NOISE. The
# AUTO_LOADed noise component runs its own to_code via the override in
# tests/benchmarks/components/noise/__init__.py.
manifest.enable_codegen()
original_to_code = manifest.to_code
@@ -0,0 +1,7 @@
from tests.testing_helpers import ComponentManifestOverride
def override_manifest(manifest: ComponentManifestOverride) -> None:
# to_code must run: it defines USE_NOISE and adds the noise-c library
# the api benchmark sources need.
manifest.enable_codegen()
@@ -0,0 +1,37 @@
"""Tests for the shared noise encryption key helpers."""
from __future__ import annotations
import pytest
from esphome import config_validation as cv
from esphome.components.noise import decode_encryption_key, validate_encryption_key
KEY = "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8="
def test_validate_encryption_key_roundtrips() -> None:
assert validate_encryption_key(KEY) == KEY
@pytest.mark.parametrize("value", ["not-base64!!!", "AAECAw=="])
def test_validate_encryption_key_rejects_bad_input(value: str) -> None:
with pytest.raises(cv.Invalid):
validate_encryption_key(value)
def test_decode_encryption_key_returns_32_bytes() -> None:
assert decode_encryption_key(KEY) == bytes(range(32))
def test_decode_encryption_key_rejects_invalid_base64() -> None:
"""The shared helper raises cv.Invalid, not binascii.Error."""
with pytest.raises(cv.Invalid, match="base64"):
decode_encryption_key("A")
def test_decode_encryption_key_rejects_short_decode() -> None:
"""a2b_base64 stops at embedded padding; a short decode must not become
a zero padded PSK on the device."""
with pytest.raises(cv.Invalid, match="32 bytes"):
decode_encryption_key("AAECAw==")
@@ -136,3 +136,19 @@ binary_sensor:
invalid_cooldown: 2s
then:
- logger.log: "Click with custom cooldown"
# Test on_click and on_double_click (compiles match_interval via
# USE_BINARY_SENSOR_CLICK_TRIGGER)
- platform: template
id: click_triggers
name: "Click Triggers"
on_click:
min_length: 50ms
max_length: 350ms
then:
- logger.log: "Clicked"
on_double_click:
min_length: 50ms
max_length: 350ms
then:
- logger.log: "Double clicked"
+7
View File
@@ -0,0 +1,7 @@
from tests.testing_helpers import ComponentManifestOverride
def override_manifest(manifest: ComponentManifestOverride) -> None:
# to_code must run: it defines USE_NOISE and adds the noise-c library
# the component sources under test need.
manifest.enable_codegen()
+1
View File
@@ -0,0 +1 @@
noise:
@@ -0,0 +1,2 @@
packages:
noise: !include common.yaml
@@ -0,0 +1,2 @@
packages:
noise: !include common.yaml
+2
View File
@@ -0,0 +1,2 @@
packages:
noise: !include common.yaml
@@ -0,0 +1,2 @@
packages:
noise: !include common.yaml
@@ -0,0 +1,199 @@
#include <gtest/gtest.h>
#include <cstring>
#include <noise/protocol.h>
#include "esphome/components/noise/noise.h"
#include "esphome/components/noise/noise_handshake.h"
namespace esphome::noise::testing {
using Action = NoiseResponderHandshake::Action;
// A raw noise-c initiator driving the same Noise_NNpsk0_25519_ChaChaPoly_SHA256
// pattern the responder class implements, so the tests exercise a real
// two-message handshake rather than mirrored calls into the class under test.
class Initiator {
public:
Initiator(const psk_t &psk, const uint8_t *prologue, size_t prologue_len) {
const NoiseProtocolId nid = {
.prefix_id = NOISE_PREFIX_STANDARD,
.pattern_id = NOISE_PATTERN_NN,
.modifier_ids = {NOISE_MODIFIER_PSK0},
.dh_id = NOISE_DH_CURVE25519,
.cipher_id = NOISE_CIPHER_CHACHAPOLY,
.hash_id = NOISE_HASH_SHA256,
.hybrid_id = NOISE_DH_NONE,
};
EXPECT_EQ(noise_handshakestate_new_by_id(&this->state_, &nid, NOISE_ROLE_INITIATOR), 0);
EXPECT_EQ(noise_handshakestate_set_pre_shared_key(this->state_, psk.data(), psk.size()), 0);
EXPECT_EQ(noise_handshakestate_set_prologue(this->state_, prologue, prologue_len), 0);
EXPECT_EQ(noise_handshakestate_start(this->state_), 0);
}
~Initiator() {
if (this->state_ != nullptr)
noise_handshakestate_free(this->state_);
if (this->send_ != nullptr)
noise_cipherstate_free(this->send_);
if (this->recv_ != nullptr)
noise_cipherstate_free(this->recv_);
}
Initiator(const Initiator &) = delete;
Initiator &operator=(const Initiator &) = delete;
size_t write_message(uint8_t *out, size_t capacity) {
NoiseBuffer mbuf;
noise_buffer_init(mbuf);
noise_buffer_set_output(mbuf, out, capacity);
EXPECT_EQ(noise_handshakestate_write_message(this->state_, &mbuf, nullptr), 0);
return mbuf.size;
}
int read_message(uint8_t *data, size_t len) {
NoiseBuffer mbuf;
noise_buffer_init(mbuf);
noise_buffer_set_input(mbuf, data, len);
return noise_handshakestate_read_message(this->state_, &mbuf, nullptr);
}
void split() { EXPECT_EQ(noise_handshakestate_split(this->state_, &this->send_, &this->recv_), 0); }
NoiseCipherState *send_{nullptr};
NoiseCipherState *recv_{nullptr};
private:
NoiseHandshakeState *state_{nullptr};
};
static const uint8_t PROLOGUE[] = {'t', 'e', 's', 't', 'p', 'r', 'o', 'l', 'o', 'g', 'u', 'e'};
static psk_t make_psk(uint8_t seed) {
psk_t psk;
for (size_t i = 0; i < psk.size(); i++) {
psk[i] = static_cast<uint8_t>(seed + i);
}
return psk;
}
TEST(NoiseResponderHandshakeTest, ActionFailedBeforeInit) {
NoiseResponderHandshake handshake;
EXPECT_EQ(handshake.action(), Action::ACTION_FAILED);
}
TEST(NoiseResponderHandshakeTest, MessageMethodsErrorBeforeInit) {
// The class doc promises a noise-c error, not a crash, when the message
// methods run outside their action() step; pin the library's null check
NoiseResponderHandshake handshake;
uint8_t buf[MAX_HANDSHAKE_SIZE] = {};
size_t out_len = 0;
EXPECT_NE(handshake.read_message(buf, sizeof(buf)), 0);
EXPECT_NE(handshake.write_message(buf, sizeof(buf), out_len), 0);
// Deliberately non-null: split() documents a nullptr postcondition on
// error, so a caller's uninitialized locals never hold garbage to free
auto *sentinel = reinterpret_cast<NoiseCipherState *>(0x1);
NoiseCipherState *send_cipher = sentinel;
NoiseCipherState *recv_cipher = sentinel;
EXPECT_NE(handshake.split(send_cipher, recv_cipher), 0);
EXPECT_EQ(send_cipher, nullptr);
EXPECT_EQ(recv_cipher, nullptr);
}
TEST(NoiseResponderHandshakeTest, FullHandshakeAndTransportRoundTrip) {
const psk_t psk = make_psk(7);
NoiseResponderHandshake responder;
ASSERT_EQ(responder.init(psk, PROLOGUE, sizeof(PROLOGUE)), 0);
EXPECT_EQ(responder.action(), Action::ACTION_READ);
Initiator initiator(psk, PROLOGUE, sizeof(PROLOGUE));
uint8_t msg[MAX_HANDSHAKE_SIZE];
size_t msg_len = initiator.write_message(msg, sizeof(msg));
ASSERT_GT(msg_len, 0u);
ASSERT_EQ(responder.read_message(msg, msg_len), 0);
ASSERT_EQ(responder.action(), Action::ACTION_WRITE);
size_t reply_len = 0;
ASSERT_EQ(responder.write_message(msg, sizeof(msg), reply_len), 0);
ASSERT_GT(reply_len, 0u);
ASSERT_EQ(responder.action(), Action::ACTION_SPLIT);
ASSERT_EQ(initiator.read_message(msg, reply_len), 0);
initiator.split();
NoiseCipherState *send_cipher = nullptr;
NoiseCipherState *recv_cipher = nullptr;
ASSERT_EQ(responder.split(send_cipher, recv_cipher), 0);
ASSERT_NE(send_cipher, nullptr);
ASSERT_NE(recv_cipher, nullptr);
// The handshake state is released by split(); the class reports FAILED after
EXPECT_EQ(responder.action(), Action::ACTION_FAILED);
EXPECT_EQ(static_cast<size_t>(noise_cipherstate_get_mac_length(send_cipher)), MAC_SIZE);
// Responder encrypts, initiator decrypts
uint8_t frame[64];
static constexpr char PLAINTEXT[] = "encrypted ota";
std::memcpy(frame, PLAINTEXT, sizeof(PLAINTEXT));
NoiseBuffer mbuf;
noise_buffer_init(mbuf);
noise_buffer_set_inout(mbuf, frame, sizeof(PLAINTEXT), sizeof(frame));
ASSERT_EQ(noise_cipherstate_encrypt(send_cipher, &mbuf), 0);
EXPECT_EQ(mbuf.size, sizeof(PLAINTEXT) + MAC_SIZE);
noise_buffer_set_inout(mbuf, frame, mbuf.size, sizeof(frame));
ASSERT_EQ(noise_cipherstate_decrypt(initiator.recv_, &mbuf), 0);
ASSERT_EQ(mbuf.size, sizeof(PLAINTEXT));
EXPECT_EQ(std::memcmp(frame, PLAINTEXT, sizeof(PLAINTEXT)), 0);
noise_cipherstate_free(send_cipher);
noise_cipherstate_free(recv_cipher);
}
TEST(NoiseResponderHandshakeTest, ReInitRestartsHandshake) {
// The documented retry shape: a repeated init() frees the previous state
// and starts over. The first message under the new key authenticating
// proves the restart took effect; the old state surviving would fail the
// MAC here.
NoiseResponderHandshake responder;
ASSERT_EQ(responder.init(make_psk(7), PROLOGUE, sizeof(PROLOGUE)), 0);
ASSERT_EQ(responder.init(make_psk(9), PROLOGUE, sizeof(PROLOGUE)), 0);
EXPECT_EQ(responder.action(), Action::ACTION_READ);
Initiator initiator(make_psk(9), PROLOGUE, sizeof(PROLOGUE));
uint8_t msg[MAX_HANDSHAKE_SIZE];
size_t msg_len = initiator.write_message(msg, sizeof(msg));
ASSERT_GT(msg_len, 0u);
EXPECT_EQ(responder.read_message(msg, msg_len), 0);
}
TEST(NoiseResponderHandshakeTest, WrongPskFailsWithMacFailure) {
NoiseResponderHandshake responder;
ASSERT_EQ(responder.init(make_psk(7), PROLOGUE, sizeof(PROLOGUE)), 0);
Initiator initiator(make_psk(200), PROLOGUE, sizeof(PROLOGUE));
uint8_t msg[MAX_HANDSHAKE_SIZE];
size_t msg_len = initiator.write_message(msg, sizeof(msg));
ASSERT_GT(msg_len, 0u);
int err = responder.read_message(msg, msg_len);
EXPECT_EQ(err, NOISE_ERROR_MAC_FAILURE);
EXPECT_EQ(responder.action(), Action::ACTION_FAILED);
}
TEST(NoiseResponderHandshakeTest, MismatchedPrologueFailsWithMacFailure) {
// The prologue binds the plaintext preamble for downgrade resistance; a
// tampered preamble must fail even with the right key.
const psk_t psk = make_psk(7);
NoiseResponderHandshake responder;
ASSERT_EQ(responder.init(psk, PROLOGUE, sizeof(PROLOGUE)), 0);
static const uint8_t TAMPERED[] = {'x'};
Initiator initiator(psk, TAMPERED, sizeof(TAMPERED));
uint8_t msg[MAX_HANDSHAKE_SIZE];
size_t msg_len = initiator.write_message(msg, sizeof(msg));
ASSERT_GT(msg_len, 0u);
EXPECT_EQ(responder.read_message(msg, msg_len), NOISE_ERROR_MAC_FAILURE);
}
} // namespace esphome::noise::testing
@@ -0,0 +1,74 @@
#include <gtest/gtest.h>
#include <cstring>
#include <noise/protocol.h>
#include "esphome/components/noise/noise.h"
namespace esphome::noise::testing {
TEST(NoiseContextTest, AllZerosPskIsReserved) {
psk_t zeros{};
EXPECT_TRUE(NoiseContext::is_all_zeros(zeros));
psk_t psk{};
psk[31] = 1;
EXPECT_FALSE(NoiseContext::is_all_zeros(psk));
NoiseContext ctx;
EXPECT_FALSE(ctx.has_psk());
ctx.set_psk(zeros);
EXPECT_FALSE(ctx.has_psk());
ctx.set_psk(psk);
EXPECT_TRUE(ctx.has_psk());
EXPECT_EQ(ctx.get_psk(), psk);
}
TEST(WireFormatTest, FrameHeaderIsIndicatorPlusBigEndianLength) {
uint8_t header[FRAME_HEADER_SIZE];
write_frame_header(header, 0x1234);
EXPECT_EQ(header[0], FRAME_INDICATOR);
EXPECT_EQ(header[1], 0x12);
EXPECT_EQ(header[2], 0x34);
}
TEST(WireFormatTest, RejectPayloadCarriesStatusByteAndMacFailureContract) {
// The MAC failure string is a wire contract: clients match it to report a
// wrong key. Format the payload exactly the way the handshake read path does.
uint8_t buf[64];
size_t len = format_reject_payload(buf, sizeof(buf), reject_reason_for(NOISE_ERROR_MAC_FAILURE));
static constexpr char EXPECTED[] = "Handshake MAC failure";
ASSERT_EQ(len, 1 + strlen(EXPECTED));
EXPECT_EQ(buf[0], HANDSHAKE_STATUS_REJECT);
EXPECT_EQ(memcmp(buf + 1, EXPECTED, strlen(EXPECTED)), 0);
// The exported floor covers the full MAC failure payload exactly
EXPECT_EQ(MAC_FAILURE_PAYLOAD_SIZE, 1 + strlen(EXPECTED));
// Any other error maps to the generic reason
len = format_reject_payload(buf, sizeof(buf), reject_reason_for(NOISE_ERROR_INVALID_STATE));
static constexpr char GENERIC[] = "Handshake error";
ASSERT_EQ(len, 1 + strlen(GENERIC));
EXPECT_EQ(memcmp(buf + 1, GENERIC, strlen(GENERIC)), 0);
}
TEST(WireFormatTest, RejectPayloadTruncatesToCapacity) {
uint8_t buf[8];
size_t len = format_reject_payload(buf, sizeof(buf), reject_reason_for(NOISE_ERROR_MAC_FAILURE));
ASSERT_EQ(len, sizeof(buf));
EXPECT_EQ(buf[0], HANDSHAKE_STATUS_REJECT);
EXPECT_EQ(memcmp(buf + 1, "Handsha", 7), 0);
// A one-byte buffer still carries the status byte
uint8_t tiny[1];
len = format_reject_payload(tiny, sizeof(tiny), reject_reason_for(NOISE_ERROR_MAC_FAILURE));
ASSERT_EQ(len, 1u);
EXPECT_EQ(tiny[0], HANDSHAKE_STATUS_REJECT);
// A zero-capacity buffer yields no payload and stays untouched
uint8_t none[1] = {0xAA};
EXPECT_EQ(format_reject_payload(none, 0, reject_reason_for(NOISE_ERROR_MAC_FAILURE)), 0u);
EXPECT_EQ(none[0], 0xAA);
}
} // namespace esphome::noise::testing
+41
View File
@@ -0,0 +1,41 @@
// Pins the lazy erase-ahead arithmetic used by the ESP-IDF OTA backend: the
// erased watermark must always cover the write end, stay 64 KiB block-aligned
// until the clamp, and never exceed the partition.
#include <gtest/gtest.h>
#include "esphome/components/ota/ota_backend.h"
namespace esphome::ota::testing {
static constexpr size_t BLOCK = 64 * 1024;
static constexpr size_t PART = 1835008; // 0x1C0000, a real app slot size
TEST(NextEraseEnd, FirstWriteRoundsUpToOneBlock) { EXPECT_EQ(next_erase_end(1024, PART), BLOCK); }
TEST(NextEraseEnd, ExactBlockBoundaryDoesNotOverErase) { EXPECT_EQ(next_erase_end(BLOCK, PART), BLOCK); }
TEST(NextEraseEnd, StraddlingWriteCoversNextBlock) { EXPECT_EQ(next_erase_end(BLOCK + 1, PART), 2 * BLOCK); }
TEST(NextEraseEnd, ClampsToPartitionEnd) {
// Partition sizes are sector multiples but not always block multiples
constexpr size_t part = 27 * BLOCK + 4096;
EXPECT_EQ(next_erase_end(27 * BLOCK + 1, part), part);
EXPECT_EQ(next_erase_end(part, part), part);
}
// Bootloader staging seeds erased_end_ mid-block (e.g. 0x8000); the target for
// a write past that seed must still cover the write end.
TEST(NextEraseEnd, MidBlockSeedStillCovered) { EXPECT_EQ(next_erase_end(0x8000 + 1024, PART), BLOCK); }
TEST(NextEraseEnd, SweepAlwaysCoversWriteEndWithinPartition) {
for (size_t end = 1; end <= PART; end += 4093) {
const size_t erased = next_erase_end(end, PART);
ASSERT_GE(erased, end);
ASSERT_LE(erased, PART);
// Block-aligned unless clamped at the partition end
ASSERT_TRUE(erased == PART || erased % BLOCK == 0);
}
}
} // namespace esphome::ota::testing
+1
View File
@@ -29,6 +29,7 @@ void setup() {
auto *ota = new esphome::ESPHomeOTAComponent(); // NOLINT
ota->set_port(8266);
App.register_component_(ota);
App.setup();
}
+24
View File
@@ -6,6 +6,7 @@ from unittest.mock import patch
import pytest
from esphome.config_helpers import (
filter_source_files_from_defines,
filter_source_files_from_platform,
frameworks_for_platforms,
get_logger_level,
@@ -18,6 +19,7 @@ from esphome.const import (
KEY_TARGET_PLATFORM,
PlatformFramework,
)
from esphome.core import Define
def test_filter_source_files_from_platform_esp32() -> None:
@@ -148,3 +150,25 @@ def test_frameworks_for_platforms_derives_and_rejects_unknown() -> None:
}
with pytest.raises(ValueError, match="unknown platform"):
frameworks_for_platforms(["esp32", "not_a_platform"])
def test_filter_source_files_from_defines() -> None:
"""Files are excluded unless one of their defines is set."""
files_map: dict[str, str | tuple[str, ...]] = {
"filter.cpp": "USE_SENSOR_FILTER",
"automation.cpp": ("USE_CLICK", "USE_MULTI_CLICK"),
}
filter_func: Callable[[], list[str]] = filter_source_files_from_defines(files_map)
with patch("esphome.config_helpers.CORE") as mock_core:
mock_core.defines = {Define("USE_SENSOR_FILTER")}
assert filter_func() == ["automation.cpp"]
mock_core.defines = {Define("USE_MULTI_CLICK")}
assert filter_func() == ["filter.cpp"]
mock_core.defines = {Define("USE_SENSOR_FILTER"), Define("USE_CLICK")}
assert filter_func() == []
mock_core.defines = set()
assert sorted(filter_func()) == ["automation.cpp", "filter.cpp"]
+25
View File
@@ -253,6 +253,31 @@ class Test_write_file_if_changed:
assert dst.read_text() == text
def test_damaged_existing_file_is_replaced(
self, tmp_path: Path, caplog: pytest.LogCaptureFixture
):
"""A non-UTF-8 existing file is logged and overwritten."""
dst = tmp_path / "generated.txt"
dst.write_bytes(b"\xff\xfe")
assert helpers.write_file_if_changed(dst, "fresh content") is True
assert dst.read_text(encoding="utf-8") == "fresh content"
assert "Replacing damaged file" in caplog.text
def test_unreadable_existing_file_still_raises(self, tmp_path: Path):
"""An OSError on the comparison read still raises EsphomeError."""
dst = tmp_path / "generated.txt"
dst.write_text("intact")
with (
patch.object(Path, "read_text", side_effect=OSError("permission denied")),
pytest.raises(EsphomeError, match="Error reading file"),
):
helpers.write_file_if_changed(dst, "fresh content")
assert dst.exists()
def test_dst_does_not_exist(self, tmp_path: Path):
text = "A files are unique.\n"
dst = tmp_path / "file-a.txt"