Compare commits

..
45 changed files with 420 additions and 430 deletions
@@ -5,7 +5,6 @@ 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,
@@ -561,11 +560,6 @@ _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]
@@ -679,15 +673,3 @@ 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,13 +1,8 @@
#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.
@@ -125,9 +120,6 @@ 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;
@@ -135,8 +127,4 @@ 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,7 +12,6 @@ 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,
@@ -3452,10 +3451,3 @@ 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"}
)
+7 -54
View File
@@ -124,15 +124,6 @@ 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;
@@ -207,28 +198,10 @@ 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.
@@ -381,11 +354,10 @@ 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 with a validly
// written frame only.
// Whether the fault address is meaningful real CPU faults only, not
// aborts/watchdogs or SoC-level pseudo exceptions.
static bool has_fault_addr() {
return s_raw_crash_data.exception == PANIC_EXCEPTION_FAULT && !s_raw_crash_data.pseudo_excause &&
cause_slot_was_written();
return s_raw_crash_data.exception == PANIC_EXCEPTION_FAULT && !s_raw_crash_data.pseudo_excause;
}
// The record was captured by a different firmware build (it survives soft
@@ -486,10 +458,6 @@ 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);
@@ -502,14 +470,6 @@ 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;
@@ -527,12 +487,8 @@ 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;
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.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);
}
@@ -554,11 +510,8 @@ 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;
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.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);
}
+2 -5
View File
@@ -1,7 +1,4 @@
#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)
#ifdef USE_ESP32
#include "gpio.h"
#include "esphome/core/log.h"
@@ -207,4 +204,4 @@ void IRAM_ATTR ISRInternalGPIOPin::pin_mode(gpio::Flags flags) {
} // namespace esphome
#endif // USE_ESP32 && USE_ESP32_INTERNAL_GPIO
#endif // USE_ESP32
-1
View File
@@ -257,7 +257,6 @@ 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() returns quickly; flash sectors are erased incrementally during write().
// begin() may block for a few seconds while it locks flash.
error_code = this->backend_->begin(ota_size, ota_type);
if (error_code != ota::OTA_RESPONSE_OK)
goto error; // NOLINT(cppcoreguidelines-avoid-goto)
@@ -159,6 +159,9 @@ 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,6 +928,11 @@ 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,6 +249,11 @@ 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,9 +64,8 @@ void OtaHttpRequestComponent::flash() {
}
}
void OtaHttpRequestComponent::cleanup_(ota::OTABackendPtr backend, const std::shared_ptr<HttpContainer> &container,
bool abort_backend) {
if (abort_backend) {
void OtaHttpRequestComponent::cleanup_(ota::OTABackendPtr backend, const std::shared_ptr<HttpContainer> &container) {
if (this->update_started_) {
ESP_LOGV(TAG, "Aborting OTA backend");
backend->abort();
}
@@ -107,8 +106,7 @@ 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);
// Nothing to abort: begin() failed, so no OTA handle was opened
this->cleanup_(std::move(backend), container, /*abort_backend=*/false);
this->cleanup_(std::move(backend), container);
return error_code;
}
@@ -142,7 +140,7 @@ uint8_t OtaHttpRequestComponent::do_ota_() {
} else {
ESP_LOGE(TAG, "Error reading data: %d", bufsize_or_error);
}
this->cleanup_(std::move(backend), container, /*abort_backend=*/true);
this->cleanup_(std::move(backend), container);
return OTA_CONNECTION_ERROR;
}
@@ -152,13 +150,14 @@ 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, /*abort_backend=*/true);
this->cleanup_(std::move(backend), container);
return error_code;
}
}
@@ -182,7 +181,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, /*abort_backend=*/true);
this->cleanup_(std::move(backend), container);
return ota::OTA_RESPONSE_ERROR_MD5_MISMATCH;
} else {
backend->set_update_md5(md5_receive_str);
@@ -198,7 +197,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, /*abort_backend=*/true);
this->cleanup_(std::move(backend), container);
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, bool abort_backend);
void cleanup_(ota::OTABackendPtr backend, const std::shared_ptr<HttpContainer> &container);
uint8_t do_ota_();
std::string get_url_with_auth_(const std::string &url);
bool http_get_md5_();
@@ -51,6 +51,7 @@ 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,6 +618,9 @@ 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:
+21 -17
View File
@@ -1,9 +1,6 @@
from esphome import automation
import esphome.codegen as cg
from esphome.config_helpers import (
filter_source_files_from_defines,
filter_source_files_from_platform,
)
from esphome.config_helpers import filter_source_files_from_platform
import esphome.config_validation as cv
from esphome.const import (
CONF_ESPHOME,
@@ -174,17 +171,24 @@ _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]:
return _filter_backend_source_files() + _filter_define_source_files()
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
-13
View File
@@ -66,19 +66,6 @@ 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,
+17 -69
View File
@@ -7,7 +7,7 @@
#include "esphome/core/log.h"
#include <esp_ota_ops.h>
#include <sdkconfig.h>
#include <esp_task_wdt.h>
#include <spi_flash_mmap.h>
#ifdef USE_OTA_DOWNGRADE_PROTECTION
#include <esp_app_desc.h>
@@ -60,38 +60,27 @@ OTAResponseTypes IDFOTABackend::begin(size_t image_size, ota::OTAType ota_type)
return OTA_RESPONSE_ERROR_NO_UPDATE_PARTITION;
}
// 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;
// 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;
}
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
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_);
if (err != ESP_OK) {
ESP_LOGE(TAG, "OTA begin failed (err=0x%X)", err);
ESP_LOGE(TAG, "esp_ota_begin failed (err=0x%X)", err);
esp_ota_abort(this->update_handle_);
this->update_handle_ = 0;
if (err == ESP_ERR_FLASH_OP_TIMEOUT || err == ESP_ERR_FLASH_OP_FAIL) {
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) {
return OTA_RESPONSE_ERROR_WRITING_FLASH;
} else if (err == ESP_ERR_OTA_PARTITION_CONFLICT) {
// This error appears with 1 factory and 1 ota partition
@@ -131,17 +120,6 @@ 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);
@@ -149,40 +127,14 @@ 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();
@@ -274,10 +226,6 @@ 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
+1 -18
View File
@@ -5,18 +5,8 @@
#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
@@ -64,9 +54,6 @@ 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.
@@ -75,11 +62,7 @@ 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_{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
const esp_partition_t *partition_;
char expected_bin_md5_[32];
bool md5_set_{false};
#ifdef USE_OTA_PARTITIONS
@@ -1,7 +1,6 @@
#ifdef USE_ESP32
#include "ota_backend_esp_idf.h"
#include "esphome/components/watchdog/watchdog.h"
#include "esphome/core/defines.h"
#ifdef USE_OTA_PARTITIONS
@@ -70,20 +69,12 @@ 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. Up to
// ESP_BOOTLOADER_SIZE of blocking erase; widen the WDT for its duration.
watchdog::WatchdogManager watchdog(15000);
// to avoid copying old data to the bootloader partition later
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, scaled to the image size over a 15 s floor.
// the duration, mirroring the erase budget in begin().
const uint32_t verify_budget_ms = 15000 + (incoming->size >> 10) * 10;
watchdog::WatchdogManager watchdog(verify_budget_ms);
-6
View File
@@ -5,7 +5,6 @@ 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,
@@ -1304,8 +1303,3 @@ 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,7 +1,6 @@
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,
@@ -257,8 +256,3 @@ 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"}
)
+7 -4
View File
@@ -1,6 +1,5 @@
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,
@@ -11,6 +10,7 @@ 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,6 +62,9 @@ async def to_code(config):
cg.add(var.set_time(time_id))
FILTER_SOURCE_FILES = filter_source_files_from_defines(
{"uptime_timestamp_sensor.cpp": "USE_TIME"}
)
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 []
@@ -117,6 +117,12 @@ 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,6 +618,8 @@ 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_();
@@ -929,6 +931,10 @@ 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();
@@ -1321,6 +1327,8 @@ 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");
@@ -2188,6 +2196,7 @@ 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)
@@ -2195,6 +2204,8 @@ 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();
@@ -2313,6 +2324,33 @@ 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),
+25 -24
View File
@@ -21,7 +21,6 @@
#include <span>
#include <string>
#include <type_traits>
#include <utility>
#include <vector>
#ifdef USE_LIBRETINY
@@ -262,38 +261,38 @@ class WiFiAP {
friend class WiFiScanResult;
public:
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(const std::string &ssid);
void set_ssid(const char *ssid);
void set_ssid(StringRef ssid) { this->ssid_ = CompactString(ssid.c_str(), ssid.size()); }
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_bssid(const bssid_t &bssid);
void clear_bssid();
void set_password(const std::string &password);
void set_password(const char *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) { this->eap_ = std::move(eap_auth); }
void set_eap(optional<EAPAuth> eap_auth);
#endif // USE_WIFI_WPA2_EAP
void set_channel(uint8_t channel) { this->channel_ = channel; }
void clear_channel() { this->channel_ = 0; }
void set_channel(uint8_t channel);
void clear_channel();
void set_priority(int8_t priority) { priority_ = priority; }
#ifdef USE_WIFI_MANUAL_IP
void set_manual_ip(optional<ManualIP> manual_ip) { this->manual_ip_ = manual_ip; }
void set_manual_ip(optional<ManualIP> manual_ip);
#endif
void set_hidden(bool hidden) { this->hidden_ = hidden; }
void set_hidden(bool hidden);
StringRef get_ssid() const { return this->ssid_.ref(); }
StringRef get_password() const { return this->password_.ref(); }
const bssid_t &get_bssid() const { return this->bssid_; }
bool has_bssid() const { return this->bssid_ != bssid_t{}; }
const bssid_t &get_bssid() const;
bool has_bssid() const;
#ifdef USE_WIFI_WPA2_EAP
const optional<EAPAuth> &get_eap() const { return this->eap_; }
const optional<EAPAuth> &get_eap() const;
#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 { return this->manual_ip_; }
const optional<ManualIP> &get_manual_ip() const;
#endif
bool get_hidden() const { return this->hidden_; }
bool get_hidden() const;
protected:
CompactString ssid_;
@@ -443,7 +442,6 @@ 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();
@@ -463,7 +461,7 @@ class WiFiComponent final : public Component {
void enable();
void disable();
bool is_disabled() { return this->state_ == WIFI_COMPONENT_STATE_DISABLED; }
bool is_disabled();
void start_scanning();
void check_scanning_finished();
void start_connecting(const WiFiAP &ap);
@@ -474,7 +472,7 @@ class WiFiComponent final : public Component {
void retry_connect();
void set_reboot_timeout(uint32_t reboot_timeout) { this->reboot_timeout_ = reboot_timeout; }
void set_reboot_timeout(uint32_t reboot_timeout);
bool is_connected() const { return this->connected_; }
@@ -494,7 +492,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) { this->passive_scan_ = passive; }
void set_passive_scan(bool passive);
void save_wifi_sta(const std::string &ssid, const std::string &password);
void save_wifi_sta(const char *ssid, const char *password);
@@ -508,7 +506,7 @@ class WiFiComponent final : public Component {
void dump_config() override;
void restart_adapter();
/// WIFI setup_priority.
float get_setup_priority() const override { return setup_priority::WIFI; }
float get_setup_priority() const override;
/// Reconnect WiFi if required.
void loop() override;
@@ -517,8 +515,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) { this->btm_ = btm; }
void set_rrm(bool rrm) { this->rrm_ = rrm; }
void set_btm(bool btm);
void set_rrm(bool rrm);
#endif
network::IPAddress get_dns_address(int num);
@@ -552,6 +550,9 @@ 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,6 +944,16 @@ 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,6 +1237,18 @@ 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,6 +762,7 @@ 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,6 +265,7 @@ 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,31 +151,6 @@ 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.
-3
View File
@@ -43,9 +43,7 @@
#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
@@ -283,7 +281,6 @@
// 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,6 +80,24 @@ 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
@@ -88,6 +106,10 @@ 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
@@ -107,6 +129,24 @@ 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,14 +109,60 @@ 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,6 +723,23 @@ 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,6 +1646,15 @@ 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,6 +60,16 @@ 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,6 +68,11 @@ 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
+19 -6
View File
@@ -35,7 +35,7 @@ _SIZE_SUFFIXES = {"K": 1024, "M": 1024 * 1024}
def _parse_size(token: str) -> int:
token = token.strip()
if not token:
return 0
raise ValueError("blank partition size cell")
if token.startswith(("0x", "0X")):
return int(token, 16)
suffix = token[-1].upper()
@@ -80,11 +80,15 @@ def _format_bar(used: int, total: int) -> str:
def print_summary(size_json: Path, partitions_csv: Path | None) -> None:
"""Print PlatformIO-shaped RAM and Flash one-liners.
"""Print PlatformIO-shaped RAM and Flash one-liners; never fails the build."""
try:
_print_summary(size_json, partitions_csv)
except Exception as e: # noqa: BLE001 # pylint: disable=broad-exception-caught
# Backstop for shapes the named guards below miss
_LOGGER.debug("Skipping size summary: %s", e)
Failures are non-fatal: the build has already succeeded, we just couldn't
summarize. Logs the cause at debug level.
"""
def _print_summary(size_json: Path, partitions_csv: Path | None) -> None:
if not size_json.is_file():
_LOGGER.debug("Skipping size summary: %s not found", size_json)
return
@@ -94,6 +98,11 @@ def print_summary(size_json: Path, partitions_csv: Path | None) -> None:
_LOGGER.debug("Skipping size summary: %s", e)
return
if not isinstance(data, dict):
# Non-object JSON has no .get
_LOGGER.debug("Skipping size summary: unexpected shape in %s", size_json)
return
memory_types = data.get("memory_types", {})
ram_region = memory_types.get("DRAM") or memory_types.get("DIRAM") or {}
ram_used = ram_region.get("used")
@@ -106,7 +115,11 @@ def print_summary(size_json: Path, partitions_csv: Path | None) -> None:
return
try:
app_size = _find_app_partition_size(partitions_csv)
except ValueError as e:
except (ValueError, OSError) as e:
_LOGGER.debug("Skipping Flash summary: %s", e)
return
if app_size <= 0:
# Skipping also fails CI's Flash extraction, the right outcome here
_LOGGER.debug("Skipping Flash summary: app partition size is 0")
return
print(f"Flash: {_format_bar(image_size, app_size)}")
+1 -12
View File
@@ -552,18 +552,7 @@ def write_file_if_changed(path: Path, text: str) -> bool:
"""
src_content = None
if path.is_file():
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
src_content = read_file(path)
if src_content == text:
return False
write_file(path, text)
@@ -136,19 +136,3 @@ 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"
-41
View File
@@ -1,41 +0,0 @@
// 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,7 +29,6 @@ void setup() {
auto *ota = new esphome::ESPHomeOTAComponent(); // NOLINT
ota->set_port(8266);
App.register_component_(ota);
App.setup();
}
-24
View File
@@ -6,7 +6,6 @@ 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,
@@ -19,7 +18,6 @@ from esphome.const import (
KEY_TARGET_PLATFORM,
PlatformFramework,
)
from esphome.core import Define
def test_filter_source_files_from_platform_esp32() -> None:
@@ -150,25 +148,3 @@ 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,31 +253,6 @@ 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"
+96
View File
@@ -4,6 +4,7 @@ from __future__ import annotations
import json
from pathlib import Path
from unittest.mock import patch
import pytest
@@ -126,3 +127,98 @@ def test_print_summary_handles_no_memory_types(
size_json = _write_size_json(tmp_path, {"image_size": 0})
print_summary(size_json, partitions_csv=None)
assert capsys.readouterr().out == ""
def test_print_summary_non_dict_json_is_skipped(
tmp_path: Path, capsys: pytest.CaptureFixture[str]
) -> None:
"""Valid JSON that is not an object must not raise past a linked build."""
size_json = tmp_path / "size.json"
size_json.write_text("[]")
print_summary(size_json, tmp_path / "partitions.csv")
assert capsys.readouterr().out == ""
def test_print_summary_unreadable_partitions_is_skipped(
tmp_path: Path, capsys: pytest.CaptureFixture[str]
) -> None:
"""An OSError reading the partition table skips the summary, not the build."""
size_json = tmp_path / "size.json"
size_json.write_text(
'{"memory_types": {"DRAM": {"used": 1, "size": 2}}, "image_size": 100}'
)
partitions = tmp_path / "partitions.csv"
partitions.write_text("app0, app, ota_0, 0x10000, 1M,\n")
real_read_text = Path.read_text
def fail_partitions_read(self: Path, *args: object, **kwargs: object) -> str:
if self == partitions:
raise OSError("permission denied")
return real_read_text(self, *args, **kwargs)
with patch.object(Path, "read_text", fail_partitions_read):
print_summary(size_json, partitions)
out = capsys.readouterr().out
assert "RAM:" in out and "Flash:" not in out
def test_print_summary_zero_app_partition_is_skipped(
tmp_path: Path, capsys: pytest.CaptureFixture[str]
) -> None:
"""A 0-size app partition drops the Flash bar instead of rendering 0%."""
size_json = tmp_path / "size.json"
size_json.write_text(
'{"memory_types": {"DRAM": {"used": 1, "size": 2}}, "image_size": 100}'
)
partitions = tmp_path / "partitions.csv"
partitions.write_text("app0, app, ota_0, 0x10000, 0,\n")
print_summary(size_json, partitions)
out = capsys.readouterr().out
assert "RAM:" in out and "Flash:" not in out
def test_print_summary_happy_path_prints_both_bars(
tmp_path: Path, capsys: pytest.CaptureFixture[str]
) -> None:
"""A well-formed size report and partition table print both bars."""
size_json = tmp_path / "size.json"
size_json.write_text(
'{"memory_types": {"DRAM": {"used": 1000, "size": 2000}}, "image_size": 100000}'
)
partitions = tmp_path / "partitions.csv"
partitions.write_text("app0, app, ota_0, 0x10000, 0x180000,\n")
print_summary(size_json, partitions)
out = capsys.readouterr().out
assert "RAM:" in out and "Flash:" in out
@pytest.mark.parametrize(
"payload",
[
{"memory_types": []},
{"memory_types": {"DRAM": 5}},
{"memory_types": {"DRAM": {"used": "x", "size": "y"}}, "image_size": 1},
],
)
def test_print_summary_nested_bad_shapes_never_raise(
tmp_path: Path, capsys: pytest.CaptureFixture[str], payload: dict
) -> None:
"""The blanket guard keeps unexpected nested shapes from raising."""
size_json = _write_size_json(tmp_path, payload)
print_summary(size_json, None)
assert "Traceback" not in capsys.readouterr().err
def test_print_summary_blank_size_cell_names_the_row(
tmp_path: Path, capsys: pytest.CaptureFixture[str]
) -> None:
"""A blank size cell raises ValueError instead of parsing to 0."""
size_json = _write_size_json(
tmp_path,
{"memory_types": {"DRAM": {"used": 1, "size": 2}}, "image_size": 100},
)
partitions = tmp_path / "partitions.csv"
partitions.write_text("app0, app, ota_0, 0x10000, ,\n")
print_summary(size_json, partitions)
out = capsys.readouterr().out
assert "RAM:" in out and "Flash:" not in out