Compare commits

..

43 Commits

Author SHA1 Message Date
J. Nick Koston
ab5037671f still de-dupe 2025-11-12 17:34:34 -06:00
J. Nick Koston
97ac7a6d9b still de-dupe 2025-11-12 17:21:12 -06:00
J. Nick Koston
62aece0f90 still de-dupe 2025-11-12 17:18:09 -06:00
J. Nick Koston
5c1ffccb64 still de-dupe 2025-11-12 17:17:23 -06:00
J. Nick Koston
1cec97008b still de-dupe 2025-11-12 17:15:14 -06:00
J. Nick Koston
078458fea9 still de-dupe 2025-11-12 17:10:27 -06:00
J. Nick Koston
0883561d5e still de-dupe 2025-11-12 17:08:03 -06:00
J. Nick Koston
5f1f1dfeb1 maybe resolution is a better indicator 2025-11-12 17:03:40 -06:00
J. Nick Koston
70bbb47778 maybe resolution is a better indicator 2025-11-12 16:56:16 -06:00
J. Nick Koston
7a4db1ddff maybe resolution is a better indicator 2025-11-12 16:43:10 -06:00
J. Nick Koston
b0120bd008 [ld2450] Prevent ghost readings by forcing zero when no target (addresses #10624) 2025-11-12 16:08:42 -06:00
J. Nick Koston
29a50da635 [wifi] Use stack allocation for BSSID formatting in logging (#11859) 2025-11-12 14:27:06 -06:00
J. Nick Koston
5f0fa68d73 [esp32_ble] Use stack allocation for MAC formatting in dump_config (#11860) 2025-11-12 14:26:57 -06:00
J. Nick Koston
2f39b10baa [esp32_ble_tracker] Use initializer_list to eliminate compiler warning and reduce flash usage (#11861) 2025-11-12 14:26:46 -06:00
J. Nick Koston
5a550cc579 [api] Eliminate heap allocations when transmitting Event types (#11773) 2025-11-12 14:26:36 -06:00
J. Nick Koston
4b58cb4ce6 [wifi] Pass ManualIP by const reference to reduce stack usage (#11858) 2025-11-12 14:01:19 -06:00
J. Nick Koston
3872a2fd91 [captive_portal] Warn when enabled without WiFi AP configured (#11856) 2025-11-12 14:01:07 -06:00
dependabot[bot]
5d613ada83 Bump pytest from 9.0.0 to 9.0.1 (#11874)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-12 14:00:50 -06:00
J. Nick Koston
9de80b635a [core] Fix wait_until hanging when used in on_boot automations (#11869) 2025-11-12 17:56:19 +00:00
Jonathan Swoboda
748aee584a [esp32] Update the recommended platform to 55.03.31-2 (#11865) 2025-11-12 10:41:22 -05:00
Jonathan Swoboda
3cbfddcc83 Merge branch 'beta' into dev 2025-11-11 23:27:24 -05:00
Jonathan Swoboda
1d71b6b93e Merge pull request #11862 from esphome/bump-2025.11.0b1
2025.11.0b1
2025-11-11 23:27:12 -05:00
J. Nick Koston
398dba4fc8 [ci] Reduce release time by removing 21 redundant ESP32-S3 IDF tests (#11850) 2025-11-12 16:44:19 +13:00
Jonathan Swoboda
298813d4fa Bump version to 2025.11.0b1 2025-11-11 22:14:22 -05:00
Jonathan Swoboda
56d141c741 Merge branch 'release' into dev 2025-11-11 20:09:55 -05:00
Jonathan Swoboda
47a7f729dd Merge pull request #11857 from esphome/bump-2025.10.5
2025.10.5
2025-11-11 20:09:41 -05:00
Jonathan Swoboda
7806eb980f Bump version to 2025.12.0-dev 2025-11-11 19:50:47 -05:00
Jonathan Swoboda
a59888224c Bump version to 2025.10.5 2025-11-11 19:44:37 -05:00
Clyde Stubbs
58ad4759f0 [lvgl] Fix rotation with unusual width (#11680) 2025-11-11 19:44:37 -05:00
Clyde Stubbs
87f79290ba [usb_uart] Fixes for transfer queue allocation (#11548) 2025-11-11 19:44:37 -05:00
Jonathan Swoboda
9326d78439 [core] Don't allow python 3.14 (#11527) 2025-11-11 19:44:37 -05:00
Stuart Parmenter
a93887a790 [const] Add CONF_ROWS (#11249) 2025-11-11 19:44:37 -05:00
Kevin Ahrendt
d7fa131a8a [network, psram, speaker wifi] Use CORE.data to enable high performance networking (#11812) 2025-11-11 18:43:06 -06:00
J. Nick Koston
79a4444928 [wifi] Conditionally compile manual_ip to save 24-72 bytes RAM (#11833) 2025-11-11 23:27:08 +00:00
J. Nick Koston
572fae5c7d [wifi] Restore two-attempt BSSID filtering for mesh networks (#11844) 2025-11-12 12:12:53 +13:00
J. Nick Koston
5dafaaced4 [wifi] Fix scan and connection failures after adapter restart (#11851)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-11-11 23:12:10 +00:00
J. Nick Koston
65a303d48f [wifi] Add min_auth_mode configuration option (#11814) 2025-11-11 16:39:55 -06:00
J. Nick Koston
00c71b7236 [wifi] Fix all-hidden networks duplicate attempts and scan skipping (#11848)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-11-11 22:33:37 +00:00
J. Nick Koston
ef04903a7a [wifi] Change priority type from float to int8_t (#11830) 2025-11-12 11:10:17 +13:00
J. Nick Koston
a2ec7f622c [wifi] Fix infinite retry loop when no hidden networks and captive portal active (#11831) 2025-11-11 16:04:37 -06:00
tomaszduda23
2f91e7bd47 [nrf52] fix boot loop (#11854) 2025-11-11 15:33:53 -06:00
tomaszduda23
80a7c6d3c3 [nrf52,debug] add partition dump (#11839)
Co-authored-by: J. Nick Koston <nick+github@koston.org>
2025-11-11 14:52:41 -06:00
CzBiX
7a92565a0c [lvgl] Fix compile when using transform_zoom (#11845) 2025-11-12 06:24:52 +11:00
83 changed files with 724 additions and 730 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 = 2025.11.0-dev
PROJECT_NUMBER = 2025.11.0b1
# 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

@@ -741,13 +741,6 @@ def command_vscode(args: ArgsProtocol) -> int | None:
def command_compile(args: ArgsProtocol, config: ConfigType) -> int | None:
# Set memory analysis options in config
if args.analyze_memory:
config.setdefault(CONF_ESPHOME, {})["analyze_memory"] = True
if args.memory_report:
config.setdefault(CONF_ESPHOME, {})["memory_report_file"] = args.memory_report
exit_code = write_cpp(config)
if exit_code != 0:
return exit_code
@@ -1209,17 +1202,6 @@ def parse_args(argv):
help="Only generate source code, do not compile.",
action="store_true",
)
parser_compile.add_argument(
"--analyze-memory",
help="Analyze and display memory usage by component after compilation.",
action="store_true",
)
parser_compile.add_argument(
"--memory-report",
help="Save memory analysis report to a file (supports .json or .txt).",
type=str,
metavar="FILE",
)
parser_upload = subparsers.add_parser(
"upload",

View File

@@ -1,7 +1,6 @@
"""CLI interface for memory analysis with report generation."""
from collections import defaultdict
import json
import sys
from . import (
@@ -284,28 +283,6 @@ class MemoryAnalyzerCLI(MemoryAnalyzer):
return "\n".join(lines)
def to_json(self) -> str:
"""Export analysis results as JSON."""
data = {
"components": {
name: {
"text": mem.text_size,
"rodata": mem.rodata_size,
"data": mem.data_size,
"bss": mem.bss_size,
"flash_total": mem.flash_total,
"ram_total": mem.ram_total,
"symbol_count": mem.symbol_count,
}
for name, mem in self.components.items()
},
"totals": {
"flash": sum(c.flash_total for c in self.components.values()),
"ram": sum(c.ram_total for c in self.components.values()),
},
}
return json.dumps(data, indent=2)
def dump_uncategorized_symbols(self, output_file: str | None = None) -> None:
"""Dump uncategorized symbols for analysis."""
# Sort by size descending

View File

@@ -122,19 +122,16 @@ template<typename... Ts> class BLEClientWriteAction : public Action<Ts...>, publ
void play_complex(const Ts &...x) override {
this->num_running_++;
this->var_ = std::make_tuple(x...);
bool result;
std::vector<uint8_t> value;
if (this->len_ >= 0) {
// Static mode: write directly from flash pointer
result = this->write(this->value_.data, this->len_);
// Static mode: copy from flash to vector
value.assign(this->value_.data, this->value_.data + this->len_);
} else {
// Template mode: call function and write the vector
std::vector<uint8_t> value = this->value_.func(x...);
result = this->write(value);
// Template mode: call function
value = this->value_.func(x...);
}
// on write failure, continue the automation chain rather than stopping so that e.g. disconnect can work.
if (!result)
if (!write(value))
this->play_next_(x...);
}
@@ -147,15 +144,15 @@ template<typename... Ts> class BLEClientWriteAction : public Action<Ts...>, publ
* errors.
*/
// initiate the write. Return true if all went well, will be followed by a WRITE_CHAR event.
bool write(const uint8_t *data, size_t len) {
bool write(const std::vector<uint8_t> &value) {
if (this->node_state != espbt::ClientState::ESTABLISHED) {
esph_log_w(Automation::TAG, "Cannot write to BLE characteristic - not connected");
return false;
}
esph_log_vv(Automation::TAG, "Will write %d bytes: %s", len, format_hex_pretty(data, len).c_str());
esp_err_t err =
esp_ble_gattc_write_char(this->parent()->get_gattc_if(), this->parent()->get_conn_id(), this->char_handle_, len,
const_cast<uint8_t *>(data), this->write_type_, ESP_GATT_AUTH_REQ_NONE);
esph_log_vv(Automation::TAG, "Will write %d bytes: %s", value.size(), format_hex_pretty(value).c_str());
esp_err_t err = esp_ble_gattc_write_char(this->parent()->get_gattc_if(), this->parent()->get_conn_id(),
this->char_handle_, value.size(), const_cast<uint8_t *>(value.data()),
this->write_type_, ESP_GATT_AUTH_REQ_NONE);
if (err != ESP_OK) {
esph_log_e(Automation::TAG, "Error writing to characteristic: %s!", esp_err_to_name(err));
return false;
@@ -163,8 +160,6 @@ template<typename... Ts> class BLEClientWriteAction : public Action<Ts...>, publ
return true;
}
bool write(const std::vector<uint8_t> &value) { return this->write(value.data(), value.size()); }
void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,
esp_ble_gattc_cb_param_t *param) override {
switch (event) {

View File

@@ -1,9 +1,12 @@
import logging
import esphome.codegen as cg
from esphome.components import web_server_base
from esphome.components.web_server_base import CONF_WEB_SERVER_BASE_ID
from esphome.config_helpers import filter_source_files_from_platform
import esphome.config_validation as cv
from esphome.const import (
CONF_AP,
CONF_ID,
PLATFORM_BK72XX,
PLATFORM_ESP32,
@@ -14,6 +17,10 @@ from esphome.const import (
)
from esphome.core import CORE, coroutine_with_priority
from esphome.coroutine import CoroPriority
import esphome.final_validate as fv
from esphome.types import ConfigType
_LOGGER = logging.getLogger(__name__)
def AUTO_LOAD() -> list[str]:
@@ -50,6 +57,27 @@ CONFIG_SCHEMA = cv.All(
)
def _final_validate(config: ConfigType) -> ConfigType:
full_config = fv.full_config.get()
wifi_conf = full_config.get("wifi")
if wifi_conf is None:
# This shouldn't happen due to DEPENDENCIES = ["wifi"], but check anyway
raise cv.Invalid("Captive portal requires the wifi component to be configured")
if CONF_AP not in wifi_conf:
_LOGGER.warning(
"Captive portal is enabled but no WiFi AP is configured. "
"The captive portal will not be accessible. "
"Add 'ap:' to your WiFi configuration to enable the captive portal."
)
return config
FINAL_VALIDATE_SCHEMA = _final_validate
@coroutine_with_priority(CoroPriority.CAPTIVE_PORTAL)
async def to_code(config):
paren = await cg.get_variable(config[CONF_WEB_SERVER_BASE_ID])

View File

@@ -49,9 +49,9 @@ void DebugComponent::dump_config() {
}
#endif // USE_TEXT_SENSOR
#ifdef USE_ESP32
this->log_partition_info_(); // Log partition information for ESP32
#endif // USE_ESP32
#if defined(USE_ESP32) || defined(USE_ZEPHYR)
this->log_partition_info_(); // Log partition information
#endif
}
void DebugComponent::loop() {

View File

@@ -62,19 +62,19 @@ class DebugComponent : public PollingComponent {
sensor::Sensor *cpu_frequency_sensor_{nullptr};
#endif // USE_SENSOR
#ifdef USE_ESP32
#if defined(USE_ESP32) || defined(USE_ZEPHYR)
/**
* @brief Logs information about the device's partition table.
*
* This function iterates through the ESP32's partition table and logs details
* This function iterates through the partition table and logs details
* about each partition, including its name, type, subtype, starting address,
* and size. The information is useful for diagnosing issues related to flash
* memory or verifying the partition configuration dynamically at runtime.
*
* Only available when compiled for ESP32 platforms.
* Only available when compiled for ESP32 and ZEPHYR platforms.
*/
void log_partition_info_();
#endif // USE_ESP32
#endif
#ifdef USE_TEXT_SENSOR
text_sensor::TextSensor *device_info_{nullptr};

View File

@@ -5,6 +5,7 @@
#include <zephyr/drivers/hwinfo.h>
#include <hal/nrf_power.h>
#include <cstdint>
#include <zephyr/storage/flash_map.h>
#define BOOTLOADER_VERSION_REGISTER NRF_TIMER2->CC[0]
@@ -86,6 +87,37 @@ std::string DebugComponent::get_reset_reason_() {
uint32_t DebugComponent::get_free_heap_() { return INT_MAX; }
static void fa_cb(const struct flash_area *fa, void *user_data) {
#if CONFIG_FLASH_MAP_LABELS
const char *fa_label = flash_area_label(fa);
if (fa_label == nullptr) {
fa_label = "-";
}
ESP_LOGCONFIG(TAG, "%2d 0x%0*" PRIxPTR " %-26s %-24.24s 0x%-10x 0x%-12x", (int) fa->fa_id,
sizeof(uintptr_t) * 2, (uintptr_t) fa->fa_dev, fa->fa_dev->name, fa_label, (uint32_t) fa->fa_off,
fa->fa_size);
#else
ESP_LOGCONFIG(TAG, "%2d 0x%0*" PRIxPTR " %-26s 0x%-10x 0x%-12x", (int) fa->fa_id, sizeof(uintptr_t) * 2,
(uintptr_t) fa->fa_dev, fa->fa_dev->name, (uint32_t) fa->fa_off, fa->fa_size);
#endif
}
void DebugComponent::log_partition_info_() {
#if CONFIG_FLASH_MAP_LABELS
ESP_LOGCONFIG(TAG, "ID | Device | Device Name "
"| Label | Offset | Size");
ESP_LOGCONFIG(TAG, "--------------------------------------------"
"-----------------------------------------------");
#else
ESP_LOGCONFIG(TAG, "ID | Device | Device Name "
"| Offset | Size");
ESP_LOGCONFIG(TAG, "-----------------------------------------"
"------------------------------");
#endif
flash_area_foreach(fa_cb, nullptr);
}
void DebugComponent::get_device_info_(std::string &device_info) {
std::string supply = "Main supply status: ";
if (nrf_power_mainregstatus_get(NRF_POWER) == NRF_POWER_MAINREGSTATUS_NORMAL) {

View File

@@ -334,12 +334,14 @@ def _is_framework_url(source: str) -> str:
# - https://github.com/espressif/arduino-esp32/releases
ARDUINO_FRAMEWORK_VERSION_LOOKUP = {
"recommended": cv.Version(3, 3, 2),
"latest": cv.Version(3, 3, 2),
"dev": cv.Version(3, 3, 2),
"latest": cv.Version(3, 3, 4),
"dev": cv.Version(3, 3, 4),
}
ARDUINO_PLATFORM_VERSION_LOOKUP = {
cv.Version(3, 3, 2): cv.Version(55, 3, 31, "1"),
cv.Version(3, 3, 1): cv.Version(55, 3, 31, "1"),
cv.Version(3, 3, 4): cv.Version(55, 3, 31, "2"),
cv.Version(3, 3, 3): cv.Version(55, 3, 31, "2"),
cv.Version(3, 3, 2): cv.Version(55, 3, 31, "2"),
cv.Version(3, 3, 1): cv.Version(55, 3, 31, "2"),
cv.Version(3, 3, 0): cv.Version(55, 3, 30, "2"),
cv.Version(3, 2, 1): cv.Version(54, 3, 21, "2"),
cv.Version(3, 2, 0): cv.Version(54, 3, 20),
@@ -357,8 +359,8 @@ ESP_IDF_FRAMEWORK_VERSION_LOOKUP = {
"dev": cv.Version(5, 5, 1),
}
ESP_IDF_PLATFORM_VERSION_LOOKUP = {
cv.Version(5, 5, 1): cv.Version(55, 3, 31, "1"),
cv.Version(5, 5, 0): cv.Version(55, 3, 31, "1"),
cv.Version(5, 5, 1): cv.Version(55, 3, 31, "2"),
cv.Version(5, 5, 0): cv.Version(55, 3, 31, "2"),
cv.Version(5, 4, 3): cv.Version(55, 3, 32),
cv.Version(5, 4, 2): cv.Version(54, 3, 21, "2"),
cv.Version(5, 4, 1): cv.Version(54, 3, 21, "2"),
@@ -373,9 +375,9 @@ ESP_IDF_PLATFORM_VERSION_LOOKUP = {
# The platform-espressif32 version
# - https://github.com/pioarduino/platform-espressif32/releases
PLATFORM_VERSION_LOOKUP = {
"recommended": cv.Version(55, 3, 31, "1"),
"latest": cv.Version(55, 3, 31, "1"),
"dev": cv.Version(55, 3, 31, "1"),
"recommended": cv.Version(55, 3, 31, "2"),
"latest": cv.Version(55, 3, 31, "2"),
"dev": cv.Version(55, 3, 31, "2"),
}

View File

@@ -96,10 +96,6 @@ void ESP32BLE::advertising_set_service_data(const std::vector<uint8_t> &data) {
}
void ESP32BLE::advertising_set_manufacturer_data(const std::vector<uint8_t> &data) {
this->advertising_set_manufacturer_data(std::span<const uint8_t>(data));
}
void ESP32BLE::advertising_set_manufacturer_data(std::span<const uint8_t> data) {
this->advertising_init_();
this->advertising_->set_manufacturer_data(data);
this->advertising_start();
@@ -638,11 +634,13 @@ void ESP32BLE::dump_config() {
io_capability_s = "invalid";
break;
}
char mac_s[18];
format_mac_addr_upper(mac_address, mac_s);
ESP_LOGCONFIG(TAG,
"BLE:\n"
" MAC address: %s\n"
" IO Capability: %s",
format_mac_address_pretty(mac_address).c_str(), io_capability_s);
mac_s, io_capability_s);
} else {
ESP_LOGCONFIG(TAG, "Bluetooth stack is not enabled");
}

View File

@@ -118,7 +118,6 @@ class ESP32BLE : public Component {
void advertising_start();
void advertising_set_service_data(const std::vector<uint8_t> &data);
void advertising_set_manufacturer_data(const std::vector<uint8_t> &data);
void advertising_set_manufacturer_data(std::span<const uint8_t> data);
void advertising_set_appearance(uint16_t appearance) { this->appearance_ = appearance; }
void advertising_set_service_data_and_name(std::span<const uint8_t> data, bool include_name);
void advertising_add_service_uuid(ESPBTUUID uuid);

View File

@@ -59,10 +59,6 @@ void BLEAdvertising::set_service_data(const std::vector<uint8_t> &data) {
}
void BLEAdvertising::set_manufacturer_data(const std::vector<uint8_t> &data) {
this->set_manufacturer_data(std::span<const uint8_t>(data));
}
void BLEAdvertising::set_manufacturer_data(std::span<const uint8_t> data) {
delete[] this->advertising_data_.p_manufacturer_data;
this->advertising_data_.p_manufacturer_data = nullptr;
this->advertising_data_.manufacturer_len = data.size();

View File

@@ -37,7 +37,6 @@ class BLEAdvertising {
void set_scan_response(bool scan_response) { this->scan_response_ = scan_response; }
void set_min_preferred_interval(uint16_t interval) { this->advertising_data_.min_interval = interval; }
void set_manufacturer_data(const std::vector<uint8_t> &data);
void set_manufacturer_data(std::span<const uint8_t> data);
void set_appearance(uint16_t appearance) { this->advertising_data_.appearance = appearance; }
void set_service_data(const std::vector<uint8_t> &data);
void set_service_data(std::span<const uint8_t> data);

View File

@@ -1,6 +1,5 @@
#include "esp32_ble_beacon.h"
#include "esphome/core/log.h"
#include "esphome/core/helpers.h"
#ifdef USE_ESP32

View File

@@ -15,10 +15,7 @@ Trigger<std::vector<uint8_t>, uint16_t> *BLETriggers::create_characteristic_on_w
Trigger<std::vector<uint8_t>, uint16_t> *on_write_trigger = // NOLINT(cppcoreguidelines-owning-memory)
new Trigger<std::vector<uint8_t>, uint16_t>();
characteristic->on_write([on_write_trigger](std::span<const uint8_t> data, uint16_t id) {
// Convert span to vector for trigger - copy is necessary because:
// 1. Trigger stores the data for use in automation actions that execute later
// 2. The span is only valid during this callback (points to temporary BLE stack data)
// 3. User lambdas in automations need persistent data they can access asynchronously
// Convert span to vector for trigger
on_write_trigger->trigger(std::vector<uint8_t>(data.begin(), data.end()), id);
});
return on_write_trigger;
@@ -30,10 +27,7 @@ Trigger<std::vector<uint8_t>, uint16_t> *BLETriggers::create_descriptor_on_write
Trigger<std::vector<uint8_t>, uint16_t> *on_write_trigger = // NOLINT(cppcoreguidelines-owning-memory)
new Trigger<std::vector<uint8_t>, uint16_t>();
descriptor->on_write([on_write_trigger](std::span<const uint8_t> data, uint16_t id) {
// Convert span to vector for trigger - copy is necessary because:
// 1. Trigger stores the data for use in automation actions that execute later
// 2. The span is only valid during this callback (points to temporary BLE stack data)
// 3. User lambdas in automations need persistent data they can access asynchronously
// Convert span to vector for trigger
on_write_trigger->trigger(std::vector<uint8_t>(data.begin(), data.end()), id);
});
return on_write_trigger;

View File

@@ -10,7 +10,7 @@ namespace esphome::esp32_ble_tracker {
class ESPBTAdvertiseTrigger : public Trigger<const ESPBTDevice &>, public ESPBTDeviceListener {
public:
explicit ESPBTAdvertiseTrigger(ESP32BLETracker *parent) { parent->register_listener(this); }
void set_addresses(const std::vector<uint64_t> &addresses) { this->address_vec_ = addresses; }
void set_addresses(std::initializer_list<uint64_t> addresses) { this->address_vec_ = addresses; }
bool parse_device(const ESPBTDevice &device) override {
uint64_t u64_addr = device.address_uint64();

View File

@@ -336,7 +336,7 @@ void ESP32ImprovComponent::process_incoming_data_() {
this->connecting_sta_ = sta;
wifi::global_wifi_component->set_sta(sta);
wifi::global_wifi_component->start_connecting(sta, false);
wifi::global_wifi_component->start_connecting(sta);
this->set_state_(improv::STATE_PROVISIONING);
ESP_LOGD(TAG, "Received Improv Wi-Fi settings ssid=%s, password=" LOG_SECRET("%s"), command.ssid.c_str(),
command.password.c_str());

View File

@@ -383,7 +383,6 @@ async def to_code(config):
cg.add(var.set_use_address(config[CONF_USE_ADDRESS]))
if CONF_MANUAL_IP in config:
cg.add_define("USE_ETHERNET_MANUAL_IP")
cg.add(var.set_manual_ip(manual_ip(config[CONF_MANUAL_IP])))
# Add compile-time define for PHY types with specific code

View File

@@ -550,14 +550,11 @@ void EthernetComponent::start_connect_() {
}
esp_netif_ip_info_t info;
#ifdef USE_ETHERNET_MANUAL_IP
if (this->manual_ip_.has_value()) {
info.ip = this->manual_ip_->static_ip;
info.gw = this->manual_ip_->gateway;
info.netmask = this->manual_ip_->subnet;
} else
#endif
{
} else {
info.ip.addr = 0;
info.gw.addr = 0;
info.netmask.addr = 0;
@@ -578,7 +575,6 @@ void EthernetComponent::start_connect_() {
err = esp_netif_set_ip_info(this->eth_netif_, &info);
ESPHL_ERROR_CHECK(err, "DHCPC set IP info error");
#ifdef USE_ETHERNET_MANUAL_IP
if (this->manual_ip_.has_value()) {
LwIPLock lock;
if (this->manual_ip_->dns1.is_set()) {
@@ -591,9 +587,7 @@ void EthernetComponent::start_connect_() {
d = this->manual_ip_->dns2;
dns_setserver(1, &d);
}
} else
#endif
{
} else {
err = esp_netif_dhcpc_start(this->eth_netif_);
if (err != ESP_ERR_ESP_NETIF_DHCP_ALREADY_STARTED) {
ESPHL_ERROR_CHECK(err, "DHCPC start error");
@@ -691,9 +685,7 @@ void EthernetComponent::set_clk_mode(emac_rmii_clock_mode_t clk_mode) { this->cl
void EthernetComponent::add_phy_register(PHYRegister register_value) { this->phy_registers_.push_back(register_value); }
#endif
void EthernetComponent::set_type(EthernetType type) { this->type_ = type; }
#ifdef USE_ETHERNET_MANUAL_IP
void EthernetComponent::set_manual_ip(const ManualIP &manual_ip) { this->manual_ip_ = manual_ip; }
#endif
// set_use_address() is guaranteed to be called during component setup by Python code generation,
// so use_address_ will always be valid when get_use_address() is called - no fallback needed.

View File

@@ -82,9 +82,7 @@ class EthernetComponent : public Component {
void add_phy_register(PHYRegister register_value);
#endif
void set_type(EthernetType type);
#ifdef USE_ETHERNET_MANUAL_IP
void set_manual_ip(const ManualIP &manual_ip);
#endif
void set_fixed_mac(const std::array<uint8_t, 6> &mac) { this->fixed_mac_ = mac; }
network::IPAddresses get_ip_addresses();
@@ -139,9 +137,7 @@ class EthernetComponent : public Component {
uint8_t mdc_pin_{23};
uint8_t mdio_pin_{18};
#endif
#ifdef USE_ETHERNET_MANUAL_IP
optional<ManualIP> manual_ip_{};
#endif
uint32_t connect_begin_;
// Group all uint8_t types together (enums and bools)

View File

@@ -107,7 +107,7 @@ void IDFI2CBus::dump_config() {
if (s.second) {
ESP_LOGCONFIG(TAG, "Found device at address 0x%02X", s.first);
} else {
ESP_LOGCONFIG(TAG, "Unknown error at address 0x%02X", s.first);
ESP_LOGE(TAG, "Unknown error at address 0x%02X", s.first);
}
}
}

View File

@@ -231,7 +231,7 @@ bool ImprovSerialComponent::parse_improv_payload_(improv::ImprovCommand &command
this->connecting_sta_ = sta;
wifi::global_wifi_component->set_sta(sta);
wifi::global_wifi_component->start_connecting(sta, false);
wifi::global_wifi_component->start_connecting(sta);
this->set_state_(improv::STATE_PROVISIONING);
ESP_LOGD(TAG, "Received settings: SSID=%s, password=" LOG_SECRET("%s"), command.ssid.c_str(),
command.password.c_str());

View File

@@ -181,6 +181,17 @@ static inline bool validate_header_footer(const uint8_t *header_footer, const ui
return std::memcmp(header_footer, buffer, HEADER_FOOTER_SIZE) == 0;
}
// Helper function to calculate direction based on speed
static inline Direction calculate_direction(int16_t speed) {
if (speed > 0) {
return DIRECTION_MOVING_AWAY;
}
if (speed < 0) {
return DIRECTION_APPROACHING;
}
return DIRECTION_STATIONARY;
}
void LD2450Component::setup() {
#ifdef USE_NUMBER
if (this->presence_timeout_number_ != nullptr) {
@@ -293,6 +304,51 @@ uint8_t LD2450Component::count_targets_in_zone_(const Zone &zone, bool is_moving
return count;
}
// Store target info for zone target counting
void LD2450Component::set_target_info_(uint8_t index, int16_t x, int16_t y, bool is_moving) {
this->target_info_[index].x = x;
this->target_info_[index].y = y;
this->target_info_[index].is_moving = is_moving;
}
#ifdef USE_TEXT_SENSOR
// Publish direction text sensor with deduplication
void LD2450Component::publish_direction_(uint8_t index, Direction direction) {
text_sensor::TextSensor *sensor = this->direction_text_sensors_[index];
if (sensor == nullptr) {
return;
}
const auto *dir_str = find_str(ld2450::DIRECTION_BY_UINT, direction);
if (!sensor->has_state() || sensor->get_state() != dir_str) {
sensor->publish_state(dir_str);
}
}
#endif
#ifdef USE_SENSOR
// Publish all target sensor values with deduplication
void LD2450Component::publish_target_sensors_(uint8_t index, int16_t x, int16_t y, uint16_t resolution, int16_t speed,
uint16_t distance, float angle) {
SAFE_PUBLISH_SENSOR(this->move_x_sensors_[index], x);
SAFE_PUBLISH_SENSOR(this->move_y_sensors_[index], y);
SAFE_PUBLISH_SENSOR(this->move_resolution_sensors_[index], resolution);
SAFE_PUBLISH_SENSOR(this->move_speed_sensors_[index], speed);
SAFE_PUBLISH_SENSOR(this->move_distance_sensors_[index], distance);
SAFE_PUBLISH_SENSOR(this->move_angle_sensors_[index], angle);
}
// Force clear all target sensor values to 0, bypassing throttle but still using deduplication
void LD2450Component::force_clear_target_sensors_(uint8_t index) {
// Use publish_state() to bypass throttle filters, with manual dedup check
SAFE_PUBLISH_SENSOR_WITHOUT_FILTERS(this->move_x_sensors_[index], 0);
SAFE_PUBLISH_SENSOR_WITHOUT_FILTERS(this->move_y_sensors_[index], 0);
SAFE_PUBLISH_SENSOR_WITHOUT_FILTERS(this->move_resolution_sensors_[index], 0);
SAFE_PUBLISH_SENSOR_WITHOUT_FILTERS(this->move_speed_sensors_[index], 0);
SAFE_PUBLISH_SENSOR_WITHOUT_FILTERS(this->move_distance_sensors_[index], 0);
SAFE_PUBLISH_SENSOR_WITHOUT_FILTERS(this->move_angle_sensors_[index], 0);
}
#endif
// Service reset_radar_zone
void LD2450Component::reset_radar_zone() {
this->zone_type_ = 0;
@@ -444,13 +500,13 @@ void LD2450Component::handle_periodic_data_() {
int16_t target_count = 0;
int16_t still_target_count = 0;
int16_t moving_target_count = 0;
int16_t res = 0;
int16_t start = 0;
int16_t tx = 0;
int16_t ty = 0;
int16_t td = 0;
int16_t ts = 0;
int16_t angle = 0;
int16_t res = 0; // Target resolution in mm
int16_t start = 0; // Buffer offset for current data field
int16_t tx = 0; // Target X coordinate in mm
int16_t ty = 0; // Target Y coordinate in mm
int16_t td = 0; // Target distance in mm (calculated from tx and ty)
int16_t ts = 0; // Target speed in mm/s
int16_t angle = 0; // Target angle in degrees
uint8_t index = 0;
Direction direction{DIRECTION_UNDEFINED};
bool is_moving = false;
@@ -464,15 +520,12 @@ void LD2450Component::handle_periodic_data_() {
is_moving = false;
// tx is used for further calculations, so always needs to be populated
tx = ld2450::decode_coordinate(this->buffer_data_[start], this->buffer_data_[start + 1]);
SAFE_PUBLISH_SENSOR(this->move_x_sensors_[index], tx);
// Y
start = TARGET_Y + index * 8;
ty = ld2450::decode_coordinate(this->buffer_data_[start], this->buffer_data_[start + 1]);
SAFE_PUBLISH_SENSOR(this->move_y_sensors_[index], ty);
// RESOLUTION
start = TARGET_RESOLUTION + index * 8;
res = (this->buffer_data_[start + 1] << 8) | this->buffer_data_[start];
SAFE_PUBLISH_SENSOR(this->move_resolution_sensors_[index], res);
#endif
// SPEED
start = TARGET_SPEED + index * 8;
@@ -481,48 +534,43 @@ void LD2450Component::handle_periodic_data_() {
is_moving = true;
moving_target_count++;
}
#ifdef USE_SENSOR
SAFE_PUBLISH_SENSOR(this->move_speed_sensors_[index], ts);
#endif
// DISTANCE
// DISTANCE (td = target distance in mm, calculated from X and Y coordinates)
// Optimized: use already decoded tx and ty values, replace pow() with multiplication
int32_t x_squared = (int32_t) tx * tx;
int32_t y_squared = (int32_t) ty * ty;
td = (uint16_t) sqrtf(x_squared + y_squared);
if (td > 0) {
td = (uint16_t) sqrtf(x_squared + y_squared); // Pythagorean theorem: distance = sqrt(x² + y²)
// Only publish sensor values when a target is actually detected (resolution > 0)
// The radar sets resolution=0 when no target is present, even if X/Y coordinates
// haven't been cleared from the buffer yet. This prevents stale/ghost readings.
if (res > 0) {
target_count++;
}
#ifdef USE_SENSOR
SAFE_PUBLISH_SENSOR(this->move_distance_sensors_[index], td);
// ANGLE
angle = ld2450::calculate_angle(static_cast<float>(ty), static_cast<float>(td));
if (tx > 0) {
angle = angle * -1;
}
SAFE_PUBLISH_SENSOR(this->move_angle_sensors_[index], angle);
// ANGLE
angle = ld2450::calculate_angle(static_cast<float>(ty), static_cast<float>(td));
if (tx > 0) {
angle = angle * -1;
}
this->publish_target_sensors_(index, tx, ty, res, ts, td, angle);
#endif
#ifdef USE_TEXT_SENSOR
// DIRECTION
if (td == 0) {
direction = DIRECTION_NA;
} else if (ts > 0) {
direction = DIRECTION_MOVING_AWAY;
} else if (ts < 0) {
direction = DIRECTION_APPROACHING;
} else {
direction = DIRECTION_STATIONARY;
}
text_sensor::TextSensor *tsd = this->direction_text_sensors_[index];
const auto *dir_str = find_str(ld2450::DIRECTION_BY_UINT, direction);
if (tsd != nullptr && (!tsd->has_state() || tsd->get_state() != dir_str)) {
tsd->publish_state(dir_str);
}
// DIRECTION
this->publish_direction_(index, calculate_direction(ts));
#endif
// Store target info for zone target count
this->target_info_[index].x = tx;
this->target_info_[index].y = ty;
this->target_info_[index].is_moving = is_moving;
// Store target info for zone target count
this->set_target_info_(index, tx, ty, is_moving);
} else {
// No target detected - force clear all sensor values to 0, bypassing throttle/dedup
#ifdef USE_SENSOR
this->force_clear_target_sensors_(index);
#endif
#ifdef USE_TEXT_SENSOR
// Set direction to NA when no target
this->publish_direction_(index, DIRECTION_NA);
#endif
// Clear target info
this->set_target_info_(index, /* x= */ 0, /* y= */ 0, /* is_moving= */ false);
}
} // End loop thru targets

View File

@@ -159,6 +159,15 @@ class LD2450Component : public Component, public uart::UARTDevice {
float restore_from_flash_();
bool get_timeout_status_(uint32_t check_millis);
uint8_t count_targets_in_zone_(const Zone &zone, bool is_moving);
void set_target_info_(uint8_t index, int16_t x, int16_t y, bool is_moving);
#ifdef USE_SENSOR
void publish_target_sensors_(uint8_t index, int16_t x, int16_t y, uint16_t resolution, int16_t speed,
uint16_t distance, float angle);
void force_clear_target_sensors_(uint8_t index);
#endif
#ifdef USE_TEXT_SENSOR
void publish_direction_(uint8_t index, Direction direction);
#endif
uint32_t presence_millis_ = 0;
uint32_t still_presence_millis_ = 0;

View File

@@ -33,6 +33,13 @@
(sensor)->publish_state_unknown(); \
}
#define SAFE_PUBLISH_SENSOR_WITHOUT_FILTERS(sensor, value) \
if ((sensor) != nullptr && (sensor)->sens->state != static_cast<float>(value)) { \
(sensor)->publish_dedup.next(value); \
(sensor)->sens->raw_state = static_cast<float>(value); \
(sensor)->sens->internal_send_state_to_frontend(static_cast<float>(value)); \
}
#define highbyte(val) (uint8_t)((val) >> 8)
#define lowbyte(val) (uint8_t)((val) &0xff)

View File

@@ -331,7 +331,7 @@ async def to_code(configs):
# This must be done after all widgets are created
for comp in helpers.lvgl_components_required:
cg.add_define(f"USE_LVGL_{comp.upper()}")
if "transform_angle" in styles_used:
if {"transform_angle", "transform_zoom"} & styles_used:
add_define("LV_COLOR_SCREEN_TRANSP", "1")
for use in helpers.lv_uses:
add_define(f"LV_USE_{use.upper()}")

View File

@@ -56,7 +56,7 @@ void MCP23016::pin_mode(uint8_t pin, gpio::Flags flags) {
this->update_reg_(pin, false, iodir);
}
}
float MCP23016::get_setup_priority() const { return setup_priority::IO; }
float MCP23016::get_setup_priority() const { return setup_priority::HARDWARE; }
bool MCP23016::read_reg_(uint8_t reg, uint8_t *value) {
if (this->is_failed())
return false;

View File

@@ -1,7 +1,9 @@
import ipaddress
import logging
import esphome.codegen as cg
from esphome.components.esp32 import add_idf_sdkconfig_option
from esphome.components.psram import is_guaranteed as psram_is_guaranteed
import esphome.config_validation as cv
from esphome.const import CONF_ENABLE_IPV6, CONF_MIN_IPV6_ADDR_COUNT
from esphome.core import CORE, CoroPriority, coroutine_with_priority
@@ -9,6 +11,13 @@ from esphome.core import CORE, CoroPriority, coroutine_with_priority
CODEOWNERS = ["@esphome/core"]
AUTO_LOAD = ["mdns"]
_LOGGER = logging.getLogger(__name__)
# High performance networking tracking infrastructure
# Components can request high performance networking and this configures lwip and WiFi settings
KEY_HIGH_PERFORMANCE_NETWORKING = "high_performance_networking"
CONF_ENABLE_HIGH_PERFORMANCE = "enable_high_performance"
network_ns = cg.esphome_ns.namespace("network")
IPAddress = network_ns.class_("IPAddress")
@@ -47,6 +56,55 @@ def ip_address_literal(ip: str | int | None) -> cg.MockObj:
return IPAddress(str(ip))
def require_high_performance_networking() -> None:
"""Request high performance networking for network and WiFi.
Call this from components that need optimized network performance for streaming
or high-throughput data transfer. This enables high performance mode which
configures both lwip TCP settings and WiFi driver settings for improved
network performance.
Settings applied (ESP-IDF only):
- lwip: Larger TCP buffers, windows, and mailbox sizes
- WiFi: Increased RX/TX buffers, AMPDU aggregation, PSRAM allocation (set by wifi component)
Configuration is PSRAM-aware:
- With PSRAM guaranteed: Aggressive settings (512 RX buffers, 512KB TCP windows)
- Without PSRAM: Conservative optimized settings (64 buffers, 65KB TCP windows)
Example:
from esphome.components import network
def _request_high_performance_networking(config):
network.require_high_performance_networking()
return config
CONFIG_SCHEMA = cv.All(
...,
_request_high_performance_networking,
)
"""
# Only set up once (idempotent - multiple components can call this)
if not CORE.data.get(KEY_HIGH_PERFORMANCE_NETWORKING, False):
CORE.data[KEY_HIGH_PERFORMANCE_NETWORKING] = True
def has_high_performance_networking() -> bool:
"""Check if high performance networking mode is enabled.
Returns True when high performance networking has been requested by a
component or explicitly enabled in the network configuration. This indicates
that lwip and WiFi will use optimized buffer sizes and settings.
This function should be called during code generation (to_code phase) by
components that need to apply performance-related settings.
Returns:
bool: True if high performance networking is enabled, False otherwise
"""
return CORE.data.get(KEY_HIGH_PERFORMANCE_NETWORKING, False)
CONFIG_SCHEMA = cv.Schema(
{
cv.SplitDefault(
@@ -71,6 +129,7 @@ CONFIG_SCHEMA = cv.Schema(
),
),
cv.Optional(CONF_MIN_IPV6_ADDR_COUNT, default=0): cv.positive_int,
cv.Optional(CONF_ENABLE_HIGH_PERFORMANCE): cv.All(cv.boolean, cv.only_on_esp32),
}
)
@@ -80,6 +139,70 @@ async def to_code(config):
cg.add_define("USE_NETWORK")
if CORE.using_arduino and CORE.is_esp32:
cg.add_library("Networking", None)
# Apply high performance networking settings
# Config can explicitly enable/disable, or default to component-driven behavior
enable_high_perf = config.get(CONF_ENABLE_HIGH_PERFORMANCE)
component_requested = CORE.data.get(KEY_HIGH_PERFORMANCE_NETWORKING, False)
# Explicit config overrides component request
should_enable = (
enable_high_perf if enable_high_perf is not None else component_requested
)
# Log when user explicitly disables but a component requested it
if enable_high_perf is False and component_requested:
_LOGGER.info(
"High performance networking disabled by user configuration (overriding component request)"
)
if CORE.is_esp32 and CORE.using_esp_idf and should_enable:
# Check if PSRAM is guaranteed (set by psram component during final validation)
psram_guaranteed = psram_is_guaranteed()
if psram_guaranteed:
_LOGGER.info(
"Applying high-performance lwip settings (PSRAM guaranteed): 512KB TCP windows, 512 mailbox sizes"
)
# PSRAM is guaranteed - use aggressive settings
# Higher maximum values are allowed because CONFIG_LWIP_WND_SCALE is set to true
# CONFIG_LWIP_WND_SCALE can only be enabled if CONFIG_SPIRAM_IGNORE_NOTFOUND isn't set
# Based on https://github.com/espressif/esp-adf/issues/297#issuecomment-783811702
# Enable window scaling for much larger TCP windows
add_idf_sdkconfig_option("CONFIG_LWIP_WND_SCALE", True)
add_idf_sdkconfig_option("CONFIG_LWIP_TCP_RCV_SCALE", 3)
# Large TCP buffers and windows (requires PSRAM)
add_idf_sdkconfig_option("CONFIG_LWIP_TCP_SND_BUF_DEFAULT", 65534)
add_idf_sdkconfig_option("CONFIG_LWIP_TCP_WND_DEFAULT", 512000)
# Large mailboxes for high throughput
add_idf_sdkconfig_option("CONFIG_LWIP_TCPIP_RECVMBOX_SIZE", 512)
add_idf_sdkconfig_option("CONFIG_LWIP_TCP_RECVMBOX_SIZE", 512)
# TCP connection limits
add_idf_sdkconfig_option("CONFIG_LWIP_MAX_ACTIVE_TCP", 16)
add_idf_sdkconfig_option("CONFIG_LWIP_MAX_LISTENING_TCP", 16)
# TCP optimizations
add_idf_sdkconfig_option("CONFIG_LWIP_TCP_MAXRTX", 12)
add_idf_sdkconfig_option("CONFIG_LWIP_TCP_SYNMAXRTX", 6)
add_idf_sdkconfig_option("CONFIG_LWIP_TCP_MSS", 1436)
add_idf_sdkconfig_option("CONFIG_LWIP_TCP_MSL", 60000)
add_idf_sdkconfig_option("CONFIG_LWIP_TCP_OVERSIZE_MSS", True)
add_idf_sdkconfig_option("CONFIG_LWIP_TCP_QUEUE_OOSEQ", True)
else:
_LOGGER.info(
"Applying optimized lwip settings: 65KB TCP windows, 64 mailbox sizes"
)
# PSRAM not guaranteed - use more conservative, but still optimized settings
# Based on https://github.com/espressif/esp-idf/blob/release/v5.4/examples/wifi/iperf/sdkconfig.defaults.esp32
add_idf_sdkconfig_option("CONFIG_LWIP_TCP_SND_BUF_DEFAULT", 65534)
add_idf_sdkconfig_option("CONFIG_LWIP_TCP_WND_DEFAULT", 65534)
add_idf_sdkconfig_option("CONFIG_LWIP_TCP_RECVMBOX_SIZE", 64)
add_idf_sdkconfig_option("CONFIG_LWIP_TCPIP_RECVMBOX_SIZE", 64)
if (enable_ipv6 := config.get(CONF_ENABLE_IPV6, None)) is not None:
cg.add_define("USE_NETWORK_IPV6", enable_ipv6)
if enable_ipv6:

View File

@@ -118,10 +118,10 @@ struct IPAddress {
operator arduino_ns::IPAddress() const { return ip_addr_get_ip4_u32(&ip_addr_); }
#endif
bool is_set() { return !ip_addr_isany(&ip_addr_); } // NOLINT(readability-simplify-boolean-expr)
bool is_ip4() { return IP_IS_V4(&ip_addr_); }
bool is_ip6() { return IP_IS_V6(&ip_addr_); }
bool is_multicast() { return ip_addr_ismulticast(&ip_addr_); }
bool is_set() const { return !ip_addr_isany(&ip_addr_); } // NOLINT(readability-simplify-boolean-expr)
bool is_ip4() const { return IP_IS_V4(&ip_addr_); }
bool is_ip6() const { return IP_IS_V6(&ip_addr_); }
bool is_multicast() const { return ip_addr_ismulticast(&ip_addr_); }
std::string str() const { return str_lower_case(ipaddr_ntoa(&ip_addr_)); }
bool operator==(const IPAddress &other) const { return ip_addr_cmp(&ip_addr_, &other.ip_addr_); }
bool operator!=(const IPAddress &other) const { return !ip_addr_cmp(&ip_addr_, &other.ip_addr_); }

View File

@@ -107,7 +107,6 @@ CONF_REG0 = "reg0"
CONF_UICR_ERASE = "uicr_erase"
VOLTAGE_LEVELS = [1.8, 2.1, 2.4, 2.7, 3.0, 3.3]
DEFAULT_VOLTAGE_LEVEL = "default"
CONFIG_SCHEMA = cv.All(
_detect_bootloader,
@@ -124,12 +123,9 @@ CONFIG_SCHEMA = cv.All(
),
cv.Optional(CONF_REG0): cv.Schema(
{
cv.Required(CONF_VOLTAGE): cv.Any(
cv.All(
cv.voltage,
cv.one_of(*VOLTAGE_LEVELS, float=True),
),
cv.one_of(*[DEFAULT_VOLTAGE_LEVEL], lower=True),
cv.Required(CONF_VOLTAGE): cv.All(
cv.voltage,
cv.one_of(*VOLTAGE_LEVELS, float=True),
),
cv.Optional(CONF_UICR_ERASE, default=False): cv.boolean,
}
@@ -202,9 +198,7 @@ async def to_code(config: ConfigType) -> None:
CORE.add_job(_dfu_to_code, dfu_config)
if reg0_config := config.get(CONF_REG0):
value = 7 # DEFAULT_VOLTAGE_LEVEL
if reg0_config[CONF_VOLTAGE] in VOLTAGE_LEVELS:
value = VOLTAGE_LEVELS.index(reg0_config[CONF_VOLTAGE])
value = VOLTAGE_LEVELS.index(reg0_config[CONF_VOLTAGE])
cg.add_define("USE_NRF52_REG0_VOUT", value)
if reg0_config[CONF_UICR_ERASE]:
cg.add_define("USE_NRF52_UICR_ERASE")

View File

@@ -69,9 +69,20 @@ static StatusFlags fix_bootloader() {
}
#endif
#define BOOTLOADER_VERSION_REGISTER NRF_TIMER2->CC[0]
static StatusFlags set_uicr() {
StatusFlags status = StatusFlags::OK;
status |= set_regout0();
#ifndef USE_BOOTLOADER_MCUBOOT
if (BOOTLOADER_VERSION_REGISTER <= 0x902) {
#ifdef CONFIG_PRINTK
printk("cannot control regout0 for %#x\n", BOOTLOADER_VERSION_REGISTER);
#endif
} else
#endif
{
status |= set_regout0();
}
#ifndef USE_BOOTLOADER_MCUBOOT
status |= fix_bootloader();
#endif

View File

@@ -35,6 +35,9 @@ DOMAIN = "psram"
DEPENDENCIES = [PLATFORM_ESP32]
# PSRAM availability tracking for cross-component coordination
KEY_PSRAM_GUARANTEED = "psram_guaranteed"
_LOGGER = logging.getLogger(__name__)
psram_ns = cg.esphome_ns.namespace(DOMAIN)
@@ -71,6 +74,23 @@ def supported() -> bool:
return variant in SPIRAM_MODES
def is_guaranteed() -> bool:
"""Check if PSRAM is guaranteed to be available.
Returns True when PSRAM is configured with both 'disabled: false' and
'ignore_not_found: false', meaning the device will fail to boot if PSRAM
is not found. This ensures safe use of high buffer configurations that
depend on PSRAM.
This function should be called during code generation (to_code phase) by
components that need to know PSRAM availability for configuration decisions.
Returns:
bool: True if PSRAM is guaranteed, False otherwise
"""
return CORE.data.get(KEY_PSRAM_GUARANTEED, False)
def validate_psram_mode(config):
esp32_config = fv.full_config.get()[PLATFORM_ESP32]
if config[CONF_SPEED] == "120MHZ":
@@ -131,7 +151,22 @@ def get_config_schema(config):
CONFIG_SCHEMA = get_config_schema
FINAL_VALIDATE_SCHEMA = validate_psram_mode
def _store_psram_guaranteed(config):
"""Store PSRAM guaranteed status in CORE.data for other components.
PSRAM is "guaranteed" when it will fail if not found, ensuring safe use
of high buffer configurations in network/wifi components.
Called during final validation to ensure the flag is available
before any to_code() functions run.
"""
psram_guaranteed = not config[CONF_DISABLED] and not config[CONF_IGNORE_NOT_FOUND]
CORE.data[KEY_PSRAM_GUARANTEED] = psram_guaranteed
return config
FINAL_VALIDATE_SCHEMA = cv.All(validate_psram_mode, _store_psram_guaranteed)
async def to_code(config):

View File

@@ -77,21 +77,23 @@ class Select : public EntityBase {
void add_on_state_callback(std::function<void(std::string, size_t)> &&callback);
/** Set the value of the select by index, this is an optional virtual method.
*
* This method is called by the SelectCall when the index is already known.
* Default implementation converts to string and calls control().
* Override this to work directly with indices and avoid string conversions.
*
* @param index The index as validated by the SelectCall.
*/
virtual void control(size_t index) { this->control(this->option_at(index)); }
protected:
friend class SelectCall;
size_t active_index_{0};
/** Set the value of the select by index, this is an optional virtual method.
*
* IMPORTANT: At least ONE of the two control() methods must be overridden by derived classes.
* Overriding this index-based version is PREFERRED as it avoids string conversions.
*
* This method is called by the SelectCall when the index is already known.
* Default implementation converts to string and calls control(const std::string&).
*
* @param index The index as validated by the SelectCall.
*/
virtual void control(size_t index) { this->control(this->option_at(index)); }
/** Set the value of the select, this is a virtual method that each select integration can implement.
*
* IMPORTANT: At least ONE of the two control() methods must be overridden by derived classes.

View File

@@ -74,9 +74,9 @@ StateClass Sensor::get_state_class() {
void Sensor::publish_state(float state) {
this->raw_state = state;
// Call raw callbacks (before filters)
this->callbacks_.call_first(this->raw_count_, state);
if (this->raw_callback_) {
this->raw_callback_->call(state);
}
ESP_LOGV(TAG, "'%s': Received new state %f", this->name_.c_str(), state);
@@ -87,12 +87,12 @@ void Sensor::publish_state(float state) {
}
}
void Sensor::add_on_state_callback(std::function<void(float)> &&callback) {
this->callbacks_.add_second(std::move(callback));
}
void Sensor::add_on_state_callback(std::function<void(float)> &&callback) { this->callback_.add(std::move(callback)); }
void Sensor::add_on_raw_state_callback(std::function<void(float)> &&callback) {
this->callbacks_.add_first(std::move(callback), &this->raw_count_);
if (!this->raw_callback_) {
this->raw_callback_ = make_unique<CallbackManager<void(float)>>();
}
this->raw_callback_->add(std::move(callback));
}
void Sensor::add_filter(Filter *filter) {
@@ -132,10 +132,7 @@ void Sensor::internal_send_state_to_frontend(float state) {
this->state = state;
ESP_LOGD(TAG, "'%s': Sending state %.5f %s with %d decimals of accuracy", this->get_name().c_str(), state,
this->get_unit_of_measurement_ref().c_str(), this->get_accuracy_decimals());
// Call filtered callbacks (after filters)
this->callbacks_.call_second(this->raw_count_, state);
this->callback_.call(state);
#if defined(USE_SENSOR) && defined(USE_CONTROLLER_REGISTRY)
ControllerRegistry::notify_sensor_update(this);
#endif

View File

@@ -124,7 +124,8 @@ class Sensor : public EntityBase, public EntityBase_DeviceClass, public EntityBa
void internal_send_state_to_frontend(float state);
protected:
PartitionedCallbackManager<void(float)> callbacks_;
std::unique_ptr<CallbackManager<void(float)>> raw_callback_; ///< Storage for raw state callbacks (lazy allocated).
CallbackManager<void(float)> callback_; ///< Storage for filtered state callbacks.
Filter *filter_list_{nullptr}; ///< Store all active filters.
@@ -139,8 +140,6 @@ class Sensor : public EntityBase, public EntityBase_DeviceClass, public EntityBa
uint8_t force_update : 1;
uint8_t reserved : 5; // Reserved for future use
} sensor_flags_{};
uint8_t raw_count_{0}; ///< Number of raw callbacks (partition point in callbacks_ vector)
};
} // namespace sensor

View File

@@ -6,7 +6,7 @@ from pathlib import Path
from esphome import automation, external_files
import esphome.codegen as cg
from esphome.components import audio, esp32, media_player, psram, speaker
from esphome.components import audio, esp32, media_player, network, psram, speaker
import esphome.config_validation as cv
from esphome.const import (
CONF_BUFFER_SIZE,
@@ -32,6 +32,7 @@ _LOGGER = logging.getLogger(__name__)
AUTO_LOAD = ["audio"]
DEPENDENCIES = ["network"]
CODEOWNERS = ["@kahrendt", "@synesthesiam"]
DOMAIN = "media_player"
@@ -280,6 +281,18 @@ PIPELINE_SCHEMA = cv.Schema(
}
)
def _request_high_performance_networking(config):
"""Request high performance networking for streaming media.
Speaker media player streams audio data, so it always benefits from
optimized WiFi and lwip settings regardless of codec support.
Called during config validation to ensure flags are set before to_code().
"""
network.require_high_performance_networking()
return config
CONFIG_SCHEMA = cv.All(
media_player.media_player_schema(SpeakerMediaPlayer).extend(
{
@@ -304,6 +317,7 @@ CONFIG_SCHEMA = cv.All(
),
cv.only_with_esp_idf,
_validate_repeated_speaker,
_request_high_performance_networking,
)
@@ -321,28 +335,10 @@ FINAL_VALIDATE_SCHEMA = cv.All(
async def to_code(config):
if CORE.data[DOMAIN][config[CONF_ID].id][CONF_CODEC_SUPPORT_ENABLED]:
# Compile all supported audio codecs and optimize the wifi settings
# Compile all supported audio codecs
cg.add_define("USE_AUDIO_FLAC_SUPPORT", True)
cg.add_define("USE_AUDIO_MP3_SUPPORT", True)
# Based on https://github.com/espressif/esp-idf/blob/release/v5.4/examples/wifi/iperf/sdkconfig.defaults.esp32
esp32.add_idf_sdkconfig_option("CONFIG_ESP_WIFI_STATIC_RX_BUFFER_NUM", 16)
esp32.add_idf_sdkconfig_option("CONFIG_ESP_WIFI_DYNAMIC_RX_BUFFER_NUM", 64)
esp32.add_idf_sdkconfig_option("CONFIG_ESP_WIFI_DYNAMIC_TX_BUFFER_NUM", 64)
esp32.add_idf_sdkconfig_option("CONFIG_ESP_WIFI_AMPDU_TX_ENABLED", True)
esp32.add_idf_sdkconfig_option("CONFIG_ESP_WIFI_TX_BA_WIN", 32)
esp32.add_idf_sdkconfig_option("CONFIG_ESP_WIFI_AMPDU_RX_ENABLED", True)
esp32.add_idf_sdkconfig_option("CONFIG_ESP_WIFI_RX_BA_WIN", 32)
esp32.add_idf_sdkconfig_option("CONFIG_LWIP_TCP_SND_BUF_DEFAULT", 65534)
esp32.add_idf_sdkconfig_option("CONFIG_LWIP_TCP_WND_DEFAULT", 65534)
esp32.add_idf_sdkconfig_option("CONFIG_LWIP_TCP_RECVMBOX_SIZE", 64)
esp32.add_idf_sdkconfig_option("CONFIG_LWIP_TCPIP_RECVMBOX_SIZE", 64)
# Allocate wifi buffers in PSRAM
esp32.add_idf_sdkconfig_option("CONFIG_SPIRAM_TRY_ALLOCATE_WIFI_LWIP", True)
var = await media_player.new_media_player(config)
await cg.register_component(var, config)

View File

@@ -26,9 +26,9 @@ void log_text_sensor(const char *tag, const char *prefix, const char *type, Text
void TextSensor::publish_state(const std::string &state) {
this->raw_state = state;
// Call raw callbacks (before filters)
this->callbacks_.call_first(this->raw_count_, state);
if (this->raw_callback_) {
this->raw_callback_->call(state);
}
ESP_LOGV(TAG, "'%s': Received new state %s", this->name_.c_str(), state.c_str());
@@ -70,11 +70,13 @@ void TextSensor::clear_filters() {
}
void TextSensor::add_on_state_callback(std::function<void(std::string)> callback) {
this->callbacks_.add_second(std::move(callback));
this->callback_.add(std::move(callback));
}
void TextSensor::add_on_raw_state_callback(std::function<void(std::string)> callback) {
this->callbacks_.add_first(std::move(callback), &this->raw_count_);
if (!this->raw_callback_) {
this->raw_callback_ = make_unique<CallbackManager<void(std::string)>>();
}
this->raw_callback_->add(std::move(callback));
}
std::string TextSensor::get_state() const { return this->state; }
@@ -83,10 +85,7 @@ void TextSensor::internal_send_state_to_frontend(const std::string &state) {
this->state = state;
this->set_has_state(true);
ESP_LOGD(TAG, "'%s': Sending state '%s'", this->name_.c_str(), state.c_str());
// Call filtered callbacks (after filters)
this->callbacks_.call_second(this->raw_count_, state);
this->callback_.call(state);
#if defined(USE_TEXT_SENSOR) && defined(USE_CONTROLLER_REGISTRY)
ControllerRegistry::notify_text_sensor_update(this);
#endif

View File

@@ -58,11 +58,11 @@ class TextSensor : public EntityBase, public EntityBase_DeviceClass {
void internal_send_state_to_frontend(const std::string &state);
protected:
PartitionedCallbackManager<void(std::string)> callbacks_;
std::unique_ptr<CallbackManager<void(std::string)>>
raw_callback_; ///< Storage for raw state callbacks (lazy allocated).
CallbackManager<void(std::string)> callback_; ///< Storage for filtered state callbacks.
Filter *filter_list_{nullptr}; ///< Store all active filters.
uint8_t raw_count_{0}; ///< Number of raw callbacks (partition point in callbacks_ vector)
};
} // namespace text_sensor

View File

@@ -353,9 +353,8 @@ void AsyncWebServerResponse::addHeader(const char *name, const char *value) {
void AsyncResponseStream::print(float value) {
// Use stack buffer to avoid temporary string allocation
// Size: sign (1) + digits (10) + decimal (1) + precision (6) + exponent (5) + null (1) = 24, use 32 for safety
constexpr size_t float_buf_size = 32;
char buf[float_buf_size];
int len = snprintf(buf, float_buf_size, "%f", value);
char buf[32];
int len = snprintf(buf, sizeof(buf), "%f", value);
this->content_.append(buf, len);
}

View File

@@ -5,7 +5,11 @@ from esphome.automation import Condition
import esphome.codegen as cg
from esphome.components.const import CONF_USE_PSRAM
from esphome.components.esp32 import add_idf_sdkconfig_option, const, get_esp32_variant
from esphome.components.network import ip_address_literal
from esphome.components.network import (
has_high_performance_networking,
ip_address_literal,
)
from esphome.components.psram import is_guaranteed as psram_is_guaranteed
from esphome.config_helpers import filter_source_files_from_platform
import esphome.config_validation as cv
from esphome.config_validation import only_with_esp_idf
@@ -56,6 +60,8 @@ _LOGGER = logging.getLogger(__name__)
AUTO_LOAD = ["network"]
_LOGGER = logging.getLogger(__name__)
NO_WIFI_VARIANTS = [const.VARIANT_ESP32H2, const.VARIANT_ESP32P4]
CONF_SAVE = "save"
CONF_MIN_AUTH_MODE = "min_auth_mode"
@@ -496,6 +502,56 @@ async def to_code(config):
if config.get(CONF_USE_PSRAM):
add_idf_sdkconfig_option("CONFIG_SPIRAM_TRY_ALLOCATE_WIFI_LWIP", True)
# Apply high performance WiFi settings if high performance networking is enabled
if CORE.is_esp32 and CORE.using_esp_idf and has_high_performance_networking():
# Check if PSRAM is guaranteed (set by psram component during final validation)
psram_guaranteed = psram_is_guaranteed()
# Always allocate WiFi buffers in PSRAM if available
add_idf_sdkconfig_option("CONFIG_SPIRAM_TRY_ALLOCATE_WIFI_LWIP", True)
if psram_guaranteed:
_LOGGER.info(
"Applying high-performance WiFi settings (PSRAM guaranteed): 512 RX buffers, 32 TX buffers"
)
# PSRAM is guaranteed - use aggressive settings
# Higher maximum values are allowed because CONFIG_LWIP_WND_SCALE is set to true in networking component
# Based on https://github.com/espressif/esp-adf/issues/297#issuecomment-783811702
# Large dynamic RX buffers (requires PSRAM)
add_idf_sdkconfig_option("CONFIG_ESP_WIFI_STATIC_RX_BUFFER_NUM", 16)
add_idf_sdkconfig_option("CONFIG_ESP_WIFI_DYNAMIC_RX_BUFFER_NUM", 512)
# Static TX buffers for better performance
add_idf_sdkconfig_option("CONFIG_ESP_WIFI_STATIC_TX_BUFFER", True)
add_idf_sdkconfig_option("CONFIG_ESP_WIFI_TX_BUFFER_TYPE", 0)
add_idf_sdkconfig_option("CONFIG_ESP_WIFI_CACHE_TX_BUFFER_NUM", 32)
add_idf_sdkconfig_option("CONFIG_ESP_WIFI_STATIC_TX_BUFFER_NUM", 8)
# AMPDU settings optimized for PSRAM
add_idf_sdkconfig_option("CONFIG_ESP_WIFI_AMPDU_TX_ENABLED", True)
add_idf_sdkconfig_option("CONFIG_ESP_WIFI_TX_BA_WIN", 16)
add_idf_sdkconfig_option("CONFIG_ESP_WIFI_AMPDU_RX_ENABLED", True)
add_idf_sdkconfig_option("CONFIG_ESP_WIFI_RX_BA_WIN", 32)
else:
_LOGGER.info(
"Applying optimized WiFi settings: 64 RX buffers, 64 TX buffers"
)
# PSRAM not guaranteed - use more conservative, but still optimized settings
# Based on https://github.com/espressif/esp-idf/blob/release/v5.4/examples/wifi/iperf/sdkconfig.defaults.esp32
# Standard buffer counts
add_idf_sdkconfig_option("CONFIG_ESP_WIFI_STATIC_RX_BUFFER_NUM", 16)
add_idf_sdkconfig_option("CONFIG_ESP_WIFI_DYNAMIC_RX_BUFFER_NUM", 64)
add_idf_sdkconfig_option("CONFIG_ESP_WIFI_DYNAMIC_TX_BUFFER_NUM", 64)
# Standard AMPDU settings
add_idf_sdkconfig_option("CONFIG_ESP_WIFI_AMPDU_TX_ENABLED", True)
add_idf_sdkconfig_option("CONFIG_ESP_WIFI_TX_BA_WIN", 32)
add_idf_sdkconfig_option("CONFIG_ESP_WIFI_AMPDU_RX_ENABLED", True)
add_idf_sdkconfig_option("CONFIG_ESP_WIFI_RX_BA_WIN", 32)
cg.add_define("USE_WIFI")
# must register before OTA safe mode check

View File

@@ -197,6 +197,10 @@ static constexpr uint8_t WIFI_RETRY_COUNT_PER_SSID = 1;
// Rationale: Fast connect prioritizes speed - try each AP once to find a working one quickly
static constexpr uint8_t WIFI_RETRY_COUNT_PER_AP = 1;
/// Cooldown duration in milliseconds after adapter restart or repeated failures
/// Allows WiFi hardware to stabilize before next connection attempt
static constexpr uint32_t WIFI_COOLDOWN_DURATION_MS = 1000;
static constexpr uint8_t get_max_retries_for_phase(WiFiRetryPhase phase) {
switch (phase) {
case WiFiRetryPhase::INITIAL_CONNECT:
@@ -275,7 +279,7 @@ int8_t WiFiComponent::find_next_hidden_sta_(int8_t start_index) {
ESP_LOGD(TAG, "Hidden candidate " LOG_SECRET("'%s'") " at index %d", sta.get_ssid().c_str(), static_cast<int>(i));
return static_cast<int8_t>(i);
}
ESP_LOGD(TAG, "Skipping " LOG_SECRET("'%s'") " (visible in scan)", sta.get_ssid().c_str());
ESP_LOGD(TAG, "Skipping hidden retry for visible network " LOG_SECRET("'%s'"), sta.get_ssid().c_str());
}
// No hidden SSIDs found
return -1;
@@ -289,7 +293,7 @@ void WiFiComponent::start_initial_connection_() {
this->selected_sta_index_ = 0;
this->retry_phase_ = WiFiRetryPhase::EXPLICIT_HIDDEN;
WiFiAP params = this->build_params_for_current_phase_();
this->start_connecting(params, false);
this->start_connecting(params);
} else {
ESP_LOGI(TAG, "Starting scan");
this->start_scanning();
@@ -371,13 +375,13 @@ void WiFiComponent::start() {
// Without saved data, try first configured network or use normal flow
if (loaded_fast_connect) {
ESP_LOGI(TAG, "Starting fast_connect (saved) " LOG_SECRET("'%s'"), params.get_ssid().c_str());
this->start_connecting(params, false);
this->start_connecting(params);
} else if (!this->sta_.empty() && !this->sta_[0].get_hidden()) {
// No saved data, but have configured networks - try first non-hidden network
ESP_LOGI(TAG, "Starting fast_connect (config) " LOG_SECRET("'%s'"), this->sta_[0].get_ssid().c_str());
this->selected_sta_index_ = 0;
params = this->build_params_for_current_phase_();
this->start_connecting(params, false);
this->start_connecting(params);
} else {
// No saved data and (no networks OR first is hidden) - use normal flow
this->start_initial_connection_();
@@ -417,6 +421,7 @@ void WiFiComponent::restart_adapter() {
// Don't set retry_phase_ or num_retried_ here - state machine handles transitions
this->state_ = WIFI_COMPONENT_STATE_COOLDOWN;
this->action_started_ = millis();
this->error_from_callback_ = false;
}
void WiFiComponent::loop() {
@@ -436,7 +441,7 @@ void WiFiComponent::loop() {
switch (this->state_) {
case WIFI_COMPONENT_STATE_COOLDOWN: {
this->status_set_warning(LOG_STR("waiting to reconnect"));
if (millis() - this->action_started_ > 5000) {
if (now - this->action_started_ > WIFI_COOLDOWN_DURATION_MS) {
// After cooldown we either restarted the adapter because of
// a failure, or something tried to connect over and over
// so we entered cooldown. In both cases we call
@@ -450,8 +455,7 @@ void WiFiComponent::loop() {
this->check_scanning_finished();
break;
}
case WIFI_COMPONENT_STATE_STA_CONNECTING:
case WIFI_COMPONENT_STATE_STA_CONNECTING_2: {
case WIFI_COMPONENT_STATE_STA_CONNECTING: {
this->status_set_warning(LOG_STR("associating to network"));
this->check_connecting_finished();
break;
@@ -662,27 +666,27 @@ void WiFiComponent::save_wifi_sta(const std::string &ssid, const std::string &pa
this->set_sta(sta);
}
void WiFiComponent::start_connecting(const WiFiAP &ap, bool two) {
void WiFiComponent::start_connecting(const WiFiAP &ap) {
// Log connection attempt at INFO level with priority
std::string bssid_formatted;
char bssid_s[18];
int8_t priority = 0;
if (ap.get_bssid().has_value()) {
bssid_formatted = format_mac_address_pretty(ap.get_bssid().value().data());
format_mac_addr_upper(ap.get_bssid().value().data(), bssid_s);
priority = this->get_sta_priority(ap.get_bssid().value());
}
ESP_LOGI(TAG,
"Connecting to " LOG_SECRET("'%s'") " " LOG_SECRET("(%s)") " (priority %d, attempt %u/%u in phase %s)...",
ap.get_ssid().c_str(), ap.get_bssid().has_value() ? bssid_formatted.c_str() : LOG_STR_LITERAL("any"),
priority, this->num_retried_ + 1, get_max_retries_for_phase(this->retry_phase_),
ap.get_ssid().c_str(), ap.get_bssid().has_value() ? bssid_s : LOG_STR_LITERAL("any"), priority,
this->num_retried_ + 1, get_max_retries_for_phase(this->retry_phase_),
LOG_STR_ARG(retry_phase_to_log_string(this->retry_phase_)));
#ifdef ESPHOME_LOG_HAS_VERBOSE
ESP_LOGV(TAG, "Connection Params:");
ESP_LOGV(TAG, " SSID: '%s'", ap.get_ssid().c_str());
if (ap.get_bssid().has_value()) {
ESP_LOGV(TAG, " BSSID: %s", format_mac_address_pretty(ap.get_bssid()->data()).c_str());
ESP_LOGV(TAG, " BSSID: %s", bssid_s);
} else {
ESP_LOGV(TAG, " BSSID: Not Set");
}
@@ -729,14 +733,11 @@ void WiFiComponent::start_connecting(const WiFiAP &ap, bool two) {
if (!this->wifi_sta_connect_(ap)) {
ESP_LOGE(TAG, "wifi_sta_connect_ failed");
this->retry_connect();
return;
}
if (!two) {
this->state_ = WIFI_COMPONENT_STATE_STA_CONNECTING;
// Enter cooldown to allow WiFi hardware to stabilize
// (immediate failure suggests hardware not ready, different from connection timeout)
this->state_ = WIFI_COMPONENT_STATE_COOLDOWN;
} else {
this->state_ = WIFI_COMPONENT_STATE_STA_CONNECTING_2;
this->state_ = WIFI_COMPONENT_STATE_STA_CONNECTING;
}
this->action_started_ = millis();
}
@@ -786,6 +787,8 @@ const LogString *get_signal_bars(int8_t rssi) {
void WiFiComponent::print_connect_params_() {
bssid_t bssid = wifi_bssid();
char bssid_s[18];
format_mac_addr_upper(bssid.data(), bssid_s);
ESP_LOGCONFIG(TAG, " Local MAC: %s", get_mac_address_pretty().c_str());
if (this->is_disabled()) {
@@ -808,9 +811,9 @@ void WiFiComponent::print_connect_params_() {
" Gateway: %s\n"
" DNS1: %s\n"
" DNS2: %s",
wifi_ssid().c_str(), format_mac_address_pretty(bssid.data()).c_str(), App.get_name().c_str(), rssi,
LOG_STR_ARG(get_signal_bars(rssi)), get_wifi_channel(), wifi_subnet_mask_().str().c_str(),
wifi_gateway_ip_().str().c_str(), wifi_dns_ip_(0).str().c_str(), wifi_dns_ip_(1).str().c_str());
wifi_ssid().c_str(), bssid_s, App.get_name().c_str(), rssi, LOG_STR_ARG(get_signal_bars(rssi)),
get_wifi_channel(), wifi_subnet_mask_().str().c_str(), wifi_gateway_ip_().str().c_str(),
wifi_dns_ip_(0).str().c_str(), wifi_dns_ip_(1).str().c_str());
#ifdef ESPHOME_LOG_HAS_VERBOSE
if (const WiFiAP *config = this->get_selected_sta_(); config && config->get_bssid().has_value()) {
ESP_LOGV(TAG, " Priority: %d", this->get_sta_priority(*config->get_bssid()));
@@ -1017,7 +1020,7 @@ void WiFiComponent::check_scanning_finished() {
WiFiAP params = this->build_params_for_current_phase_();
// Ensure we're in SCAN_CONNECTING phase when connecting with scan results
// (needed when scan was started directly without transition_to_phase_, e.g., initial scan)
this->start_connecting(params, false);
this->start_connecting(params);
}
void WiFiComponent::dump_config() {
@@ -1086,18 +1089,12 @@ void WiFiComponent::check_connecting_finished() {
uint32_t now = millis();
if (now - this->action_started_ > 30000) {
ESP_LOGW(TAG, "Connection timeout");
// Move from STA_CONNECTING_2 back to STA_CONNECTING state
// since we know the connection attempt has failed
this->state_ = WIFI_COMPONENT_STATE_STA_CONNECTING;
this->retry_connect();
return;
}
if (this->error_from_callback_) {
ESP_LOGW(TAG, "Connecting to network failed (callback)");
// Move from STA_CONNECTING_2 back to STA_CONNECTING state
// since we know the connection attempt is finished
this->state_ = WIFI_COMPONENT_STATE_STA_CONNECTING;
this->retry_connect();
return;
}
@@ -1106,9 +1103,6 @@ void WiFiComponent::check_connecting_finished() {
return;
}
// Move from STA_CONNECTING_2 back to STA_CONNECTING state
// since we know the connection attempt is finished
this->state_ = WIFI_COMPONENT_STATE_STA_CONNECTING;
if (status == WiFiSTAConnectStatus::ERROR_NETWORK_NOT_FOUND) {
ESP_LOGW(TAG, "Network no longer found");
this->retry_connect();
@@ -1398,9 +1392,10 @@ void WiFiComponent::log_and_adjust_priority_for_failed_connect_() {
(old_priority > std::numeric_limits<int8_t>::min()) ? (old_priority - 1) : std::numeric_limits<int8_t>::min();
this->set_sta_priority(failed_bssid.value(), new_priority);
}
ESP_LOGD(TAG, "Failed " LOG_SECRET("'%s'") " " LOG_SECRET("(%s)") ", priority %d → %d", ssid.c_str(),
format_mac_address_pretty(failed_bssid.value().data()).c_str(), old_priority, new_priority);
char bssid_s[18];
format_mac_addr_upper(failed_bssid.value().data(), bssid_s);
ESP_LOGD(TAG, "Failed " LOG_SECRET("'%s'") " " LOG_SECRET("(%s)") ", priority %d → %d", ssid.c_str(), bssid_s,
old_priority, new_priority);
// After adjusting priority, check if all priorities are now at minimum
// If so, clear the vector to save memory and reset for fresh start
@@ -1492,27 +1487,14 @@ void WiFiComponent::retry_connect() {
this->error_from_callback_ = false;
if (this->state_ == WIFI_COMPONENT_STATE_STA_CONNECTING) {
yield();
// Check if we have a valid target before building params
// After exhausting all networks in a phase, selected_sta_index_ may be -1
// In that case, skip connection and let next wifi_loop() handle phase transition
if (this->selected_sta_index_ >= 0) {
this->state_ = WIFI_COMPONENT_STATE_STA_CONNECTING_2;
WiFiAP params = this->build_params_for_current_phase_();
this->start_connecting(params, true);
}
return;
yield();
// Check if we have a valid target before building params
// After exhausting all networks in a phase, selected_sta_index_ may be -1
// In that case, skip connection and let next wifi_loop() handle phase transition
if (this->selected_sta_index_ >= 0) {
WiFiAP params = this->build_params_for_current_phase_();
this->start_connecting(params);
}
// If we can't progress forward its likely because scanning failed
// or the stack is in a bad state after restart so we cooldown first
// and once it finishes, cooldown will call check_connecting_finished()
// which will progress the state machine
ESP_LOGD(TAG, "Entering cooldown from state %d and phase %s", this->state_,
LOG_STR_ARG(retry_phase_to_log_string(this->retry_phase_)));
this->state_ = WIFI_COMPONENT_STATE_COOLDOWN;
this->action_started_ = millis();
}
void WiFiComponent::set_reboot_timeout(uint32_t reboot_timeout) { this->reboot_timeout_ = reboot_timeout; }

View File

@@ -74,12 +74,6 @@ enum WiFiComponentState : uint8_t {
WIFI_COMPONENT_STATE_STA_SCANNING,
/** WiFi is in STA(+AP) mode and currently connecting to an AP. */
WIFI_COMPONENT_STATE_STA_CONNECTING,
/** WiFi is in STA(+AP) mode and currently connecting to an AP a second time.
*
* This is required because for some reason ESPs don't like to connect to WiFi APs directly after
* a scan.
* */
WIFI_COMPONENT_STATE_STA_CONNECTING_2,
/** WiFi is in STA(+AP) mode and successfully connected. */
WIFI_COMPONENT_STATE_STA_CONNECTED,
/** WiFi is in AP-only mode and internal AP is already enabled. */
@@ -275,7 +269,9 @@ class WiFiComponent : public Component {
bool is_disabled();
void start_scanning();
void check_scanning_finished();
void start_connecting(const WiFiAP &ap, bool two);
void start_connecting(const WiFiAP &ap);
// Backward compatibility overload - ignores 'two' parameter
void start_connecting(const WiFiAP &ap, bool /* two */) { this->start_connecting(ap); }
void check_connecting_finished();
@@ -430,7 +426,7 @@ class WiFiComponent : public Component {
bool wifi_sta_pre_setup_();
bool wifi_apply_output_power_(float output_power);
bool wifi_apply_power_save_();
bool wifi_sta_ip_config_(optional<ManualIP> manual_ip);
bool wifi_sta_ip_config_(const optional<ManualIP> &manual_ip);
bool wifi_apply_hostname_();
bool wifi_sta_connect_(const WiFiAP &ap);
void wifi_pre_setup_();
@@ -438,7 +434,7 @@ class WiFiComponent : public Component {
bool wifi_scan_start_(bool passive);
#ifdef USE_WIFI_AP
bool wifi_ap_ip_config_(optional<ManualIP> manual_ip);
bool wifi_ap_ip_config_(const optional<ManualIP> &manual_ip);
bool wifi_start_ap_(const WiFiAP &ap);
#endif // USE_WIFI_AP

View File

@@ -117,7 +117,7 @@ void netif_set_addr(struct netif *netif, const ip4_addr_t *ip, const ip4_addr_t
};
#endif
bool WiFiComponent::wifi_sta_ip_config_(optional<ManualIP> manual_ip) {
bool WiFiComponent::wifi_sta_ip_config_(const optional<ManualIP> &manual_ip) {
// enable STA
if (!this->wifi_mode_(true, {}))
return false;
@@ -525,8 +525,10 @@ void WiFiComponent::wifi_event_callback(System_Event_t *event) {
ESP_LOGW(TAG, "Disconnected ssid='%s' reason='Probe Request Unsuccessful'", buf);
s_sta_connect_not_found = true;
} else {
ESP_LOGW(TAG, "Disconnected ssid='%s' bssid=" LOG_SECRET("%s") " reason='%s'", buf,
format_mac_address_pretty(it.bssid).c_str(), LOG_STR_ARG(get_disconnect_reason_str(it.reason)));
char bssid_s[18];
format_mac_addr_upper(it.bssid, bssid_s);
ESP_LOGW(TAG, "Disconnected ssid='%s' bssid=" LOG_SECRET("%s") " reason='%s'", buf, bssid_s,
LOG_STR_ARG(get_disconnect_reason_str(it.reason)));
s_sta_connect_error = true;
}
s_sta_connected = false;
@@ -730,7 +732,7 @@ void WiFiComponent::wifi_scan_done_callback_(void *arg, STATUS status) {
}
#ifdef USE_WIFI_AP
bool WiFiComponent::wifi_ap_ip_config_(optional<ManualIP> manual_ip) {
bool WiFiComponent::wifi_ap_ip_config_(const optional<ManualIP> &manual_ip) {
// enable AP
if (!this->wifi_mode_({}, true))
return false;

View File

@@ -487,7 +487,7 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) {
return true;
}
bool WiFiComponent::wifi_sta_ip_config_(optional<ManualIP> manual_ip) {
bool WiFiComponent::wifi_sta_ip_config_(const optional<ManualIP> &manual_ip) {
// enable STA
if (!this->wifi_mode_(true, {}))
return false;
@@ -746,8 +746,10 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) {
ESP_LOGI(TAG, "Disconnected ssid='%s' reason='Station Roaming'", buf);
return;
} else {
ESP_LOGW(TAG, "Disconnected ssid='%s' bssid=" LOG_SECRET("%s") " reason='%s'", buf,
format_mac_address_pretty(it.bssid).c_str(), get_disconnect_reason_str(it.reason));
char bssid_s[18];
format_mac_addr_upper(it.bssid, bssid_s);
ESP_LOGW(TAG, "Disconnected ssid='%s' bssid=" LOG_SECRET("%s") " reason='%s'", buf, bssid_s,
get_disconnect_reason_str(it.reason));
s_sta_connect_error = true;
}
s_sta_connected = false;
@@ -884,7 +886,7 @@ bool WiFiComponent::wifi_scan_start_(bool passive) {
}
#ifdef USE_WIFI_AP
bool WiFiComponent::wifi_ap_ip_config_(optional<ManualIP> manual_ip) {
bool WiFiComponent::wifi_ap_ip_config_(const optional<ManualIP> &manual_ip) {
esp_err_t err;
// enable AP

View File

@@ -68,7 +68,7 @@ bool WiFiComponent::wifi_sta_pre_setup_() {
return true;
}
bool WiFiComponent::wifi_apply_power_save_() { return WiFi.setSleep(this->power_save_ != WIFI_POWER_SAVE_NONE); }
bool WiFiComponent::wifi_sta_ip_config_(optional<ManualIP> manual_ip) {
bool WiFiComponent::wifi_sta_ip_config_(const optional<ManualIP> &manual_ip) {
// enable STA
if (!this->wifi_mode_(true, {}))
return false;
@@ -299,8 +299,10 @@ void WiFiComponent::wifi_event_callback_(esphome_wifi_event_id_t event, esphome_
if (it.reason == WIFI_REASON_NO_AP_FOUND) {
ESP_LOGW(TAG, "Disconnected ssid='%s' reason='Probe Request Unsuccessful'", buf);
} else {
ESP_LOGW(TAG, "Disconnected ssid='%s' bssid=" LOG_SECRET("%s") " reason='%s'", buf,
format_mac_address_pretty(it.bssid).c_str(), get_disconnect_reason_str(it.reason));
char bssid_s[18];
format_mac_addr_upper(it.bssid, bssid_s);
ESP_LOGW(TAG, "Disconnected ssid='%s' bssid=" LOG_SECRET("%s") " reason='%s'", buf, bssid_s,
get_disconnect_reason_str(it.reason));
}
uint8_t reason = it.reason;
@@ -434,7 +436,7 @@ void WiFiComponent::wifi_scan_done_callback_() {
}
#ifdef USE_WIFI_AP
bool WiFiComponent::wifi_ap_ip_config_(optional<ManualIP> manual_ip) {
bool WiFiComponent::wifi_ap_ip_config_(const optional<ManualIP> &manual_ip) {
// enable AP
if (!this->wifi_mode_({}, true))
return false;

View File

@@ -72,7 +72,7 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) {
bool WiFiComponent::wifi_sta_pre_setup_() { return this->wifi_mode_(true, {}); }
bool WiFiComponent::wifi_sta_ip_config_(optional<ManualIP> manual_ip) {
bool WiFiComponent::wifi_sta_ip_config_(const optional<ManualIP> &manual_ip) {
if (!manual_ip.has_value()) {
return true;
}
@@ -146,7 +146,7 @@ bool WiFiComponent::wifi_scan_start_(bool passive) {
}
#ifdef USE_WIFI_AP
bool WiFiComponent::wifi_ap_ip_config_(optional<ManualIP> manual_ip) {
bool WiFiComponent::wifi_ap_ip_config_(const optional<ManualIP> &manual_ip) {
esphome::network::IPAddress ip_address, gateway, subnet, dns;
if (manual_ip.has_value()) {
ip_address = manual_ip->static_ip;

View File

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

View File

@@ -710,15 +710,6 @@ class EsphomeCore:
def relative_piolibdeps_path(self, *path: str | Path) -> Path:
return self.relative_build_path(".piolibdeps", *path)
@property
def platformio_cache_dir(self) -> str:
"""Get the PlatformIO cache directory path."""
# Check if running in Docker/HA addon with custom cache dir
if (cache_dir := os.environ.get("PLATFORMIO_CACHE_DIR")) and cache_dir.strip():
return cache_dir
# Default PlatformIO cache location
return os.path.expanduser("~/.platformio/.cache")
@property
def firmware_bin(self) -> Path:
if self.is_libretiny:

View File

@@ -412,7 +412,12 @@ template<typename... Ts> class WaitUntilAction : public Action<Ts...>, public Co
void setup() override {
// Start with loop disabled - only enable when there's work to do
this->disable_loop();
// IMPORTANT: Only disable if num_running_ is 0, otherwise play_complex() was already
// called before our setup() (e.g., from on_boot trigger at same priority level)
// and we must not undo its enable_loop() call
if (this->num_running_ == 0) {
this->disable_loop();
}
}
void play_complex(const Ts &...x) override {

View File

@@ -216,7 +216,6 @@
#define USE_ARDUINO_VERSION_CODE VERSION_CODE(3, 3, 2)
#define USE_ETHERNET
#define USE_ETHERNET_KSZ8081
#define USE_ETHERNET_MANUAL_IP
#endif
#ifdef USE_ESP_IDF

View File

@@ -414,8 +414,10 @@ int8_t step_to_accuracy_decimals(float step) {
return str.length() - dot_pos - 1;
}
// Store BASE64 characters as array - automatically placed in flash/ROM on embedded platforms
static const char BASE64_CHARS[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
// Use C-style string constant to store in ROM instead of RAM (saves 24 bytes)
static constexpr const char *BASE64_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
"abcdefghijklmnopqrstuvwxyz"
"0123456789+/";
// Helper function to find the index of a base64 character in the lookup table.
// Returns the character's position (0-63) if found, or 0 if not found.
@@ -425,8 +427,8 @@ static const char BASE64_CHARS[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqr
// stops processing at the first invalid character due to the is_base64() check in its
// while loop condition, making this edge case harmless in practice.
static inline uint8_t base64_find_char(char c) {
const void *ptr = memchr(BASE64_CHARS, c, sizeof(BASE64_CHARS));
return ptr ? (static_cast<const char *>(ptr) - BASE64_CHARS) : 0;
const char *pos = strchr(BASE64_CHARS, c);
return pos ? (pos - BASE64_CHARS) : 0;
}
static inline bool is_base64(char c) { return (isalnum(c) || (c == '+') || (c == '/')); }

View File

@@ -145,9 +145,6 @@ template<typename T, size_t N> class StaticVector {
size_t size() const { return count_; }
bool empty() const { return count_ == 0; }
// Direct access to size counter for efficient in-place construction
size_t &count() { return count_; }
T &operator[](size_t i) { return data_[i]; }
const T &operator[](size_t i) const { return data_[i]; }
@@ -872,73 +869,6 @@ template<typename... Ts> class CallbackManager<void(Ts...)> {
std::vector<std::function<void(Ts...)>> callbacks_;
};
template<typename... X> class PartitionedCallbackManager;
/** Helper class for callbacks partitioned into two sections.
*
* Uses a single vector partitioned into two sections: [first_0, ..., first_m-1, second_0, ..., second_n-1]
* The partition point is tracked externally by the caller (typically stored in the entity class for optimal alignment).
*
* Memory efficient: Only stores a single pointer (4 bytes on 32-bit platforms, 8 bytes on 64-bit platforms).
* The partition count lives in the entity class where it can be packed with other small fields to avoid padding waste.
*
* Design rationale: The asymmetric API (add_first takes first_count*, while call_first/call_second take it by value)
* is intentional - add_first must increment the count, while call methods only read it. This avoids storing first_count
* internally, saving memory per instance.
*
* @tparam Ts The arguments for the callbacks, wrapped in void().
*/
template<typename... Ts> class PartitionedCallbackManager<void(Ts...)> {
public:
/// Add a callback to the first partition.
void add_first(std::function<void(Ts...)> &&callback, uint8_t *first_count) {
if (!this->callbacks_) {
this->callbacks_ = make_unique<std::vector<std::function<void(Ts...)>>>();
}
// Add to first partition: append then rotate into position
this->callbacks_->push_back(std::move(callback));
// Avoid potential underflow: rewrite comparison to not subtract from size()
if (*first_count + 1 < this->callbacks_->size()) {
// Use std::rotate to maintain registration order in second partition
std::rotate(this->callbacks_->begin() + *first_count, this->callbacks_->end() - 1, this->callbacks_->end());
}
(*first_count)++;
}
/// Add a callback to the second partition.
void add_second(std::function<void(Ts...)> &&callback) {
if (!this->callbacks_) {
this->callbacks_ = make_unique<std::vector<std::function<void(Ts...)>>>();
}
// Add to second partition: just append (already at end after first partition)
this->callbacks_->push_back(std::move(callback));
}
/// Call all callbacks in the first partition.
void call_first(uint8_t first_count, Ts... args) {
if (this->callbacks_) {
for (size_t i = 0; i < first_count; i++) {
(*this->callbacks_)[i](args...);
}
}
}
/// Call all callbacks in the second partition.
void call_second(uint8_t first_count, Ts... args) {
if (this->callbacks_) {
for (size_t i = first_count; i < this->callbacks_->size(); i++) {
(*this->callbacks_)[i](args...);
}
}
}
protected:
/// Partitioned callback storage: [first_0, ..., first_m-1, second_0, ..., second_n-1]
std::unique_ptr<std::vector<std::function<void(Ts...)>>> callbacks_;
};
/// Helper class to deduplicate items in a series of values.
template<typename T> class Deduplicator {
public:

View File

@@ -94,9 +94,10 @@ class Scheduler {
} name_;
uint32_t interval;
// Split time to handle millis() rollover. The scheduler combines the 32-bit millis()
// with a 16-bit rollover counter to create a 48-bit time space (stored as 64-bit
// for compatibility). With 49.7 days per 32-bit rollover, the 16-bit counter
// supports 49.7 days × 65536 = ~8900 years. This ensures correct scheduling
// with a 16-bit rollover counter to create a 48-bit time space (using 32+16 bits).
// This is intentionally limited to 48 bits, not stored as a full 64-bit value.
// With 49.7 days per 32-bit rollover, the 16-bit counter supports
// 49.7 days × 65536 = ~8900 years. This ensures correct scheduling
// even when devices run for months. Split into two fields for better memory
// alignment on 32-bit systems.
uint32_t next_execution_low_; // Lower 32 bits of execution time (millis value)

View File

@@ -145,16 +145,7 @@ def run_compile(config, verbose):
args = []
if CONF_COMPILE_PROCESS_LIMIT in config[CONF_ESPHOME]:
args += [f"-j{config[CONF_ESPHOME][CONF_COMPILE_PROCESS_LIMIT]}"]
result = run_platformio_cli_run(config, verbose, *args)
# Run memory analysis if enabled
if config.get(CONF_ESPHOME, {}).get("analyze_memory", False):
try:
analyze_memory_usage(config)
except Exception as e:
_LOGGER.warning("Failed to analyze memory usage: %s", e)
return result
return run_platformio_cli_run(config, verbose, *args)
def _run_idedata(config):
@@ -403,74 +394,3 @@ class IDEData:
if path.endswith(".exe")
else f"{path[:-3]}readelf"
)
def analyze_memory_usage(config: dict[str, Any]) -> None:
"""Analyze memory usage by component after compilation."""
# Lazy import to avoid overhead when not needed
from esphome.analyze_memory.cli import MemoryAnalyzerCLI
from esphome.analyze_memory.helpers import get_esphome_components
idedata = get_idedata(config)
# Get paths to tools
elf_path = idedata.firmware_elf_path
objdump_path = idedata.objdump_path
readelf_path = idedata.readelf_path
# Debug logging
_LOGGER.debug("ELF path from idedata: %s", elf_path)
# Check if file exists
if not Path(elf_path).exists():
# Try alternate path
alt_path = Path(CORE.relative_build_path(".pioenvs", CORE.name, "firmware.elf"))
if alt_path.exists():
elf_path = str(alt_path)
_LOGGER.debug("Using alternate ELF path: %s", elf_path)
else:
_LOGGER.warning("ELF file not found at %s or %s", elf_path, alt_path)
return
# Extract external components from config
external_components = set()
# Get the list of built-in ESPHome components
builtin_components = get_esphome_components()
# Special non-component keys that appear in configs
NON_COMPONENT_KEYS = {
CONF_ESPHOME,
"substitutions",
"packages",
"globals",
"<<",
}
# Check all top-level keys in config
for key in config:
if key not in builtin_components and key not in NON_COMPONENT_KEYS:
# This is an external component
external_components.add(key)
_LOGGER.debug("Detected external components: %s", external_components)
# Create analyzer and run analysis
analyzer = MemoryAnalyzerCLI(
elf_path, objdump_path, readelf_path, external_components
)
analyzer.analyze()
# Generate and print report
report = analyzer.generate_report()
_LOGGER.info("\n%s", report)
# Optionally save to file
if config.get(CONF_ESPHOME, {}).get("memory_report_file"):
report_file = Path(config[CONF_ESPHOME]["memory_report_file"])
if report_file.suffix == ".json":
report_file.write_text(analyzer.to_json())
_LOGGER.info("Memory report saved to %s", report_file)
else:
report_file.write_text(report)
_LOGGER.info("Memory report saved to %s", report_file)

View File

@@ -5,7 +5,7 @@ pyupgrade==3.21.1 # also change in .pre-commit-config.yaml when updating
pre-commit
# Unit tests
pytest==9.0.0
pytest==9.0.1
pytest-cov==7.0.0
pytest-mock==3.15.1
pytest-asyncio==1.3.0

View File

@@ -66,6 +66,5 @@ def test_text_config_lamda_is_set(generate_main):
main_cpp = generate_main("tests/component_tests/text/test_text.yaml")
# Then
# Stateless lambda optimization: empty capture list allows function pointer conversion
assert "it_4->set_template([]() -> esphome::optional<std::string> {" in main_cpp
assert 'return std::string{"Hello"};' in main_cpp

View File

@@ -1,2 +0,0 @@
packages:
common: !include common.yaml

View File

@@ -1,4 +0,0 @@
packages:
i2c: !include ../../test_build_components/common/i2c/esp32-s3-idf.yaml
<<: !include common.yaml

View File

@@ -1,15 +0,0 @@
packages:
common: !include common.yaml
matrix_keypad:
id: keypad
rows:
- pin: 10
- pin: 11
columns:
- pin: 12
- pin: 13
keys: "1234"
has_pulldowns: true
on_key:
- lambda: ESP_LOGI("KEY", "key %d pressed", x);

View File

@@ -1,4 +0,0 @@
packages:
i2c: !include ../../test_build_components/common/i2c/esp32-s3-idf.yaml
<<: !include common.yaml

View File

@@ -1,4 +0,0 @@
packages:
i2c: !include ../../test_build_components/common/i2c/esp32-s3-idf.yaml
<<: !include common.yaml

View File

@@ -1 +1,4 @@
<<: !include common.yaml
network:
enable_high_performance: true

View File

@@ -1,4 +0,0 @@
packages:
i2c: !include ../../test_build_components/common/i2c/esp32-s3-idf.yaml
<<: !include common.yaml

View File

@@ -6,4 +6,4 @@ nrf52:
mode:
output: true
reg0:
voltage: default
voltage: 1.8V

View File

@@ -1,4 +0,0 @@
substitutions:
pin: GPIO4
<<: !include common.yaml

View File

@@ -1,4 +0,0 @@
substitutions:
pin: GPIO1
<<: !include common.yaml

View File

@@ -1,2 +0,0 @@
packages:
common: !include common.yaml

View File

@@ -1,8 +0,0 @@
substitutions:
scl_pin: GPIO40
sda_pin: GPIO41
packages:
i2c: !include ../../test_build_components/common/i2c/esp32-s3-idf.yaml
<<: !include common.yaml

View File

@@ -1,2 +0,0 @@
packages:
common: !include common.yaml

View File

@@ -1,48 +0,0 @@
<<: !include ../logger/common-usb_serial_jtag.yaml
esphome:
on_boot:
then:
- uart.write:
id: uart_1
data: 'Hello World'
- uart.write:
id: uart_1
data: [0x00, 0x20, 0x42]
uart:
- id: uart_1
tx_pin: 4
rx_pin: 5
flow_control_pin: 6
baud_rate: 9600
data_bits: 8
rx_buffer_size: 512
rx_full_threshold: 10
rx_timeout: 1
parity: EVEN
stop_bits: 2
- id: uart_2
tx_pin: 7
rx_pin: 8
flow_control_pin: 9
baud_rate: 9600
data_bits: 8
rx_buffer_size: 512
rx_full_threshold: 10
rx_timeout: 1
parity: EVEN
stop_bits: 2
- id: uart_3
tx_pin: 10
rx_pin: 11
flow_control_pin: 12
baud_rate: 9600
data_bits: 8
rx_buffer_size: 512
rx_full_threshold: 10
rx_timeout: 1
parity: EVEN
stop_bits: 2

View File

@@ -1,9 +0,0 @@
substitutions:
scl_pin: GPIO40
sda_pin: GPIO41
packages:
i2c: !include ../../test_build_components/common/i2c/esp32-s3-idf.yaml
uart_bridge_2: !include ../../test_build_components/common/uart_bridge_2/esp32-s3-idf.yaml
<<: !include common.yaml

View File

@@ -1,11 +0,0 @@
substitutions:
clk_pin: GPIO40
miso_pin: GPIO41
mosi_pin: GPIO6
cs_pin: GPIO19
packages:
spi: !include ../../test_build_components/common/spi/esp32-s3-idf.yaml
uart_bridge_2: !include ../../test_build_components/common/uart_bridge_2/esp32-s3-idf.yaml
<<: !include common.yaml

View File

@@ -1,9 +0,0 @@
substitutions:
scl_pin: GPIO40
sda_pin: GPIO41
packages:
i2c: !include ../../test_build_components/common/i2c/esp32-s3-idf.yaml
uart_bridge_4: !include ../../test_build_components/common/uart_bridge_4/esp32-s3-idf.yaml
<<: !include common.yaml

View File

@@ -1,11 +0,0 @@
substitutions:
clk_pin: GPIO40
miso_pin: GPIO41
mosi_pin: GPIO6
cs_pin: GPIO19
packages:
spi: !include ../../test_build_components/common/spi/esp32-s3-idf.yaml
uart_bridge_4: !include ../../test_build_components/common/uart_bridge_4/esp32-s3-idf.yaml
<<: !include common.yaml

View File

@@ -1,9 +0,0 @@
substitutions:
scl_pin: GPIO40
sda_pin: GPIO41
packages:
i2c: !include ../../test_build_components/common/i2c/esp32-s3-idf.yaml
uart_bridge_4: !include ../../test_build_components/common/uart_bridge_4/esp32-s3-idf.yaml
<<: !include common.yaml

View File

@@ -1,11 +0,0 @@
substitutions:
clk_pin: GPIO40
miso_pin: GPIO41
mosi_pin: GPIO6
cs_pin: GPIO19
packages:
spi: !include ../../test_build_components/common/spi/esp32-s3-idf.yaml
uart_bridge_4: !include ../../test_build_components/common/uart_bridge_4/esp32-s3-idf.yaml
<<: !include common.yaml

View File

@@ -1,9 +0,0 @@
substitutions:
scl_pin: GPIO40
sda_pin: GPIO41
packages:
i2c: !include ../../test_build_components/common/i2c/esp32-s3-idf.yaml
uart_bridge_4: !include ../../test_build_components/common/uart_bridge_4/esp32-s3-idf.yaml
<<: !include common.yaml

View File

@@ -1,11 +0,0 @@
substitutions:
clk_pin: GPIO40
miso_pin: GPIO41
mosi_pin: GPIO6
cs_pin: GPIO19
packages:
spi: !include ../../test_build_components/common/spi/esp32-s3-idf.yaml
uart_bridge_4: !include ../../test_build_components/common/uart_bridge_4/esp32-s3-idf.yaml
<<: !include common.yaml

View File

@@ -1,59 +0,0 @@
esphome:
name: test-user-services-union
friendly_name: Test User Services Union Storage
esp32:
board: esp32dev
framework:
type: esp-idf
logger:
level: DEBUG
wifi:
ssid: "test"
password: "password"
api:
actions:
# Test service with no arguments
- action: test_no_args
then:
- logger.log: "No args service called"
# Test service with one argument
- action: test_one_arg
variables:
value: int
then:
- logger.log:
format: "One arg service: %d"
args: [value]
# Test service with multiple arguments of different types
- action: test_multi_args
variables:
int_val: int
float_val: float
str_val: string
bool_val: bool
then:
- logger.log:
format: "Multi args: %d, %.2f, %s, %d"
args: [int_val, float_val, str_val.c_str(), bool_val]
# Test service with max typical arguments
- action: test_many_args
variables:
arg1: int
arg2: int
arg3: int
arg4: string
arg5: float
then:
- logger.log: "Many args service called"
binary_sensor:
- platform: template
name: "Test Binary Sensor"
id: test_sensor

View File

@@ -0,0 +1,47 @@
# Test for wait_until in on_boot automation
# Reproduces bug where wait_until in on_boot would hang forever
# because WaitUntilAction::setup() would disable_loop() after
# play_complex() had already enabled it.
esphome:
name: wait-until-on-boot
on_boot:
then:
- logger.log: "on_boot: Starting wait_until test"
- globals.set:
id: on_boot_started
value: 'true'
- wait_until:
condition:
lambda: return id(test_flag);
timeout: 5s
- logger.log: "on_boot: wait_until completed successfully"
host:
logger:
level: DEBUG
globals:
- id: on_boot_started
type: bool
initial_value: 'false'
- id: test_flag
type: bool
initial_value: 'false'
api:
actions:
- action: set_test_flag
then:
- globals.set:
id: test_flag
value: 'true'
- action: check_on_boot_started
then:
- lambda: |-
if (id(on_boot_started)) {
ESP_LOGI("test", "on_boot has started");
} else {
ESP_LOGI("test", "on_boot has NOT started");
}

View File

@@ -0,0 +1,91 @@
"""Integration test for wait_until in on_boot automation.
This test validates that wait_until works correctly when triggered from on_boot,
which runs at the same setup priority as WaitUntilAction itself. This was broken
before the fix because WaitUntilAction::setup() would unconditionally disable_loop(),
even if play_complex() had already been called and enabled the loop.
The bug: on_boot fires during StartupTrigger::setup(), which calls WaitUntilAction::play_complex()
before WaitUntilAction::setup() has run. Then when WaitUntilAction::setup() runs, it calls
disable_loop(), undoing the enable_loop() from play_complex(), causing wait_until to hang forever.
"""
from __future__ import annotations
import asyncio
import re
import pytest
from .types import APIClientConnectedFactory, RunCompiledFunction
@pytest.mark.asyncio
async def test_wait_until_on_boot(
yaml_config: str,
run_compiled: RunCompiledFunction,
api_client_connected: APIClientConnectedFactory,
) -> None:
"""Test that wait_until works in on_boot automation with a condition that becomes true later."""
loop = asyncio.get_running_loop()
on_boot_started = False
on_boot_completed = False
on_boot_started_pattern = re.compile(r"on_boot: Starting wait_until test")
on_boot_complete_pattern = re.compile(r"on_boot: wait_until completed successfully")
on_boot_started_future = loop.create_future()
on_boot_complete_future = loop.create_future()
def check_output(line: str) -> None:
"""Check log output for test progress."""
nonlocal on_boot_started, on_boot_completed
if on_boot_started_pattern.search(line):
on_boot_started = True
if not on_boot_started_future.done():
on_boot_started_future.set_result(True)
if on_boot_complete_pattern.search(line):
on_boot_completed = True
if not on_boot_complete_future.done():
on_boot_complete_future.set_result(True)
async with (
run_compiled(yaml_config, line_callback=check_output),
api_client_connected() as client,
):
# Wait for on_boot to start
await asyncio.wait_for(on_boot_started_future, timeout=10.0)
assert on_boot_started, "on_boot did not start"
# At this point, on_boot is blocked in wait_until waiting for test_flag to become true
# If the bug exists, wait_until's loop is disabled and it will never complete
# even after we set the flag
# Give a moment for setup to complete
await asyncio.sleep(0.5)
# Now set the flag that wait_until is waiting for
_, services = await client.list_entities_services()
set_flag_service = next(
(s for s in services if s.name == "set_test_flag"), None
)
assert set_flag_service is not None, "set_test_flag service not found"
client.execute_service(set_flag_service, {})
# If the fix works, wait_until's loop() will check the condition and proceed
# If the bug exists, wait_until is stuck with disabled loop and will timeout
try:
await asyncio.wait_for(on_boot_complete_future, timeout=2.0)
assert on_boot_completed, (
"on_boot wait_until did not complete after flag was set"
)
except TimeoutError:
pytest.fail(
"wait_until in on_boot did not complete within 2s after condition became true. "
"This indicates the bug where WaitUntilAction::setup() disables the loop "
"after play_complex() has already enabled it."
)

View File

@@ -355,7 +355,6 @@ def test_clean_build(
mock_core.relative_pioenvs_path.return_value = pioenvs_dir
mock_core.relative_piolibdeps_path.return_value = piolibdeps_dir
mock_core.relative_build_path.return_value = dependencies_lock
mock_core.platformio_cache_dir = str(platformio_cache_dir)
# Verify all exist before
assert pioenvs_dir.exists()