mirror of
https://github.com/esphome/esphome.git
synced 2026-08-23 22:56:19 +00:00
Compare commits
15
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8b3cebaf64 | ||
|
|
cf31c08a5c | ||
|
|
e7574a574b | ||
|
|
33484108a9 | ||
|
|
e697a40fda | ||
|
|
d1f065671e | ||
|
|
02da5c6484 | ||
|
|
b2440cb655 | ||
|
|
160d8b8f0c | ||
|
|
8899713ef9 | ||
|
|
f3cdefce21 | ||
|
|
b115813fbe | ||
|
|
ab45ab316a | ||
|
|
5b3a6c05bf | ||
|
|
cd53681787 |
@@ -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
|
||||
|
||||
@@ -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"}
|
||||
)
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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"}
|
||||
)
|
||||
|
||||
@@ -5,7 +5,10 @@ import re
|
||||
from esphome import automation, pins
|
||||
import esphome.codegen as cg
|
||||
from esphome.components.const import CONF_DATA_BITS, CONF_PARITY, CONF_STOP_BITS
|
||||
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_AFTER,
|
||||
@@ -521,7 +524,7 @@ async def final_step():
|
||||
cg.add_define("USE_UART_WAKE_LOOP_ON_RX")
|
||||
|
||||
|
||||
FILTER_SOURCE_FILES = filter_source_files_from_platform(
|
||||
_platform_filter = filter_source_files_from_platform(
|
||||
{
|
||||
"uart_component_esp_idf.cpp": {
|
||||
PlatformFramework.ESP32_IDF,
|
||||
@@ -537,3 +540,13 @@ FILTER_SOURCE_FILES = filter_source_files_from_platform(
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
# uart_debugger.cpp is fully #ifdef'd on USE_UART_DEBUGGER, set only when a
|
||||
# debug block is configured.
|
||||
_define_filter = filter_source_files_from_defines(
|
||||
{"uart_debugger.cpp": "USE_UART_DEBUGGER"}
|
||||
)
|
||||
|
||||
|
||||
def FILTER_SOURCE_FILES() -> list[str]:
|
||||
return _platform_filter() + _define_filter()
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -281,6 +283,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
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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});
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
+30
-108
@@ -26,7 +26,6 @@ from __future__ import annotations
|
||||
import csv
|
||||
import json
|
||||
import logging
|
||||
import math
|
||||
from pathlib import Path
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -35,6 +34,8 @@ _SIZE_SUFFIXES = {"K": 1024, "M": 1024 * 1024}
|
||||
|
||||
def _parse_size(token: str) -> int:
|
||||
token = token.strip()
|
||||
if not token:
|
||||
return 0
|
||||
if token.startswith(("0x", "0X")):
|
||||
return int(token, 16)
|
||||
suffix = token[-1].upper()
|
||||
@@ -43,19 +44,16 @@ def _parse_size(token: str) -> int:
|
||||
return int(token)
|
||||
|
||||
|
||||
def _find_app_partition_size(partitions_csv: Path) -> int | None:
|
||||
"""The firmware's app partition size; None when there is nothing to find.
|
||||
def _find_app_partition_size(partitions_csv: Path) -> int:
|
||||
"""Return the size of the firmware's app partition.
|
||||
|
||||
Mirrors PlatformIO's ``platform-espressif32/builder/main.py::
|
||||
_update_max_upload_size``: take the first ``app``-type partition
|
||||
whose subtype is ``factory`` or ``ota_0``. Order matters because
|
||||
layouts like Adafruit's ``partitions-4MB-tinyuf2.csv`` repurpose
|
||||
``factory`` for a UF2 bootloader before the real OTA slot, so a
|
||||
naive "prefer factory" rule would pick the wrong row. No qualifying
|
||||
row is legitimate absence (None); a build cannot succeed with a
|
||||
missing or malformed table (gen_esp32part consumes it first), so
|
||||
those states belong to the backstop -- the missing-file raise just
|
||||
names that one cleanly.
|
||||
naive "prefer factory" rule would pick the wrong row. Raises
|
||||
``ValueError`` if no qualifying partition is present.
|
||||
"""
|
||||
if not partitions_csv.is_file():
|
||||
raise ValueError(f"partitions.csv not found at {partitions_csv}")
|
||||
@@ -66,7 +64,7 @@ def _find_app_partition_size(partitions_csv: Path) -> int | None:
|
||||
ptype, psubtype, psize = cells[1], cells[2], cells[4]
|
||||
if ptype in ("app", "0") and psubtype in ("factory", "ota_0"):
|
||||
return _parse_size(psize)
|
||||
return None
|
||||
raise ValueError(f"No app+factory or app+ota_0 partition in {partitions_csv}")
|
||||
|
||||
|
||||
def _format_bar(used: int, total: int) -> str:
|
||||
@@ -82,109 +80,33 @@ 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; 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; warning so a
|
||||
# regression here cannot go missing indefinitely
|
||||
_LOGGER.warning(
|
||||
"Skipping size summary for %s: %s: %s",
|
||||
size_json,
|
||||
type(e).__name__,
|
||||
e,
|
||||
exc_info=True,
|
||||
)
|
||||
"""Print PlatformIO-shaped RAM and Flash one-liners.
|
||||
|
||||
|
||||
def _print_summary(size_json: Path, partitions_csv: Path | None) -> None:
|
||||
# The build's own POST_BUILD step writes this file; its absence or an
|
||||
# unexpected shape is a regression signal, so these skips warn.
|
||||
# FileNotFoundError lands in the OSError arm with the path in its text.
|
||||
Failures are non-fatal: the build has already succeeded, we just couldn't
|
||||
summarize. Logs the cause at debug level.
|
||||
"""
|
||||
if not size_json.is_file():
|
||||
_LOGGER.debug("Skipping size summary: %s not found", size_json)
|
||||
return
|
||||
try:
|
||||
data = json.loads(size_json.read_text(encoding="utf-8"))
|
||||
except (OSError, ValueError) as e:
|
||||
# ValueError covers JSONDecodeError and a non-UTF-8 (truncated) file
|
||||
_LOGGER.warning("Skipping size summary: cannot read %s: %s", size_json, e)
|
||||
return
|
||||
if not isinstance(data, dict):
|
||||
# Non-object JSON has no .get
|
||||
_LOGGER.warning("Skipping size summary: unexpected shape in %s", size_json)
|
||||
except (OSError, json.JSONDecodeError) as e:
|
||||
_LOGGER.debug("Skipping size summary: %s", e)
|
||||
return
|
||||
|
||||
if (ram := _ram_line(data, size_json)) is not None:
|
||||
print(ram)
|
||||
if (flash := _flash_line(data, size_json, partitions_csv)) is not None:
|
||||
print(flash)
|
||||
memory_types = data.get("memory_types", {})
|
||||
ram_region = memory_types.get("DRAM") or memory_types.get("DIRAM") or {}
|
||||
ram_used = ram_region.get("used")
|
||||
ram_total = ram_region.get("size")
|
||||
if ram_total and ram_used is not None:
|
||||
print(f"RAM: {_format_bar(ram_used, ram_total)}")
|
||||
|
||||
|
||||
def _dict_get(mapping: object, key: str) -> object:
|
||||
"""dict.get that reads None from any non-dict."""
|
||||
return mapping.get(key) if isinstance(mapping, dict) else None
|
||||
|
||||
|
||||
def _present_but_not_dict(value: object) -> bool:
|
||||
return value is not None and not isinstance(value, dict)
|
||||
|
||||
|
||||
def _is_number(value: object) -> bool:
|
||||
# bool subclasses int; NaN/Infinity are valid JSON for json.loads
|
||||
return (
|
||||
isinstance(value, (int, float))
|
||||
and not isinstance(value, bool)
|
||||
and math.isfinite(value)
|
||||
)
|
||||
|
||||
|
||||
def _ram_line(data: dict, size_json: Path) -> str | None:
|
||||
"""The formatted RAM line, or None (already logged) to skip it."""
|
||||
memory_types = data.get("memory_types")
|
||||
ram_region = None
|
||||
if isinstance(memory_types, dict):
|
||||
# Key presence, not truthiness: a falsy DRAM value is corrupt, not
|
||||
# absent, and must not fall through to DIRAM
|
||||
for key in ("DRAM", "DIRAM"):
|
||||
if key in memory_types:
|
||||
ram_region = memory_types[key]
|
||||
break
|
||||
used = _dict_get(ram_region, "used")
|
||||
total = _dict_get(ram_region, "size")
|
||||
if _is_number(used) and _is_number(total) and total > 0:
|
||||
return f"RAM: {_format_bar(int(used), int(total))}"
|
||||
malformed = (
|
||||
_present_but_not_dict(memory_types)
|
||||
or _present_but_not_dict(ram_region)
|
||||
or any(v is not None and not _is_number(v) for v in (used, total))
|
||||
)
|
||||
if malformed:
|
||||
# A structurally corrupt report, not a variant without the region
|
||||
_LOGGER.warning("Skipping RAM summary: malformed memory_types in %s", size_json)
|
||||
else:
|
||||
# A variant may name its RAM region differently; healthy builds
|
||||
# must not warn
|
||||
_LOGGER.debug(
|
||||
"Skipping RAM summary: no usable DRAM/DIRAM region in %s", size_json
|
||||
)
|
||||
return None
|
||||
|
||||
|
||||
def _flash_line(data: dict, size_json: Path, partitions_csv: Path | None) -> str | None:
|
||||
"""The formatted Flash line, or None (already logged) to skip it.
|
||||
|
||||
Owns both sides of the bar, so nothing after a print can raise: the
|
||||
blanket guard is left for genuinely unforeseen shapes.
|
||||
"""
|
||||
image_size = data.get("image_size")
|
||||
if not _is_number(image_size):
|
||||
_LOGGER.warning("Skipping Flash summary: no usable image_size in %s", size_json)
|
||||
return None
|
||||
if partitions_csv is None:
|
||||
_LOGGER.debug("Skipping Flash summary: no partition table given")
|
||||
return None
|
||||
app_size = _find_app_partition_size(partitions_csv)
|
||||
if not app_size:
|
||||
# No qualifying row (a zero-size row has nothing to report either):
|
||||
# legitimate for non-app layouts
|
||||
_LOGGER.debug("Skipping Flash summary: no app partition in %s", partitions_csv)
|
||||
return None
|
||||
return f"Flash: {_format_bar(int(image_size), app_size)}"
|
||||
if image_size is None or partitions_csv is None:
|
||||
return
|
||||
try:
|
||||
app_size = _find_app_partition_size(partitions_csv)
|
||||
except ValueError as e:
|
||||
_LOGGER.debug("Skipping Flash summary: %s", e)
|
||||
return
|
||||
print(f"Flash: {_format_bar(image_size, app_size)}")
|
||||
|
||||
@@ -456,8 +456,8 @@ def run_compile(config, verbose: bool) -> int:
|
||||
rc = run_idf_py(*args, jobs=config[CONF_ESPHOME].get(CONF_COMPILE_PROCESS_LIMIT))
|
||||
if rc == 0:
|
||||
size_json = CORE.relative_build_path("build", "esp_idf_size.json")
|
||||
# size_summary owns the missing-table policy
|
||||
print_summary(size_json, CORE.relative_build_path("partitions.csv"))
|
||||
partitions = CORE.relative_build_path("partitions.csv")
|
||||
print_summary(size_json, partitions if partitions.is_file() else None)
|
||||
return rc
|
||||
|
||||
|
||||
|
||||
+12
-1
@@ -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)
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
@@ -29,6 +29,7 @@ void setup() {
|
||||
|
||||
auto *ota = new esphome::ESPHomeOTAComponent(); // NOLINT
|
||||
ota->set_port(8266);
|
||||
App.register_component_(ota);
|
||||
|
||||
App.setup();
|
||||
}
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -3,9 +3,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
|
||||
@@ -71,18 +69,6 @@ def _s3_size_data() -> dict:
|
||||
}
|
||||
|
||||
|
||||
def _dram_size_data(image_size: int = 100) -> dict:
|
||||
return {"memory_types": {"DRAM": {"used": 1, "size": 2}}, "image_size": image_size}
|
||||
|
||||
|
||||
def _write_partitions(
|
||||
tmp_path: Path, size: str, ptype: str = "app", subtype: str = "ota_0"
|
||||
) -> Path:
|
||||
partitions = tmp_path / "partitions.csv"
|
||||
partitions.write_text(f"app0, {ptype}, {subtype}, 0x10000, {size},\n")
|
||||
return partitions
|
||||
|
||||
|
||||
def test_print_summary_esp32_uses_dram(
|
||||
tmp_path: Path, capsys: pytest.CaptureFixture[str]
|
||||
) -> None:
|
||||
@@ -126,15 +112,11 @@ def test_print_summary_skips_when_diram_total_collapses(
|
||||
|
||||
|
||||
def test_print_summary_handles_missing_json(
|
||||
tmp_path: Path,
|
||||
capsys: pytest.CaptureFixture[str],
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
tmp_path: Path, capsys: pytest.CaptureFixture[str]
|
||||
) -> None:
|
||||
"""Missing size json is non-fatal and prints nothing."""
|
||||
print_summary(tmp_path / "does_not_exist.json", partitions_csv=None)
|
||||
assert capsys.readouterr().out == ""
|
||||
assert "cannot read" in caplog.text
|
||||
assert "Skipping size summary for" not in caplog.text
|
||||
|
||||
|
||||
def test_print_summary_handles_no_memory_types(
|
||||
@@ -144,184 +126,3 @@ 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], caplog: pytest.LogCaptureFixture
|
||||
) -> None:
|
||||
"""An OSError reading the partition table skips the summary, not the build."""
|
||||
size_json = _write_size_json(tmp_path, _dram_size_data())
|
||||
partitions = _write_partitions(tmp_path, "1M")
|
||||
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)
|
||||
# An impossible post-build state is the backstop's business
|
||||
out = capsys.readouterr().out
|
||||
assert "RAM:" in out and "Flash:" not in out
|
||||
assert "Skipping size summary for" in caplog.text
|
||||
|
||||
|
||||
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 = _write_partitions(tmp_path, "0x180000")
|
||||
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},
|
||||
{"memory_types": {"DRAM": []}},
|
||||
{"memory_types": {"DRAM": {"used": True, "size": True}}},
|
||||
],
|
||||
)
|
||||
def test_print_summary_nested_bad_shapes_never_raise(
|
||||
tmp_path: Path,
|
||||
capsys: pytest.CaptureFixture[str],
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
payload: dict,
|
||||
) -> None:
|
||||
"""Corrupt nested shapes hit the named malformed guard, not the blanket."""
|
||||
size_json = _write_size_json(tmp_path, payload)
|
||||
print_summary(size_json, None)
|
||||
# No half-formed bar for CI to scrape; every payload fails before printing
|
||||
assert capsys.readouterr().out == ""
|
||||
assert "malformed memory_types" in caplog.text
|
||||
assert "Skipping size summary for" not in caplog.text
|
||||
|
||||
|
||||
def test_print_summary_absent_region_stays_quiet(
|
||||
tmp_path: Path,
|
||||
capsys: pytest.CaptureFixture[str],
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
"""A well-shaped report without DRAM/DIRAM is a variant difference, not
|
||||
a broken artifact: debug, never a per-build warning."""
|
||||
size_json = _write_size_json(tmp_path, {"memory_types": {}, "image_size": 1})
|
||||
with caplog.at_level(logging.DEBUG, logger="esphome.espidf.size_summary"):
|
||||
print_summary(size_json, None)
|
||||
assert "RAM:" not in capsys.readouterr().out
|
||||
assert "no usable DRAM/DIRAM region" in caplog.text
|
||||
assert not [
|
||||
r for r in caplog.records if r.levelno >= logging.WARNING and "RAM" in r.message
|
||||
]
|
||||
|
||||
|
||||
def test_print_summary_non_numeric_image_size_warns_by_name(
|
||||
tmp_path: Path,
|
||||
capsys: pytest.CaptureFixture[str],
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
"""A non-numeric image_size hits the named guard, not the blanket."""
|
||||
size_json = _write_size_json(
|
||||
tmp_path,
|
||||
{"memory_types": {"DRAM": {"used": 1, "size": 2}}, "image_size": "x"},
|
||||
)
|
||||
print_summary(size_json, _write_partitions(tmp_path, "0x100000"))
|
||||
assert "Flash:" not in capsys.readouterr().out
|
||||
assert "no usable image_size" in caplog.text
|
||||
assert "Skipping size summary for" not in caplog.text
|
||||
|
||||
|
||||
def test_print_summary_blanket_guard_catches_the_rest(
|
||||
tmp_path: Path,
|
||||
capsys: pytest.CaptureFixture[str],
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
"""A genuinely unforeseen failure warns via the blanket backstop and
|
||||
never raises past a linked build."""
|
||||
size_json = _write_size_json(tmp_path, _dram_size_data())
|
||||
with patch(
|
||||
"esphome.espidf.size_summary._flash_line",
|
||||
side_effect=RuntimeError("unforeseen"),
|
||||
):
|
||||
print_summary(size_json, None)
|
||||
assert "Skipping size summary for" in caplog.text
|
||||
|
||||
|
||||
@pytest.mark.parametrize("cell", ["1M", "1048576"], ids=["suffixed", "decimal"])
|
||||
def test_print_summary_suffixed_size_cell(
|
||||
tmp_path: Path, capsys: pytest.CaptureFixture[str], cell: str
|
||||
) -> None:
|
||||
"""K/M suffixes and plain decimals parse like PlatformIO's rule."""
|
||||
size_json = _write_size_json(tmp_path, _dram_size_data())
|
||||
partitions = tmp_path / "partitions.csv"
|
||||
partitions.write_text(
|
||||
f"# comment row\nshort,row\napp0, app, ota_0, 0x10000, {cell},\n"
|
||||
)
|
||||
print_summary(size_json, partitions)
|
||||
assert "from 1048576 bytes" in capsys.readouterr().out
|
||||
|
||||
|
||||
def test_print_summary_missing_or_appless_partitions_stay_quiet(
|
||||
tmp_path: Path,
|
||||
capsys: pytest.CaptureFixture[str],
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
"""A table without a qualifying app row is a legitimate layout: the
|
||||
Flash line drops at debug, never at warning."""
|
||||
size_json = _write_size_json(tmp_path, _dram_size_data())
|
||||
partitions = _write_partitions(tmp_path, "0x1000", ptype="data", subtype="spiffs")
|
||||
with caplog.at_level(logging.DEBUG, logger="esphome.espidf.size_summary"):
|
||||
print_summary(size_json, partitions)
|
||||
out = capsys.readouterr().out
|
||||
assert "Flash:" not in out
|
||||
# Quiet means debug-logged, not unlogged
|
||||
assert "Skipping Flash summary: no app partition" in caplog.text
|
||||
assert not [r for r in caplog.records if r.levelno >= logging.WARNING]
|
||||
|
||||
|
||||
def test_print_summary_corrupt_size_json_warns(
|
||||
tmp_path: Path,
|
||||
capsys: pytest.CaptureFixture[str],
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
"""The build's own size report failing to parse is a regression signal."""
|
||||
size_json = tmp_path / "size.json"
|
||||
size_json.write_text("not json {{{")
|
||||
print_summary(size_json, None)
|
||||
size_json.write_bytes(b"\xff\xfe\x00")
|
||||
print_summary(size_json, None)
|
||||
assert capsys.readouterr().out == ""
|
||||
# The named arm, not the blanket, for both damage classes
|
||||
assert caplog.text.count("cannot read") == 2
|
||||
assert "Skipping size summary for" not in caplog.text
|
||||
|
||||
|
||||
def test_print_summary_missing_partitions_named_in_backstop(
|
||||
tmp_path: Path,
|
||||
capsys: pytest.CaptureFixture[str],
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
"""A vanished table is an impossible post-build state; the backstop
|
||||
reports it by name instead of a bare FileNotFoundError."""
|
||||
size_json = _write_size_json(tmp_path, _dram_size_data())
|
||||
print_summary(size_json, tmp_path / "nope.csv")
|
||||
assert "Flash:" not in capsys.readouterr().out
|
||||
assert "partitions.csv not found" in caplog.text
|
||||
|
||||
Reference in New Issue
Block a user