Compare commits

..

15 Commits

Author SHA1 Message Date
Jonathan Swoboda
77df3933db Merge pull request #13309 from esphome/bump-2026.1.0b3
2026.1.0b3
2026-01-16 23:45:26 -05:00
Jonathan Swoboda
19514ccdf4 Bump version to 2026.1.0b3 2026-01-16 23:05:59 -05:00
Mike Ford
2947642ca5 [http_request] Unable to handle chunked responses (#7884)
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-01-16 23:05:59 -05:00
Stuart Parmenter
60e333db08 [hub75] Bump esp-hub75 version to 0.3.0 (#13243) 2026-01-16 23:05:59 -05:00
J. Nick Koston
d8463f4813 [hmac_sha256] Replace unsafe sprintf with format_hex_to (#13290) 2026-01-16 23:05:59 -05:00
mrtoy-me
e1800d2fe2 [ntc, resistance] change log level to verbose (#13268) 2026-01-16 23:05:59 -05:00
J. Nick Koston
50aa4b1992 [esp32_ble_client] Reduce GATT data event logging to prevent firmware update failures (#13252) 2026-01-16 23:05:59 -05:00
J. Nick Koston
edb303e495 [api] Fix clock conflicts when multiple clients connected to homeassistant time (#13253) 2026-01-16 23:05:59 -05:00
J. Nick Koston
973fc4c5dc [dallas_temp] Use const char* for set_timeout to fix deprecation warning and heap churn (#13250) 2026-01-16 23:05:59 -05:00
J. Nick Koston
f88e8fc43b [sprinkler] Fix scheduler deprecation warnings and heap churn with FixedVector (#13251) 2026-01-16 23:05:59 -05:00
Jonathan Swoboda
c4c31a2e8e Merge branch 'release' into beta 2026-01-16 22:49:38 -05:00
Jonathan Swoboda
e6790f0042 Merge pull request #13308 from esphome/bump-2025.12.7
2025.12.7
2026-01-16 22:49:26 -05:00
Jonathan Swoboda
ec7f72e280 Bump version to 2025.12.7 2026-01-16 22:24:05 -05:00
J. Nick Koston
6f29dbd6f1 [api] Use subtraction for protobuf bounds checking (#13306) 2026-01-16 22:24:05 -05:00
Kevin Ahrendt
9caf78aa7e [i2s_audio] Bugfix: Buffer overflow in software volume control (#13190) 2026-01-16 22:24:05 -05:00
44 changed files with 188 additions and 574 deletions

View File

@@ -48,7 +48,7 @@ PROJECT_NAME = ESPHome
# could be handy for archiving the generated documentation or if some version
# control system is used.
PROJECT_NUMBER = 2026.2.0-dev
PROJECT_NUMBER = 2026.1.0b3
# Using the PROJECT_BRIEF tag one can provide an optional one line description
# for a project that appears at the top of each page and should give viewer a

View File

@@ -22,7 +22,7 @@ from .helpers import (
map_section_name,
parse_symbol_line,
)
from .toolchain import find_tool, resolve_tool_path, run_tool
from .toolchain import find_tool, run_tool
if TYPE_CHECKING:
from esphome.platformio_api import IDEData
@@ -132,12 +132,6 @@ class MemoryAnalyzer:
readelf_path = readelf_path or idedata.readelf_path
_LOGGER.debug("Using toolchain paths from PlatformIO idedata")
# Validate paths exist, fall back to find_tool if they don't
# This handles cases like Zephyr where cc_path doesn't include full path
# and the toolchain prefix may differ (e.g., arm-zephyr-eabi- vs arm-none-eabi-)
objdump_path = resolve_tool_path("objdump", objdump_path, objdump_path)
readelf_path = resolve_tool_path("readelf", readelf_path, objdump_path)
self.objdump_path = objdump_path or "objdump"
self.readelf_path = readelf_path or "readelf"
self.external_components = external_components or set()

View File

@@ -9,61 +9,11 @@ ESPHOME_COMPONENT_PATTERN = re.compile(r"esphome::([a-zA-Z0-9_]+)::")
# Maps standard section names to their various platform-specific variants
# Note: Order matters! More specific patterns (.bss) must come before general ones (.dram)
# because ESP-IDF uses names like ".dram0.bss" which would match ".dram" otherwise
#
# Platform-specific sections:
# - ESP8266/ESP32: .iram*, .dram*
# - LibreTiny RTL87xx: .xip.code_* (flash), .ram.code_* (RAM)
# - LibreTiny BK7231: .itcm.code (fast RAM), .vectors (interrupt vectors)
# - LibreTiny LN882X: .flash_text, .flash_copy* (flash code)
# - Zephyr/nRF52: text, rodata, datas, bss (no leading dots)
SECTION_MAPPING = {
".text": frozenset(
[
".text",
".iram",
# LibreTiny RTL87xx XIP (eXecute In Place) flash code
".xip.code",
# LibreTiny RTL87xx RAM code
".ram.code_text",
# LibreTiny BK7231 fast RAM code and vectors
".itcm.code",
".vectors",
# LibreTiny LN882X flash code
".flash_text",
".flash_copy",
# Zephyr/nRF52 sections (no leading dots)
"text",
"rom_start",
]
),
".rodata": frozenset(
[
".rodata",
# LibreTiny RTL87xx read-only data in RAM
".ram.code_rodata",
# Zephyr/nRF52 sections (no leading dots)
"rodata",
]
),
# .bss patterns - must be before .data to catch ".dram0.bss"
".bss": frozenset(
[
".bss",
# LibreTiny LN882X BSS
".bss_ram",
# Zephyr/nRF52 sections (no leading dots)
"bss",
"noinit",
]
),
".data": frozenset(
[
".data",
".dram",
# Zephyr/nRF52 sections (no leading dots)
"datas",
]
),
".text": frozenset([".text", ".iram"]),
".rodata": frozenset([".rodata"]),
".bss": frozenset([".bss"]), # Must be before .data to catch ".dram0.bss"
".data": frozenset([".data", ".dram"]),
}
# Section to ComponentMemory attribute mapping

View File

@@ -94,13 +94,13 @@ def parse_symbol_line(line: str) -> tuple[str, str, int, str] | None:
return None
# Find section, size, and name
# Try each part as a potential section name
for i, part in enumerate(parts):
# Skip parts that are clearly flags, addresses, or other metadata
# Sections start with '.' (standard ELF) or are known section names (Zephyr)
if not part.startswith("."):
continue
section = map_section_name(part)
if not section:
continue
break
# Need at least size field after section
if i + 1 >= len(parts):

View File

@@ -3,7 +3,6 @@
from __future__ import annotations
import logging
import os
from pathlib import Path
import subprocess
from typing import TYPE_CHECKING
@@ -18,82 +17,10 @@ TOOLCHAIN_PREFIXES = [
"xtensa-lx106-elf-", # ESP8266
"xtensa-esp32-elf-", # ESP32
"xtensa-esp-elf-", # ESP32 (newer IDF)
"arm-zephyr-eabi-", # nRF52/Zephyr SDK
"arm-none-eabi-", # Generic ARM (RP2040, etc.)
"", # System default (no prefix)
]
def _find_in_platformio_packages(tool_name: str) -> str | None:
"""Search for a tool in PlatformIO package directories.
This handles cases like Zephyr SDK where tools are installed in nested
directories that aren't in PATH.
Args:
tool_name: Name of the tool (e.g., "readelf", "objdump")
Returns:
Full path to the tool or None if not found
"""
# Get PlatformIO packages directory
platformio_home = Path(os.path.expanduser("~/.platformio/packages"))
if not platformio_home.exists():
return None
# Search patterns for toolchains that might contain the tool
# Order matters - more specific patterns first
search_patterns = [
# Zephyr SDK deeply nested structure (4 levels)
# e.g., toolchain-gccarmnoneeabi/zephyr-sdk-0.17.4/arm-zephyr-eabi/bin/arm-zephyr-eabi-objdump
f"toolchain-*/*/*/bin/*-{tool_name}",
# Zephyr SDK nested structure (3 levels)
f"toolchain-*/*/bin/*-{tool_name}",
f"toolchain-*/bin/*-{tool_name}",
# Standard PlatformIO toolchain structure
f"toolchain-*/bin/*{tool_name}",
]
for pattern in search_patterns:
matches = list(platformio_home.glob(pattern))
if matches:
# Sort to get consistent results, prefer arm-zephyr-eabi over arm-none-eabi
matches.sort(key=lambda p: ("zephyr" not in str(p), str(p)))
tool_path = str(matches[0])
_LOGGER.debug("Found %s in PlatformIO packages: %s", tool_name, tool_path)
return tool_path
return None
def resolve_tool_path(
tool_name: str,
derived_path: str | None,
objdump_path: str | None = None,
) -> str | None:
"""Resolve a tool path, falling back to find_tool if derived path doesn't exist.
Args:
tool_name: Name of the tool (e.g., "objdump", "readelf")
derived_path: Path derived from idedata (may not exist for some platforms)
objdump_path: Path to objdump binary to derive other tool paths from
Returns:
Resolved path to the tool, or the original derived_path if it exists
"""
if derived_path and not Path(derived_path).exists():
found = find_tool(tool_name, objdump_path)
if found:
_LOGGER.debug(
"Derived %s path %s not found, using %s",
tool_name,
derived_path,
found,
)
return found
return derived_path
def find_tool(
tool_name: str,
objdump_path: str | None = None,
@@ -101,8 +28,7 @@ def find_tool(
"""Find a toolchain tool by name.
First tries to derive the tool path from objdump_path (if provided),
then searches PlatformIO package directories (for cross-compile toolchains),
and finally falls back to searching for platform-specific tools in PATH.
then falls back to searching for platform-specific tools.
Args:
tool_name: Name of the tool (e.g., "objdump", "nm", "c++filt")
@@ -121,13 +47,7 @@ def find_tool(
_LOGGER.debug("Found %s at: %s", tool_name, potential_path)
return potential_path
# Search in PlatformIO packages directory first (handles Zephyr SDK, etc.)
# This must come before PATH search because system tools (e.g., /usr/bin/objdump)
# are for the host architecture, not the target (ARM, Xtensa, etc.)
if found := _find_in_platformio_packages(tool_name):
return found
# Try platform-specific tools in PATH (fallback for when tools are installed globally)
# Try platform-specific tools
for prefix in TOOLCHAIN_PREFIXES:
cmd = f"{prefix}{tool_name}"
try:

View File

@@ -48,14 +48,14 @@ uint32_t ProtoDecodableMessage::count_repeated_field(const uint8_t *buffer, size
}
uint32_t field_length = res->as_uint32();
ptr += consumed;
if (ptr + field_length > end) {
if (field_length > static_cast<size_t>(end - ptr)) {
return count; // Out of bounds
}
ptr += field_length;
break;
}
case WIRE_TYPE_FIXED32: { // 32-bit - skip 4 bytes
if (ptr + 4 > end) {
if (end - ptr < 4) {
return count;
}
ptr += 4;
@@ -110,7 +110,7 @@ void ProtoDecodableMessage::decode(const uint8_t *buffer, size_t length) {
}
uint32_t field_length = res->as_uint32();
ptr += consumed;
if (ptr + field_length > end) {
if (field_length > static_cast<size_t>(end - ptr)) {
ESP_LOGV(TAG, "Out-of-bounds Length Delimited at offset %ld", (long) (ptr - buffer));
return;
}
@@ -121,7 +121,7 @@ void ProtoDecodableMessage::decode(const uint8_t *buffer, size_t length) {
break;
}
case WIRE_TYPE_FIXED32: { // 32-bit
if (ptr + 4 > end) {
if (end - ptr < 4) {
ESP_LOGV(TAG, "Out-of-bounds Fixed32-bit at offset %ld", (long) (ptr - buffer));
return;
}

View File

@@ -30,7 +30,7 @@ void DebugComponent::dump_config() {
char device_info_buffer[DEVICE_INFO_BUFFER_SIZE];
ESP_LOGD(TAG, "ESPHome version %s", ESPHOME_VERSION);
size_t pos = buf_append_printf(device_info_buffer, DEVICE_INFO_BUFFER_SIZE, 0, "%s", ESPHOME_VERSION);
size_t pos = buf_append(device_info_buffer, DEVICE_INFO_BUFFER_SIZE, 0, "%s", ESPHOME_VERSION);
this->free_heap_ = get_free_heap_();
ESP_LOGD(TAG, "Free Heap Size: %" PRIu32 " bytes", this->free_heap_);

View File

@@ -5,6 +5,12 @@
#include "esphome/core/helpers.h"
#include "esphome/core/macros.h"
#include <span>
#include <cstdarg>
#include <cstdio>
#include <algorithm>
#ifdef USE_ESP8266
#include <pgmspace.h>
#endif
#ifdef USE_SENSOR
#include "esphome/components/sensor/sensor.h"
@@ -19,7 +25,40 @@ namespace debug {
static constexpr size_t DEVICE_INFO_BUFFER_SIZE = 256;
static constexpr size_t RESET_REASON_BUFFER_SIZE = 128;
// buf_append_printf is now provided by esphome/core/helpers.h
#ifdef USE_ESP8266
// ESP8266: Use vsnprintf_P to keep format strings in flash (PROGMEM)
// Format strings must be wrapped with PSTR() macro
inline size_t buf_append_p(char *buf, size_t size, size_t pos, PGM_P fmt, ...) {
if (pos >= size) {
return size;
}
va_list args;
va_start(args, fmt);
int written = vsnprintf_P(buf + pos, size - pos, fmt, args);
va_end(args);
if (written < 0) {
return pos; // encoding error
}
return std::min(pos + static_cast<size_t>(written), size);
}
#define buf_append(buf, size, pos, fmt, ...) buf_append_p(buf, size, pos, PSTR(fmt), ##__VA_ARGS__)
#else
/// Safely append formatted string to buffer, returning new position (capped at size)
__attribute__((format(printf, 4, 5))) inline size_t buf_append(char *buf, size_t size, size_t pos, const char *fmt,
...) {
if (pos >= size) {
return size;
}
va_list args;
va_start(args, fmt);
int written = vsnprintf(buf + pos, size - pos, fmt, args);
va_end(args);
if (written < 0) {
return pos; // encoding error
}
return std::min(pos + static_cast<size_t>(written), size);
}
#endif
class DebugComponent : public PollingComponent {
public:
@@ -35,11 +74,8 @@ class DebugComponent : public PollingComponent {
#ifdef USE_SENSOR
void set_free_sensor(sensor::Sensor *free_sensor) { free_sensor_ = free_sensor; }
void set_block_sensor(sensor::Sensor *block_sensor) { block_sensor_ = block_sensor; }
#if (defined(USE_ESP8266) && USE_ARDUINO_VERSION_CODE >= VERSION_CODE(2, 5, 2)) || defined(USE_ESP32)
#if defined(USE_ESP8266) && USE_ARDUINO_VERSION_CODE >= VERSION_CODE(2, 5, 2)
void set_fragmentation_sensor(sensor::Sensor *fragmentation_sensor) { fragmentation_sensor_ = fragmentation_sensor; }
#endif
#if defined(USE_ESP32) || defined(USE_LIBRETINY)
void set_min_free_sensor(sensor::Sensor *min_free_sensor) { min_free_sensor_ = min_free_sensor; }
#endif
void set_loop_time_sensor(sensor::Sensor *loop_time_sensor) { loop_time_sensor_ = loop_time_sensor; }
#ifdef USE_ESP32
@@ -61,11 +97,8 @@ class DebugComponent : public PollingComponent {
sensor::Sensor *free_sensor_{nullptr};
sensor::Sensor *block_sensor_{nullptr};
#if (defined(USE_ESP8266) && USE_ARDUINO_VERSION_CODE >= VERSION_CODE(2, 5, 2)) || defined(USE_ESP32)
#if defined(USE_ESP8266) && USE_ARDUINO_VERSION_CODE >= VERSION_CODE(2, 5, 2)
sensor::Sensor *fragmentation_sensor_{nullptr};
#endif
#if defined(USE_ESP32) || defined(USE_LIBRETINY)
sensor::Sensor *min_free_sensor_{nullptr};
#endif
sensor::Sensor *loop_time_sensor_{nullptr};
#ifdef USE_ESP32

View File

@@ -173,8 +173,8 @@ size_t DebugComponent::get_device_info_(std::span<char, DEVICE_INFO_BUFFER_SIZE>
uint32_t flash_size = ESP.getFlashChipSize() / 1024; // NOLINT
uint32_t flash_speed = ESP.getFlashChipSpeed() / 1000000; // NOLINT
ESP_LOGD(TAG, "Flash Chip: Size=%" PRIu32 "kB Speed=%" PRIu32 "MHz Mode=%s", flash_size, flash_speed, flash_mode);
pos = buf_append_printf(buf, size, pos, "|Flash: %" PRIu32 "kB Speed:%" PRIu32 "MHz Mode:%s", flash_size, flash_speed,
flash_mode);
pos = buf_append(buf, size, pos, "|Flash: %" PRIu32 "kB Speed:%" PRIu32 "MHz Mode:%s", flash_size, flash_speed,
flash_mode);
#endif
esp_chip_info_t info;
@@ -182,71 +182,60 @@ size_t DebugComponent::get_device_info_(std::span<char, DEVICE_INFO_BUFFER_SIZE>
const char *model = ESPHOME_VARIANT;
// Build features string
pos = buf_append_printf(buf, size, pos, "|Chip: %s Features:", model);
pos = buf_append(buf, size, pos, "|Chip: %s Features:", model);
bool first_feature = true;
for (const auto &feature : CHIP_FEATURES) {
if (info.features & feature.bit) {
pos = buf_append_printf(buf, size, pos, "%s%s", first_feature ? "" : ", ", feature.name);
pos = buf_append(buf, size, pos, "%s%s", first_feature ? "" : ", ", feature.name);
first_feature = false;
info.features &= ~feature.bit;
}
}
if (info.features != 0) {
pos = buf_append_printf(buf, size, pos, "%sOther:0x%" PRIx32, first_feature ? "" : ", ", info.features);
pos = buf_append(buf, size, pos, "%sOther:0x%" PRIx32, first_feature ? "" : ", ", info.features);
}
ESP_LOGD(TAG, "Chip: Model=%s, Cores=%u, Revision=%u", model, info.cores, info.revision);
pos = buf_append_printf(buf, size, pos, " Cores:%u Revision:%u", info.cores, info.revision);
pos = buf_append(buf, size, pos, " Cores:%u Revision:%u", info.cores, info.revision);
uint32_t cpu_freq_mhz = arch_get_cpu_freq_hz() / 1000000;
ESP_LOGD(TAG, "CPU Frequency: %" PRIu32 " MHz", cpu_freq_mhz);
pos = buf_append_printf(buf, size, pos, "|CPU Frequency: %" PRIu32 " MHz", cpu_freq_mhz);
pos = buf_append(buf, size, pos, "|CPU Frequency: %" PRIu32 " MHz", cpu_freq_mhz);
// Framework detection
#ifdef USE_ARDUINO
ESP_LOGD(TAG, "Framework: Arduino");
pos = buf_append_printf(buf, size, pos, "|Framework: Arduino");
pos = buf_append(buf, size, pos, "|Framework: Arduino");
#elif defined(USE_ESP32)
ESP_LOGD(TAG, "Framework: ESP-IDF");
pos = buf_append_printf(buf, size, pos, "|Framework: ESP-IDF");
pos = buf_append(buf, size, pos, "|Framework: ESP-IDF");
#else
ESP_LOGW(TAG, "Framework: UNKNOWN");
pos = buf_append_printf(buf, size, pos, "|Framework: UNKNOWN");
pos = buf_append(buf, size, pos, "|Framework: UNKNOWN");
#endif
ESP_LOGD(TAG, "ESP-IDF Version: %s", esp_get_idf_version());
pos = buf_append_printf(buf, size, pos, "|ESP-IDF: %s", esp_get_idf_version());
pos = buf_append(buf, size, pos, "|ESP-IDF: %s", esp_get_idf_version());
uint8_t mac[6];
get_mac_address_raw(mac);
ESP_LOGD(TAG, "EFuse MAC: %02X:%02X:%02X:%02X:%02X:%02X", mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
pos = buf_append_printf(buf, size, pos, "|EFuse MAC: %02X:%02X:%02X:%02X:%02X:%02X", mac[0], mac[1], mac[2], mac[3],
mac[4], mac[5]);
pos = buf_append(buf, size, pos, "|EFuse MAC: %02X:%02X:%02X:%02X:%02X:%02X", mac[0], mac[1], mac[2], mac[3], mac[4],
mac[5]);
char reason_buffer[RESET_REASON_BUFFER_SIZE];
const char *reset_reason = get_reset_reason_(std::span<char, RESET_REASON_BUFFER_SIZE>(reason_buffer));
pos = buf_append_printf(buf, size, pos, "|Reset: %s", reset_reason);
pos = buf_append(buf, size, pos, "|Reset: %s", reset_reason);
const char *wakeup_cause = get_wakeup_cause_(std::span<char, RESET_REASON_BUFFER_SIZE>(reason_buffer));
pos = buf_append_printf(buf, size, pos, "|Wakeup: %s", wakeup_cause);
pos = buf_append(buf, size, pos, "|Wakeup: %s", wakeup_cause);
return pos;
}
void DebugComponent::update_platform_() {
#ifdef USE_SENSOR
uint32_t max_alloc = heap_caps_get_largest_free_block(MALLOC_CAP_INTERNAL);
if (this->block_sensor_ != nullptr) {
this->block_sensor_->publish_state(max_alloc);
}
if (this->min_free_sensor_ != nullptr) {
this->min_free_sensor_->publish_state(heap_caps_get_minimum_free_size(MALLOC_CAP_INTERNAL));
}
if (this->fragmentation_sensor_ != nullptr) {
uint32_t free_heap = heap_caps_get_free_size(MALLOC_CAP_INTERNAL);
if (free_heap > 0) {
float fragmentation = 100.0f - (100.0f * max_alloc / free_heap);
this->fragmentation_sensor_->publish_state(fragmentation);
}
this->block_sensor_->publish_state(heap_caps_get_largest_free_block(MALLOC_CAP_INTERNAL));
}
if (this->psram_sensor_ != nullptr) {
this->psram_sensor_->publish_state(heap_caps_get_free_size(MALLOC_CAP_SPIRAM));

View File

@@ -53,8 +53,8 @@ size_t DebugComponent::get_device_info_(std::span<char, DEVICE_INFO_BUFFER_SIZE>
uint32_t flash_size = ESP.getFlashChipSize() / 1024; // NOLINT
uint32_t flash_speed = ESP.getFlashChipSpeed() / 1000000; // NOLINT
ESP_LOGD(TAG, "Flash Chip: Size=%" PRIu32 "kB Speed=%" PRIu32 "MHz Mode=%s", flash_size, flash_speed, flash_mode);
pos = buf_append_printf(buf, size, pos, "|Flash: %" PRIu32 "kB Speed:%" PRIu32 "MHz Mode:%s", flash_size, flash_speed,
flash_mode);
pos = buf_append(buf, size, pos, "|Flash: %" PRIu32 "kB Speed:%" PRIu32 "MHz Mode:%s", flash_size, flash_speed,
flash_mode);
#if !defined(CLANG_TIDY)
char reason_buffer[RESET_REASON_BUFFER_SIZE];
@@ -77,15 +77,15 @@ size_t DebugComponent::get_device_info_(std::span<char, DEVICE_INFO_BUFFER_SIZE>
chip_id, ESP.getSdkVersion(), ESP.getCoreVersion().c_str(), boot_version, boot_mode, cpu_freq, flash_chip_id,
reset_reason, ESP.getResetInfo().c_str());
pos = buf_append_printf(buf, size, pos, "|Chip: 0x%08" PRIX32, chip_id);
pos = buf_append_printf(buf, size, pos, "|SDK: %s", ESP.getSdkVersion());
pos = buf_append_printf(buf, size, pos, "|Core: %s", ESP.getCoreVersion().c_str());
pos = buf_append_printf(buf, size, pos, "|Boot: %u", boot_version);
pos = buf_append_printf(buf, size, pos, "|Mode: %u", boot_mode);
pos = buf_append_printf(buf, size, pos, "|CPU: %u", cpu_freq);
pos = buf_append_printf(buf, size, pos, "|Flash: 0x%08" PRIX32, flash_chip_id);
pos = buf_append_printf(buf, size, pos, "|Reset: %s", reset_reason);
pos = buf_append_printf(buf, size, pos, "|%s", ESP.getResetInfo().c_str());
pos = buf_append(buf, size, pos, "|Chip: 0x%08" PRIX32, chip_id);
pos = buf_append(buf, size, pos, "|SDK: %s", ESP.getSdkVersion());
pos = buf_append(buf, size, pos, "|Core: %s", ESP.getCoreVersion().c_str());
pos = buf_append(buf, size, pos, "|Boot: %u", boot_version);
pos = buf_append(buf, size, pos, "|Mode: %u", boot_mode);
pos = buf_append(buf, size, pos, "|CPU: %u", cpu_freq);
pos = buf_append(buf, size, pos, "|Flash: 0x%08" PRIX32, flash_chip_id);
pos = buf_append(buf, size, pos, "|Reset: %s", reset_reason);
pos = buf_append(buf, size, pos, "|%s", ESP.getResetInfo().c_str());
#endif
return pos;

View File

@@ -36,12 +36,12 @@ size_t DebugComponent::get_device_info_(std::span<char, DEVICE_INFO_BUFFER_SIZE>
lt_get_version(), lt_cpu_get_model_name(), lt_cpu_get_model(), lt_cpu_get_freq_mhz(), mac_id,
lt_get_board_code(), flash_kib, ram_kib, reset_reason);
pos = buf_append_printf(buf, size, pos, "|Version: %s", LT_BANNER_STR + 10);
pos = buf_append_printf(buf, size, pos, "|Reset Reason: %s", reset_reason);
pos = buf_append_printf(buf, size, pos, "|Chip Name: %s", lt_cpu_get_model_name());
pos = buf_append_printf(buf, size, pos, "|Chip ID: 0x%06" PRIX32, mac_id);
pos = buf_append_printf(buf, size, pos, "|Flash: %" PRIu32 " KiB", flash_kib);
pos = buf_append_printf(buf, size, pos, "|RAM: %" PRIu32 " KiB", ram_kib);
pos = buf_append(buf, size, pos, "|Version: %s", LT_BANNER_STR + 10);
pos = buf_append(buf, size, pos, "|Reset Reason: %s", reset_reason);
pos = buf_append(buf, size, pos, "|Chip Name: %s", lt_cpu_get_model_name());
pos = buf_append(buf, size, pos, "|Chip ID: 0x%06" PRIX32, mac_id);
pos = buf_append(buf, size, pos, "|Flash: %" PRIu32 " KiB", flash_kib);
pos = buf_append(buf, size, pos, "|RAM: %" PRIu32 " KiB", ram_kib);
return pos;
}
@@ -51,9 +51,6 @@ void DebugComponent::update_platform_() {
if (this->block_sensor_ != nullptr) {
this->block_sensor_->publish_state(lt_heap_get_max_alloc());
}
if (this->min_free_sensor_ != nullptr) {
this->min_free_sensor_->publish_state(lt_heap_get_min_free());
}
#endif
}

View File

@@ -19,7 +19,7 @@ size_t DebugComponent::get_device_info_(std::span<char, DEVICE_INFO_BUFFER_SIZE>
uint32_t cpu_freq = rp2040.f_cpu();
ESP_LOGD(TAG, "CPU Frequency: %" PRIu32, cpu_freq);
pos = buf_append_printf(buf, size, pos, "|CPU Frequency: %" PRIu32, cpu_freq);
pos = buf_append(buf, size, pos, "|CPU Frequency: %" PRIu32, cpu_freq);
return pos;
}

View File

@@ -20,9 +20,9 @@ static size_t append_reset_reason(char *buf, size_t size, size_t pos, bool set,
return pos;
}
if (pos > 0) {
pos = buf_append_printf(buf, size, pos, ", ");
pos = buf_append(buf, size, pos, ", ");
}
return buf_append_printf(buf, size, pos, "%s", reason);
return buf_append(buf, size, pos, "%s", reason);
}
static inline uint32_t read_mem_u32(uintptr_t addr) {
@@ -140,7 +140,7 @@ size_t DebugComponent::get_device_info_(std::span<char, DEVICE_INFO_BUFFER_SIZE>
const char *supply_status =
(nrf_power_mainregstatus_get(NRF_POWER) == NRF_POWER_MAINREGSTATUS_NORMAL) ? "Normal voltage." : "High voltage.";
ESP_LOGD(TAG, "Main supply status: %s", supply_status);
pos = buf_append_printf(buf, size, pos, "|Main supply status: %s", supply_status);
pos = buf_append(buf, size, pos, "|Main supply status: %s", supply_status);
// Regulator stage 0
if (nrf_power_mainregstatus_get(NRF_POWER) == NRF_POWER_MAINREGSTATUS_HIGH) {
@@ -172,16 +172,16 @@ size_t DebugComponent::get_device_info_(std::span<char, DEVICE_INFO_BUFFER_SIZE>
reg0_voltage = "???V";
}
ESP_LOGD(TAG, "Regulator stage 0: %s, %s", reg0_type, reg0_voltage);
pos = buf_append_printf(buf, size, pos, "|Regulator stage 0: %s, %s", reg0_type, reg0_voltage);
pos = buf_append(buf, size, pos, "|Regulator stage 0: %s, %s", reg0_type, reg0_voltage);
} else {
ESP_LOGD(TAG, "Regulator stage 0: disabled");
pos = buf_append_printf(buf, size, pos, "|Regulator stage 0: disabled");
pos = buf_append(buf, size, pos, "|Regulator stage 0: disabled");
}
// Regulator stage 1
const char *reg1_type = nrf_power_dcdcen_get(NRF_POWER) ? "DC/DC" : "LDO";
ESP_LOGD(TAG, "Regulator stage 1: %s", reg1_type);
pos = buf_append_printf(buf, size, pos, "|Regulator stage 1: %s", reg1_type);
pos = buf_append(buf, size, pos, "|Regulator stage 1: %s", reg1_type);
// USB power state
const char *usb_state;
@@ -195,7 +195,7 @@ size_t DebugComponent::get_device_info_(std::span<char, DEVICE_INFO_BUFFER_SIZE>
usb_state = "disconnected";
}
ESP_LOGD(TAG, "USB power state: %s", usb_state);
pos = buf_append_printf(buf, size, pos, "|USB power state: %s", usb_state);
pos = buf_append(buf, size, pos, "|USB power state: %s", usb_state);
// Power-fail comparator
bool enabled;
@@ -300,14 +300,14 @@ size_t DebugComponent::get_device_info_(std::span<char, DEVICE_INFO_BUFFER_SIZE>
break;
}
ESP_LOGD(TAG, "Power-fail comparator: %s, VDDH: %s", pof_voltage, vddh_voltage);
pos = buf_append_printf(buf, size, pos, "|Power-fail comparator: %s, VDDH: %s", pof_voltage, vddh_voltage);
pos = buf_append(buf, size, pos, "|Power-fail comparator: %s, VDDH: %s", pof_voltage, vddh_voltage);
} else {
ESP_LOGD(TAG, "Power-fail comparator: %s", pof_voltage);
pos = buf_append_printf(buf, size, pos, "|Power-fail comparator: %s", pof_voltage);
pos = buf_append(buf, size, pos, "|Power-fail comparator: %s", pof_voltage);
}
} else {
ESP_LOGD(TAG, "Power-fail comparator: disabled");
pos = buf_append_printf(buf, size, pos, "|Power-fail comparator: disabled");
pos = buf_append(buf, size, pos, "|Power-fail comparator: disabled");
}
auto package = [](uint32_t value) {

View File

@@ -11,9 +11,6 @@ from esphome.const import (
ENTITY_CATEGORY_DIAGNOSTIC,
ICON_COUNTER,
ICON_TIMER,
PLATFORM_BK72XX,
PLATFORM_LN882X,
PLATFORM_RTL87XX,
UNIT_BYTES,
UNIT_HERTZ,
UNIT_MILLISECOND,
@@ -28,7 +25,6 @@ from . import ( # noqa: F401 pylint: disable=unused-import
DEPENDENCIES = ["debug"]
CONF_MIN_FREE = "min_free"
CONF_PSRAM = "psram"
CONFIG_SCHEMA = {
@@ -46,14 +42,8 @@ CONFIG_SCHEMA = {
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
),
cv.Optional(CONF_FRAGMENTATION): cv.All(
cv.Any(
cv.All(
cv.only_on_esp8266,
cv.require_framework_version(esp8266_arduino=cv.Version(2, 5, 2)),
),
cv.only_on_esp32,
msg="This feature is only available on ESP8266 (Arduino 2.5.2+) and ESP32",
),
cv.only_on_esp8266,
cv.require_framework_version(esp8266_arduino=cv.Version(2, 5, 2)),
sensor.sensor_schema(
unit_of_measurement=UNIT_PERCENT,
icon=ICON_COUNTER,
@@ -61,19 +51,6 @@ CONFIG_SCHEMA = {
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
),
),
cv.Optional(CONF_MIN_FREE): cv.All(
cv.Any(
cv.only_on_esp32,
cv.only_on([PLATFORM_BK72XX, PLATFORM_LN882X, PLATFORM_RTL87XX]),
msg="This feature is only available on ESP32 and LibreTiny (BK72xx, LN882x, RTL87xx)",
),
sensor.sensor_schema(
unit_of_measurement=UNIT_BYTES,
icon=ICON_COUNTER,
accuracy_decimals=0,
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
),
),
cv.Optional(CONF_LOOP_TIME): sensor.sensor_schema(
unit_of_measurement=UNIT_MILLISECOND,
icon=ICON_TIMER,
@@ -116,10 +93,6 @@ async def to_code(config):
sens = await sensor.new_sensor(fragmentation_conf)
cg.add(debug_component.set_fragmentation_sensor(sens))
if min_free_conf := config.get(CONF_MIN_FREE):
sens = await sensor.new_sensor(min_free_conf)
cg.add(debug_component.set_min_free_sensor(sens))
if loop_time_conf := config.get(CONF_LOOP_TIME):
sens = await sensor.new_sensor(loop_time_conf)
cg.add(debug_component.set_loop_time_sensor(sens))

View File

@@ -1,4 +1,3 @@
#include <cstdio>
#include <cstring>
#include "hmac_sha256.h"
#if defined(USE_ESP32) || defined(USE_ESP8266) || defined(USE_RP2040) || defined(USE_LIBRETINY) || defined(USE_HOST)
@@ -26,9 +25,7 @@ void HmacSHA256::calculate() { mbedtls_md_hmac_finish(&this->ctx_, this->digest_
void HmacSHA256::get_bytes(uint8_t *output) { memcpy(output, this->digest_, SHA256_DIGEST_SIZE); }
void HmacSHA256::get_hex(char *output) {
for (size_t i = 0; i < SHA256_DIGEST_SIZE; i++) {
sprintf(output + (i * 2), "%02x", this->digest_[i]);
}
format_hex_to(output, SHA256_DIGEST_SIZE * 2 + 1, this->digest_, SHA256_DIGEST_SIZE);
}
bool HmacSHA256::equals_bytes(const uint8_t *expected) {

View File

@@ -242,9 +242,7 @@ template<typename... Ts> class HttpRequestSendAction : public Action<Ts...> {
return;
}
size_t content_length = container->content_length;
size_t max_length = std::min(content_length, this->max_response_buffer_size_);
size_t max_length = this->max_response_buffer_size_;
#ifdef USE_HTTP_REQUEST_RESPONSE
if (this->capture_response_.value(x...)) {
std::string response_body;

View File

@@ -213,18 +213,12 @@ int HttpContainerIDF::read(uint8_t *buf, size_t max_len) {
const uint32_t start = millis();
watchdog::WatchdogManager wdm(this->parent_->get_watchdog_timeout());
int bufsize = std::min(max_len, this->content_length - this->bytes_read_);
if (bufsize == 0) {
this->duration_ms += (millis() - start);
return 0;
this->feed_wdt();
int read_len = esp_http_client_read(this->client_, (char *) buf, max_len);
this->feed_wdt();
if (read_len > 0) {
this->bytes_read_ += read_len;
}
this->feed_wdt();
int read_len = esp_http_client_read(this->client_, (char *) buf, bufsize);
this->feed_wdt();
this->bytes_read_ += read_len;
this->duration_ms += (millis() - start);
return read_len;

View File

@@ -1,3 +1,4 @@
import logging
from typing import Any
from esphome import automation, pins
@@ -18,13 +19,16 @@ from esphome.const import (
CONF_ROTATION,
CONF_UPDATE_INTERVAL,
)
from esphome.core import ID
from esphome.core import ID, EnumValue
from esphome.cpp_generator import MockObj, TemplateArgsType
import esphome.final_validate as fv
from esphome.helpers import add_class_to_obj
from esphome.types import ConfigType
from . import boards, hub75_ns
_LOGGER = logging.getLogger(__name__)
DEPENDENCIES = ["esp32"]
CODEOWNERS = ["@stuartparmenter"]
@@ -120,13 +124,51 @@ PANEL_LAYOUTS = {
}
Hub75ScanWiring = cg.global_ns.enum("Hub75ScanWiring", is_class=True)
SCAN_PATTERNS = {
SCAN_WIRINGS = {
"STANDARD_TWO_SCAN": Hub75ScanWiring.STANDARD_TWO_SCAN,
"FOUR_SCAN_16PX_HIGH": Hub75ScanWiring.FOUR_SCAN_16PX_HIGH,
"FOUR_SCAN_32PX_HIGH": Hub75ScanWiring.FOUR_SCAN_32PX_HIGH,
"FOUR_SCAN_64PX_HIGH": Hub75ScanWiring.FOUR_SCAN_64PX_HIGH,
"SCAN_1_4_16PX_HIGH": Hub75ScanWiring.SCAN_1_4_16PX_HIGH,
"SCAN_1_8_32PX_HIGH": Hub75ScanWiring.SCAN_1_8_32PX_HIGH,
"SCAN_1_8_40PX_HIGH": Hub75ScanWiring.SCAN_1_8_40PX_HIGH,
"SCAN_1_8_64PX_HIGH": Hub75ScanWiring.SCAN_1_8_64PX_HIGH,
}
# Deprecated scan wiring names - mapped to new names
DEPRECATED_SCAN_WIRINGS = {
"FOUR_SCAN_16PX_HIGH": "SCAN_1_4_16PX_HIGH",
"FOUR_SCAN_32PX_HIGH": "SCAN_1_8_32PX_HIGH",
"FOUR_SCAN_64PX_HIGH": "SCAN_1_8_64PX_HIGH",
}
def _validate_scan_wiring(value):
"""Validate scan_wiring with deprecation warnings for old names."""
value = cv.string(value).upper().replace(" ", "_")
# Check if using deprecated name
# Remove deprecated names in 2026.7.0
if value in DEPRECATED_SCAN_WIRINGS:
new_name = DEPRECATED_SCAN_WIRINGS[value]
_LOGGER.warning(
"Scan wiring '%s' is deprecated and will be removed in ESPHome 2026.7.0. "
"Please use '%s' instead.",
value,
new_name,
)
value = new_name
# Validate against allowed values
if value not in SCAN_WIRINGS:
raise cv.Invalid(
f"Unknown scan wiring '{value}'. "
f"Valid options are: {', '.join(sorted(SCAN_WIRINGS.keys()))}"
)
# Return as EnumValue like cv.enum does
result = add_class_to_obj(value, EnumValue)
result.enum_value = SCAN_WIRINGS[value]
return result
Hub75ClockSpeed = cg.global_ns.enum("Hub75ClockSpeed", is_class=True)
CLOCK_SPEEDS = {
"8MHZ": Hub75ClockSpeed.HZ_8M,
@@ -382,9 +424,7 @@ CONFIG_SCHEMA = cv.All(
cv.Optional(CONF_LAYOUT_COLS): cv.positive_int,
cv.Optional(CONF_LAYOUT): cv.enum(PANEL_LAYOUTS, upper=True, space="_"),
# Panel hardware configuration
cv.Optional(CONF_SCAN_WIRING): cv.enum(
SCAN_PATTERNS, upper=True, space="_"
),
cv.Optional(CONF_SCAN_WIRING): _validate_scan_wiring,
cv.Optional(CONF_SHIFT_DRIVER): cv.enum(SHIFT_DRIVERS, upper=True),
# Display configuration
cv.Optional(CONF_DOUBLE_BUFFER): cv.boolean,
@@ -547,7 +587,7 @@ def _build_config_struct(
async def to_code(config: ConfigType) -> None:
add_idf_component(
name="esphome/esp-hub75",
ref="0.2.2",
ref="0.3.0",
)
# Set compile-time configuration via build flags (so external library sees them)

View File

@@ -18,15 +18,7 @@ InfraredCall &InfraredCall::set_carrier_frequency(uint32_t frequency) {
InfraredCall &InfraredCall::set_raw_timings(const std::vector<int32_t> &timings) {
this->raw_timings_ = &timings;
this->packed_data_ = nullptr;
this->base85_ptr_ = nullptr;
return *this;
}
InfraredCall &InfraredCall::set_raw_timings_base85(const std::string &base85) {
this->base85_ptr_ = &base85;
this->raw_timings_ = nullptr;
this->packed_data_ = nullptr;
this->packed_data_ = nullptr; // Clear packed if vector is set
return *this;
}
@@ -34,8 +26,7 @@ InfraredCall &InfraredCall::set_raw_timings_packed(const uint8_t *data, uint16_t
this->packed_data_ = data;
this->packed_length_ = length;
this->packed_count_ = count;
this->raw_timings_ = nullptr;
this->base85_ptr_ = nullptr;
this->raw_timings_ = nullptr; // Clear vector if packed is set
return *this;
}
@@ -101,14 +92,6 @@ void Infrared::control(const InfraredCall &call) {
call.get_packed_count());
ESP_LOGD(TAG, "Transmitting packed raw timings: count=%u, repeat=%u", call.get_packed_count(),
call.get_repeat_count());
} else if (call.is_base85()) {
// Decode base85 directly into transmit buffer (zero heap allocations)
if (!transmit_data->set_data_from_base85(call.get_base85_data())) {
ESP_LOGE(TAG, "Invalid base85 data");
return;
}
ESP_LOGD(TAG, "Transmitting base85 raw timings: count=%zu, repeat=%u", transmit_data->get_data().size(),
call.get_repeat_count());
} else {
// From vector (lambdas/automations)
transmit_data->set_data(call.get_raw_timings());

View File

@@ -28,29 +28,12 @@ class InfraredCall {
/// Set the carrier frequency in Hz
InfraredCall &set_carrier_frequency(uint32_t frequency);
// ===== Raw Timings Methods =====
// All set_raw_timings_* methods store pointers/references to external data.
// The referenced data must remain valid until perform() completes.
// Safe pattern: call.set_raw_timings_xxx(data); call.perform(); // synchronous
// Unsafe pattern: call.set_raw_timings_xxx(data); defer([call]() { call.perform(); }); // data may be gone!
/// Set the raw timings from a vector (positive = mark, negative = space)
/// @note Lifetime: Stores a pointer to the vector. The vector must outlive perform().
/// @note Usage: Primarily for lambdas/automations where the vector is in scope.
/// Set the raw timings (positive = mark, negative = space)
/// Note: The timings vector must outlive the InfraredCall (zero-copy reference)
InfraredCall &set_raw_timings(const std::vector<int32_t> &timings);
/// Set the raw timings from base85-encoded int32 data
/// @note Lifetime: Stores a pointer to the string. The string must outlive perform().
/// @note Usage: For web_server where the encoded string is on the stack.
/// @note Decoding happens at perform() time, directly into the transmit buffer.
InfraredCall &set_raw_timings_base85(const std::string &base85);
/// Set the raw timings from packed protobuf sint32 data (zigzag + varint encoded)
/// @note Lifetime: Stores a pointer to the buffer. The buffer must outlive perform().
/// @note Usage: For API component where data comes directly from the protobuf message.
/// Set the raw timings from packed protobuf sint32 data (zero-copy from wire)
/// Note: The data must outlive the InfraredCall
InfraredCall &set_raw_timings_packed(const uint8_t *data, uint16_t length, uint16_t count);
/// Set the number of times to repeat transmission (1 = transmit once, 2 = transmit twice, etc.)
InfraredCall &set_repeat_count(uint32_t count);
@@ -59,18 +42,12 @@ class InfraredCall {
/// Get the carrier frequency
const optional<uint32_t> &get_carrier_frequency() const { return this->carrier_frequency_; }
/// Get the raw timings (only valid if set via set_raw_timings, not packed or base85)
/// Get the raw timings (only valid if set via set_raw_timings, not packed)
const std::vector<int32_t> &get_raw_timings() const { return *this->raw_timings_; }
/// Check if raw timings have been set (vector, packed, or base85)
bool has_raw_timings() const {
return this->raw_timings_ != nullptr || this->packed_data_ != nullptr || this->base85_ptr_ != nullptr;
}
/// Check if raw timings have been set (either vector or packed)
bool has_raw_timings() const { return this->raw_timings_ != nullptr || this->packed_data_ != nullptr; }
/// Check if using packed data format
bool is_packed() const { return this->packed_data_ != nullptr; }
/// Check if using base85 data format
bool is_base85() const { return this->base85_ptr_ != nullptr; }
/// Get the base85 data string
const std::string &get_base85_data() const { return *this->base85_ptr_; }
/// Get packed data (only valid if set via set_raw_timings_packed)
const uint8_t *get_packed_data() const { return this->packed_data_; }
uint16_t get_packed_length() const { return this->packed_length_; }
@@ -82,11 +59,9 @@ class InfraredCall {
uint32_t repeat_count_{1};
Infrared *parent_;
optional<uint32_t> carrier_frequency_;
// Pointer to vector-based timings (caller-owned, must outlive perform())
// Vector-based timings (for lambdas/automations)
const std::vector<int32_t> *raw_timings_{nullptr};
// Pointer to base85-encoded string (caller-owned, must outlive perform())
const std::string *base85_ptr_{nullptr};
// Pointer to packed protobuf buffer (caller-owned, must outlive perform())
// Packed protobuf timings (for API zero-copy)
const uint8_t *packed_data_{nullptr};
uint16_t packed_length_{0};
uint16_t packed_count_{0};

View File

@@ -413,7 +413,6 @@ class TextValidator(LValidator):
str_args = [str(x) for x in value[CONF_ARGS]]
arg_expr = cg.RawExpression(",".join(str_args))
format_str = cpp_string_escape(format_str)
# str_sprintf justified: user-defined format, can't optimize without permanent RAM cost
sprintf_str = f"str_sprintf({format_str}, {arg_expr}).c_str()"
if nanval := value.get(CONF_IF_NAN):
nanval = cpp_string_escape(nanval)

View File

@@ -65,10 +65,7 @@ std::string lv_event_code_name_for(uint8_t event_code) {
if (event_code < sizeof(EVENT_NAMES) / sizeof(EVENT_NAMES[0])) {
return EVENT_NAMES[event_code];
}
// max 4 bytes: "%u" with uint8_t (max 255, 3 digits) + null
char buf[4];
snprintf(buf, sizeof(buf), "%u", event_code);
return buf;
return str_sprintf("%2d", event_code);
}
static void rounder_cb(lv_disp_drv_t *disp_drv, lv_area_t *area) {

View File

@@ -23,7 +23,7 @@ void NTC::process_(float value) {
double v = this->a_ + this->b_ * lr + this->c_ * lr * lr * lr;
auto temp = float(1.0 / v - 273.15);
ESP_LOGD(TAG, "'%s' - Temperature: %.1f°C", this->name_.c_str(), temp);
ESP_LOGV(TAG, "'%s' - Temperature: %.1f°C", this->name_.c_str(), temp);
this->publish_state(temp);
}

View File

@@ -159,10 +159,6 @@ void RemoteTransmitData::set_data_from_packed_sint32(const uint8_t *data, size_t
}
}
bool RemoteTransmitData::set_data_from_base85(const std::string &base85) {
return base85_decode_int32_vector(base85, this->data_);
}
/* RemoteTransmitterBase */
void RemoteTransmitterBase::send_(uint32_t send_times, uint32_t send_wait) {

View File

@@ -36,11 +36,6 @@ class RemoteTransmitData {
/// @param len Length of the buffer in bytes
/// @param count Number of values (for reserve optimization)
void set_data_from_packed_sint32(const uint8_t *data, size_t len, size_t count);
/// Set data from base85-encoded int32 values
/// Decodes directly into internal buffer (zero heap allocations)
/// @param base85 Base85-encoded string (5 chars per int32 value)
/// @return true if successful, false if decode failed or invalid size
bool set_data_from_base85(const std::string &base85);
void reset() {
this->data_.clear();
this->carrier_frequency_ = 0;

View File

@@ -39,7 +39,7 @@ void ResistanceSensor::process_(float value) {
}
res *= this->resistor_;
ESP_LOGD(TAG, "'%s' - Resistance %.1fΩ", this->name_.c_str(), res);
ESP_LOGV(TAG, "'%s' - Resistance %.1fΩ", this->name_.c_str(), res);
this->publish_state(res);
}

View File

@@ -753,6 +753,9 @@ void WebServer::handle_button_request(AsyncWebServerRequest *request, const UrlM
}
request->send(404);
}
std::string WebServer::button_state_json_generator(WebServer *web_server, void *source) {
return web_server->button_json_((button::Button *) (source), DETAIL_STATE);
}
std::string WebServer::button_all_json_generator(WebServer *web_server, void *source) {
return web_server->button_json_((button::Button *) (source), DETAIL_ALL);
}

View File

@@ -295,7 +295,7 @@ class WebServer : public Controller,
/// Handle a button request under '/button/<id>/press'.
void handle_button_request(AsyncWebServerRequest *request, const UrlMatch &match);
// Buttons are stateless, so there is no button_state_json_generator
static std::string button_state_json_generator(WebServer *web_server, void *source);
static std::string button_all_json_generator(WebServer *web_server, void *source);
#endif

View File

@@ -4,7 +4,7 @@ from enum import Enum
from esphome.enum import StrEnum
__version__ = "2026.2.0-dev"
__version__ = "2026.1.0b3"
ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_"
VALID_SUBSTITUTIONS_CHARACTERS = (

View File

@@ -617,55 +617,6 @@ std::vector<uint8_t> base64_decode(const std::string &encoded_string) {
return ret;
}
/// Encode int32 to 5 base85 characters + null terminator
/// Standard ASCII85 alphabet: '!' (33) = 0 through 'u' (117) = 84
inline void base85_encode_int32(int32_t value, std::span<char, BASE85_INT32_ENCODED_SIZE> output) {
uint32_t v = static_cast<uint32_t>(value);
// Encode least significant digit first, then reverse
for (int i = 4; i >= 0; i--) {
output[i] = static_cast<char>('!' + (v % 85));
v /= 85;
}
output[5] = '\0';
}
/// Decode 5 base85 characters to int32
inline bool base85_decode_int32(const char *input, int32_t &out) {
uint8_t c0 = static_cast<uint8_t>(input[0] - '!');
uint8_t c1 = static_cast<uint8_t>(input[1] - '!');
uint8_t c2 = static_cast<uint8_t>(input[2] - '!');
uint8_t c3 = static_cast<uint8_t>(input[3] - '!');
uint8_t c4 = static_cast<uint8_t>(input[4] - '!');
// Each digit must be 0-84. Since uint8_t wraps, chars below '!' become > 84
if (c0 > 84 || c1 > 84 || c2 > 84 || c3 > 84 || c4 > 84)
return false;
// 85^4 = 52200625, 85^3 = 614125, 85^2 = 7225, 85^1 = 85
out = static_cast<int32_t>(c0 * 52200625u + c1 * 614125u + c2 * 7225u + c3 * 85u + c4);
return true;
}
/// Decode base85 string directly into vector (no intermediate buffer)
bool base85_decode_int32_vector(const std::string &base85, std::vector<int32_t> &out) {
size_t len = base85.size();
if (len % 5 != 0)
return false;
out.clear();
const char *ptr = base85.data();
const char *end = ptr + len;
while (ptr < end) {
int32_t value;
if (!base85_decode_int32(ptr, value))
return false;
out.push_back(value);
ptr += 5;
}
return true;
}
// Colors
float gamma_correct(float value, float gamma) {

View File

@@ -1,11 +1,8 @@
#pragma once
#include <algorithm>
#include <array>
#include <cmath>
#include <cstdarg>
#include <cstdint>
#include <cstdio>
#include <cstring>
#include <functional>
#include <iterator>
@@ -21,7 +18,6 @@
#ifdef USE_ESP8266
#include <Esp.h>
#include <pgmspace.h>
#endif
#ifdef USE_RP2040
@@ -572,53 +568,6 @@ std::string __attribute__((format(printf, 1, 3))) str_snprintf(const char *fmt,
/// sprintf-like function returning std::string.
std::string __attribute__((format(printf, 1, 2))) str_sprintf(const char *fmt, ...);
#ifdef USE_ESP8266
// ESP8266: Use vsnprintf_P to keep format strings in flash (PROGMEM)
// Format strings must be wrapped with PSTR() macro
/// Safely append formatted string to buffer, returning new position (capped at size).
/// @param buf Output buffer
/// @param size Total buffer size
/// @param pos Current position in buffer
/// @param fmt Format string (must be in PROGMEM on ESP8266)
/// @return New position after appending (capped at size on overflow)
inline size_t buf_append_printf_p(char *buf, size_t size, size_t pos, PGM_P fmt, ...) {
if (pos >= size) {
return size;
}
va_list args;
va_start(args, fmt);
int written = vsnprintf_P(buf + pos, size - pos, fmt, args);
va_end(args);
if (written < 0) {
return pos; // encoding error
}
return std::min(pos + static_cast<size_t>(written), size);
}
#define buf_append_printf(buf, size, pos, fmt, ...) buf_append_printf_p(buf, size, pos, PSTR(fmt), ##__VA_ARGS__)
#else
/// Safely append formatted string to buffer, returning new position (capped at size).
/// Handles snprintf edge cases: negative returns (encoding errors) and truncation.
/// @param buf Output buffer
/// @param size Total buffer size
/// @param pos Current position in buffer
/// @param fmt printf-style format string
/// @return New position after appending (capped at size on overflow)
__attribute__((format(printf, 4, 5))) inline size_t buf_append_printf(char *buf, size_t size, size_t pos,
const char *fmt, ...) {
if (pos >= size) {
return size;
}
va_list args;
va_start(args, fmt);
int written = vsnprintf(buf + pos, size - pos, fmt, args);
va_end(args);
if (written < 0) {
return pos; // encoding error
}
return std::min(pos + static_cast<size_t>(written), size);
}
#endif
/// Concatenate a name with a separator and suffix using an efficient stack-based approach.
/// This avoids multiple heap allocations during string construction.
/// Maximum name length supported is 120 characters for friendly names.
@@ -1137,14 +1086,6 @@ std::vector<uint8_t> base64_decode(const std::string &encoded_string);
size_t base64_decode(std::string const &encoded_string, uint8_t *buf, size_t buf_len);
size_t base64_decode(const uint8_t *encoded_data, size_t encoded_len, uint8_t *buf, size_t buf_len);
/// Size of buffer needed for base85 encoded int32 (5 chars + null terminator)
static constexpr size_t BASE85_INT32_ENCODED_SIZE = 6;
void base85_encode_int32(int32_t value, std::span<char, BASE85_INT32_ENCODED_SIZE> output);
bool base85_decode_int32(const char *input, int32_t &out);
bool base85_decode_int32_vector(const std::string &base85, std::vector<int32_t> &out);
///@}
/// @name Colors

View File

@@ -28,8 +28,8 @@ dependencies:
rules:
- if: "target in [esp32s2, esp32s3, esp32p4]"
esphome/esp-hub75:
version: 0.2.2
version: 0.3.0
rules:
- if: "target in [esp32, esp32s2, esp32s3, esp32p4]"
- if: "target in [esp32, esp32s2, esp32s3, esp32c6, esp32p4]"
esp32async/asynctcp:
version: 3.4.91

View File

@@ -19,7 +19,7 @@ ruamel.yaml==0.19.1 # dashboard_import
ruamel.yaml.clib==0.2.15 # dashboard_import
esphome-glyphsets==0.2.0
pillow==11.3.0
resvg-py==0.2.6
resvg-py==0.2.5
freetype-py==2.5.1
jinja2==3.1.6
bleak==2.1.1

View File

@@ -1,6 +1,6 @@
pylint==4.0.4
flake8==7.3.0 # also change in .pre-commit-config.yaml when updating
ruff==0.14.12 # also change in .pre-commit-config.yaml when updating
ruff==0.14.11 # also change in .pre-commit-config.yaml when updating
pyupgrade==3.21.2 # also change in .pre-commit-config.yaml when updating
pre-commit

View File

@@ -90,10 +90,7 @@ class Platform(StrEnum):
ESP32_S2_IDF = "esp32-s2-idf"
ESP32_S3_IDF = "esp32-s3-idf"
BK72XX_ARD = "bk72xx-ard" # LibreTiny BK7231N
RTL87XX_ARD = "rtl87xx-ard" # LibreTiny RTL8720x
LN882X_ARD = "ln882x-ard" # LibreTiny LN882x
RP2040_ARD = "rp2040-ard" # Raspberry Pi Pico
NRF52_ZEPHYR = "nrf52-adafruit" # Nordic nRF52 (Zephyr)
# Memory impact analysis constants
@@ -113,7 +110,7 @@ PLATFORM_SPECIFIC_COMPONENTS = frozenset(
"rtl87xx", # Realtek RTL87xx platform implementation (uses LibreTiny)
"ln882x", # Winner Micro LN882x platform implementation (uses LibreTiny)
"host", # Host platform (for testing on development machine)
"nrf52", # Nordic nRF52 platform implementation (uses Zephyr)
"nrf52", # Nordic nRF52 platform implementation
}
)
@@ -125,9 +122,8 @@ PLATFORM_SPECIFIC_COMPONENTS = frozenset(
# fastest build times, most sensitive to code size changes
# 3. ESP32 IDF - Primary ESP32 platform, most representative of modern ESPHome
# 4-6. Other ESP32 variants - Less commonly used but still supported
# 7-9. LibreTiny platforms (BK72XX, RTL87XX, LN882X) - good for detecting LibreTiny-specific changes
# 10. RP2040 - Raspberry Pi Pico platform
# 11. nRF52 - Nordic nRF52 with Zephyr (good for detecting Zephyr-specific changes)
# 7. BK72XX - LibreTiny platform (good for detecting LibreTiny-specific changes)
# 8. RP2040 - Raspberry Pi Pico platform
MEMORY_IMPACT_PLATFORM_PREFERENCE = [
Platform.ESP32_C6_IDF, # ESP32-C6 IDF (newest, supports Thread/Zigbee)
Platform.ESP8266_ARD, # ESP8266 Arduino (most memory constrained, fastest builds)
@@ -136,10 +132,7 @@ MEMORY_IMPACT_PLATFORM_PREFERENCE = [
Platform.ESP32_S2_IDF, # ESP32-S2 IDF
Platform.ESP32_S3_IDF, # ESP32-S3 IDF
Platform.BK72XX_ARD, # LibreTiny BK7231N
Platform.RTL87XX_ARD, # LibreTiny RTL8720x
Platform.LN882X_ARD, # LibreTiny LN882x
Platform.RP2040_ARD, # Raspberry Pi Pico
Platform.NRF52_ZEPHYR, # Nordic nRF52 (Zephyr)
]
@@ -418,8 +411,6 @@ def _detect_platform_hint_from_filename(filename: str) -> Platform | None:
- wifi_component_esp8266.cpp, *_esp8266.h -> ESP8266_ARD
- *_esp32*.cpp -> ESP32 IDF (generic)
- *_libretiny.cpp, *_bk72*.* -> BK72XX (LibreTiny)
- *_rtl87*.* -> RTL87XX (LibreTiny Realtek)
- *_ln882*.* -> LN882X (LibreTiny Lightning)
- *_pico.cpp, *_rp2040.* -> RP2040_ARD
Args:
@@ -453,12 +444,7 @@ def _detect_platform_hint_from_filename(filename: str) -> Platform | None:
if "esp32" in filename_lower:
return Platform.ESP32_IDF
# LibreTiny platforms (check specific variants before generic libretiny)
# Check specific variants first to handle paths like libretiny/wifi_rtl87xx.cpp
if "rtl87" in filename_lower:
return Platform.RTL87XX_ARD
if "ln882" in filename_lower:
return Platform.LN882X_ARD
# LibreTiny (via 'libretiny' pattern or BK72xx-specific files)
if "libretiny" in filename_lower or "bk72" in filename_lower:
return Platform.BK72XX_ARD
@@ -466,10 +452,6 @@ def _detect_platform_hint_from_filename(filename: str) -> Platform | None:
if "pico" in filename_lower or "rp2040" in filename_lower:
return Platform.RP2040_ARD
# nRF52 / Zephyr
if "nrf52" in filename_lower or "zephyr" in filename_lower:
return Platform.NRF52_ZEPHYR
return None

View File

@@ -11,8 +11,6 @@ sensor:
- platform: debug
free:
name: "Heap Free"
block:
name: "Heap Block"
loop_time:
name: "Loop Time"
cpu_frequency:

View File

@@ -1,6 +1 @@
<<: !include common.yaml
sensor:
- platform: debug
min_free:
name: "Heap Min Free"

View File

@@ -2,10 +2,3 @@
esp32:
cpu_frequency: 240MHz
sensor:
- platform: debug
fragmentation:
name: "Heap Fragmentation"
min_free:
name: "Heap Min Free"

View File

@@ -9,9 +9,5 @@ sensor:
name: "Heap Free"
psram:
name: "Free PSRAM"
fragmentation:
name: "Heap Fragmentation"
min_free:
name: "Heap Min Free"
psram:

View File

@@ -1,8 +1 @@
<<: !include common.yaml
sensor:
- platform: debug
fragmentation:
name: "Heap Fragmentation"
min_free:
name: "Heap Min Free"

View File

@@ -1,6 +1 @@
<<: !include common.yaml
sensor:
- platform: debug
fragmentation:
name: "Heap Fragmentation"

View File

@@ -1,6 +1 @@
<<: !include common.yaml
sensor:
- platform: debug
min_free:
name: "Heap Min Free"

View File

@@ -1,6 +0,0 @@
<<: !include common.yaml
sensor:
- platform: debug
min_free:
name: "Heap Min Free"

View File

@@ -1472,24 +1472,6 @@ def test_detect_memory_impact_config_runs_at_component_limit(tmp_path: Path) ->
determine_jobs.Platform.BK72XX_ARD,
),
("esphome/components/ble/ble_bk72xx.cpp", determine_jobs.Platform.BK72XX_ARD),
# RTL87xx (LibreTiny Realtek) detection
(
"tests/components/logger/test.rtl87xx-ard.yaml",
determine_jobs.Platform.RTL87XX_ARD,
),
(
"esphome/components/libretiny/wifi_rtl87xx.cpp",
determine_jobs.Platform.RTL87XX_ARD,
),
# LN882x (LibreTiny Lightning) detection
(
"tests/components/logger/test.ln882x-ard.yaml",
determine_jobs.Platform.LN882X_ARD,
),
(
"esphome/components/libretiny/wifi_ln882x.cpp",
determine_jobs.Platform.LN882X_ARD,
),
# RP2040 / Raspberry Pi Pico detection
("esphome/components/gpio/gpio_rp2040.cpp", determine_jobs.Platform.RP2040_ARD),
("esphome/components/wifi/wifi_rp2040.cpp", determine_jobs.Platform.RP2040_ARD),
@@ -1499,23 +1481,6 @@ def test_detect_memory_impact_config_runs_at_component_limit(tmp_path: Path) ->
"tests/components/rp2040/test.rp2040-ard.yaml",
determine_jobs.Platform.RP2040_ARD,
),
# nRF52 / Zephyr detection
(
"tests/components/logger/test.nrf52-adafruit.yaml",
determine_jobs.Platform.NRF52_ZEPHYR,
),
(
"esphome/components/nrf52/gpio.cpp",
determine_jobs.Platform.NRF52_ZEPHYR,
),
(
"esphome/components/zephyr/core.cpp",
determine_jobs.Platform.NRF52_ZEPHYR,
),
(
"esphome/components/zephyr_ble_server/ble_server.cpp",
determine_jobs.Platform.NRF52_ZEPHYR,
),
# No platform hint (generic files)
("esphome/components/wifi/wifi.cpp", None),
("esphome/components/sensor/sensor.h", None),
@@ -1536,19 +1501,11 @@ def test_detect_memory_impact_config_runs_at_component_limit(tmp_path: Path) ->
"esp32_in_name",
"libretiny",
"bk72xx",
"rtl87xx_test_yaml",
"rtl87xx_wifi",
"ln882x_test_yaml",
"ln882x_wifi",
"rp2040_gpio",
"rp2040_wifi",
"pico_i2c",
"pico_spi",
"rp2040_test_yaml",
"nrf52_test_yaml",
"nrf52_gpio",
"zephyr_core",
"zephyr_ble_server",
"generic_wifi_no_hint",
"generic_sensor_no_hint",
"core_helpers_no_hint",
@@ -1575,11 +1532,6 @@ def test_detect_platform_hint_from_filename(
("file_ESP8266.cpp", determine_jobs.Platform.ESP8266_ARD),
# ESP32 with different cases
("file_ESP32.cpp", determine_jobs.Platform.ESP32_IDF),
# nRF52/Zephyr with different cases
("file_NRF52.cpp", determine_jobs.Platform.NRF52_ZEPHYR),
("file_Nrf52.cpp", determine_jobs.Platform.NRF52_ZEPHYR),
("file_ZEPHYR.cpp", determine_jobs.Platform.NRF52_ZEPHYR),
("file_Zephyr.cpp", determine_jobs.Platform.NRF52_ZEPHYR),
],
ids=[
"rp2040_uppercase",
@@ -1588,10 +1540,6 @@ def test_detect_platform_hint_from_filename(
"pico_titlecase",
"esp8266_uppercase",
"esp32_uppercase",
"nrf52_uppercase",
"nrf52_mixedcase",
"zephyr_uppercase",
"zephyr_titlecase",
],
)
def test_detect_platform_hint_from_filename_case_insensitive(