1
0
mirror of https://github.com/esphome/esphome.git synced 2025-11-12 04:45:47 +00:00

Compare commits

..

24 Commits

Author SHA1 Message Date
Jonathan Swoboda
1d71b6b93e Merge pull request #11862 from esphome/bump-2025.11.0b1
2025.11.0b1
2025-11-11 23:27:12 -05: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
tomaszduda23
661920c51e [nrf52,ssd1306_i2c] fix build error (#11847) 2025-11-11 18:18:17 +00:00
tomaszduda23
a6b905e148 [nrf52,pcf8563] fix build error (#11846)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
Co-authored-by: J. Nick Koston <nick+github@koston.org>
2025-11-11 17:50:07 +00:00
tomaszduda23
a6b7c1f18c [nrf52,gpio] add gpio levels for high voltage mode (#9858)
Co-authored-by: J. Nick Koston <nick+github@koston.org>
2025-11-11 15:17:25 +00:00
46 changed files with 805 additions and 173 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

@@ -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

@@ -23,7 +23,7 @@ void DS1307Component::dump_config() {
if (this->is_failed()) {
ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL);
}
ESP_LOGCONFIG(TAG, " Timezone: '%s'", this->timezone_.c_str());
RealTimeClock::dump_config();
}
float DS1307Component::get_setup_priority() const { return setup_priority::DATA; }

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

@@ -7,10 +7,8 @@ namespace homeassistant {
static const char *const TAG = "homeassistant.time";
void HomeassistantTime::dump_config() {
ESP_LOGCONFIG(TAG,
"Home Assistant Time:\n"
" Timezone: '%s'",
this->timezone_.c_str());
ESP_LOGCONFIG(TAG, "Home Assistant Time");
RealTimeClock::dump_config();
}
float HomeassistantTime::get_setup_priority() const { return setup_priority::DATA; }

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

@@ -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

@@ -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

@@ -25,6 +25,7 @@ from esphome.const import (
CONF_FRAMEWORK,
CONF_ID,
CONF_RESET_PIN,
CONF_VOLTAGE,
KEY_CORE,
KEY_FRAMEWORK_VERSION,
KEY_TARGET_FRAMEWORK,
@@ -102,6 +103,10 @@ nrf52_ns = cg.esphome_ns.namespace("nrf52")
DeviceFirmwareUpdate = nrf52_ns.class_("DeviceFirmwareUpdate", cg.Component)
CONF_DFU = "dfu"
CONF_REG0 = "reg0"
CONF_UICR_ERASE = "uicr_erase"
VOLTAGE_LEVELS = [1.8, 2.1, 2.4, 2.7, 3.0, 3.3]
CONFIG_SCHEMA = cv.All(
_detect_bootloader,
@@ -116,6 +121,15 @@ CONFIG_SCHEMA = cv.All(
cv.Required(CONF_RESET_PIN): pins.gpio_output_pin_schema,
}
),
cv.Optional(CONF_REG0): cv.Schema(
{
cv.Required(CONF_VOLTAGE): cv.All(
cv.voltage,
cv.one_of(*VOLTAGE_LEVELS, float=True),
),
cv.Optional(CONF_UICR_ERASE, default=False): cv.boolean,
}
),
}
),
)
@@ -183,6 +197,12 @@ async def to_code(config: ConfigType) -> None:
if dfu_config := config.get(CONF_DFU):
CORE.add_job(_dfu_to_code, dfu_config)
if reg0_config := config.get(CONF_REG0):
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")
@coroutine_with_priority(CoroPriority.DIAGNOSTICS)
async def _dfu_to_code(dfu_config):

View File

@@ -0,0 +1,121 @@
#include "esphome/core/defines.h"
#ifdef USE_NRF52_REG0_VOUT
#include <zephyr/init.h>
#include <hal/nrf_power.h>
#include <zephyr/sys/printk.h>
extern "C" {
void nvmc_config(uint32_t mode);
void nvmc_wait();
nrfx_err_t nrfx_nvmc_uicr_erase();
}
namespace esphome::nrf52 {
enum class StatusFlags : uint8_t {
OK = 0x00,
NEED_RESET = 0x01,
NEED_ERASE = 0x02,
};
constexpr StatusFlags &operator|=(StatusFlags &a, StatusFlags b) {
a = static_cast<StatusFlags>(static_cast<uint8_t>(a) | static_cast<uint8_t>(b));
return a;
}
constexpr bool operator&(StatusFlags a, StatusFlags b) {
return (static_cast<uint8_t>(a) & static_cast<uint8_t>(b)) != 0;
}
static bool regout0_ok() {
return (NRF_UICR->REGOUT0 & UICR_REGOUT0_VOUT_Msk) == (USE_NRF52_REG0_VOUT << UICR_REGOUT0_VOUT_Pos);
}
static StatusFlags set_regout0() {
/* If the board is powered from USB (high voltage mode),
* GPIO output voltage is set to 1.8 volts by default.
*/
if (!regout0_ok()) {
nvmc_config(NVMC_CONFIG_WEN_Wen);
NRF_UICR->REGOUT0 =
(NRF_UICR->REGOUT0 & ~((uint32_t) UICR_REGOUT0_VOUT_Msk)) | (USE_NRF52_REG0_VOUT << UICR_REGOUT0_VOUT_Pos);
nvmc_wait();
nvmc_config(NVMC_CONFIG_WEN_Ren);
return regout0_ok() ? StatusFlags::NEED_RESET : StatusFlags::NEED_ERASE;
}
return StatusFlags::OK;
}
#ifndef USE_BOOTLOADER_MCUBOOT
// https://github.com/adafruit/Adafruit_nRF52_Bootloader/blob/6a9a6a3e6d0f86918e9286188426a279976645bd/lib/sdk11/components/libraries/bootloader_dfu/dfu_types.h#L61
constexpr uint32_t BOOTLOADER_REGION_START = 0x000F4000;
constexpr uint32_t BOOTLOADER_MBR_PARAMS_PAGE_ADDRESS = 0x000FE000;
static bool bootloader_ok() {
return NRF_UICR->NRFFW[0] == BOOTLOADER_REGION_START && NRF_UICR->NRFFW[1] == BOOTLOADER_MBR_PARAMS_PAGE_ADDRESS;
}
static StatusFlags fix_bootloader() {
if (!bootloader_ok()) {
nvmc_config(NVMC_CONFIG_WEN_Wen);
NRF_UICR->NRFFW[0] = BOOTLOADER_REGION_START;
NRF_UICR->NRFFW[1] = BOOTLOADER_MBR_PARAMS_PAGE_ADDRESS;
nvmc_wait();
nvmc_config(NVMC_CONFIG_WEN_Ren);
return bootloader_ok() ? StatusFlags::NEED_RESET : StatusFlags::NEED_ERASE;
}
return StatusFlags::OK;
}
#endif
#define BOOTLOADER_VERSION_REGISTER NRF_TIMER2->CC[0]
static StatusFlags set_uicr() {
StatusFlags status = StatusFlags::OK;
#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
return status;
}
static int board_esphome_init() {
StatusFlags status = set_uicr();
#ifdef USE_NRF52_UICR_ERASE
if (status & StatusFlags::NEED_ERASE) {
nrfx_err_t ret = nrfx_nvmc_uicr_erase();
if (ret != NRFX_SUCCESS) {
#ifdef CONFIG_PRINTK
printk("nrfx_nvmc_uicr_erase failed %d\n", ret);
#endif
} else {
status |= set_uicr();
}
}
#endif
if (status & StatusFlags::NEED_RESET) {
/* a reset is required for changes to take effect */
NVIC_SystemReset();
}
return 0;
}
} // namespace esphome::nrf52
static int board_esphome_init() { return esphome::nrf52::board_esphome_init(); }
SYS_INIT(board_esphome_init, PRE_KERNEL_1, CONFIG_KERNEL_INIT_PRIORITY_DEFAULT);
#endif

View File

@@ -23,7 +23,7 @@ void PCF85063Component::dump_config() {
if (this->is_failed()) {
ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL);
}
ESP_LOGCONFIG(TAG, " Timezone: '%s'", this->timezone_.c_str());
RealTimeClock::dump_config();
}
float PCF85063Component::get_setup_priority() const { return setup_priority::DATA; }

View File

@@ -23,7 +23,7 @@ void PCF8563Component::dump_config() {
if (this->is_failed()) {
ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL);
}
ESP_LOGCONFIG(TAG, " Timezone: '%s'", this->timezone_.c_str());
RealTimeClock::dump_config();
}
float PCF8563Component::get_setup_priority() const { return setup_priority::DATA; }

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

@@ -62,6 +62,7 @@ void RX8130Component::update() { this->read_time(); }
void RX8130Component::dump_config() {
ESP_LOGCONFIG(TAG, "RX8130:");
LOG_I2C_DEVICE(this);
RealTimeClock::dump_config();
}
void RX8130Component::read_time() {

View File

@@ -61,6 +61,7 @@ void SNTPComponent::dump_config() {
for (auto &server : this->servers_) {
ESP_LOGCONFIG(TAG, " Server %zu: '%s'", i++, server);
}
RealTimeClock::dump_config();
}
void SNTPComponent::update() {
#if !defined(USE_ESP32)

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

@@ -23,6 +23,13 @@ namespace time {
static const char *const TAG = "time";
RealTimeClock::RealTimeClock() = default;
void RealTimeClock::dump_config() {
#ifdef USE_TIME_TIMEZONE
ESP_LOGCONFIG(TAG, "Timezone: '%s'", this->timezone_.c_str());
#endif
}
void RealTimeClock::synchronize_epoch_(uint32_t epoch) {
ESP_LOGVV(TAG, "Got epoch %" PRIu32, epoch);
// Update UTC epoch time.

View File

@@ -52,6 +52,8 @@ class RealTimeClock : public PollingComponent {
this->time_sync_callback_.add(std::move(callback));
};
void dump_config() override;
protected:
/// Report a unix epoch as current time.
void synchronize_epoch_(uint32_t epoch);

View File

@@ -1,9 +1,15 @@
import logging
from esphome import automation
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
@@ -42,6 +48,7 @@ from esphome.const import (
CONF_TTLS_PHASE_2,
CONF_USE_ADDRESS,
CONF_USERNAME,
Platform,
PlatformFramework,
)
from esphome.core import CORE, CoroPriority, HexInt, coroutine_with_priority
@@ -49,10 +56,15 @@ import esphome.final_validate as fv
from . import wpa2_eap
_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"
# Maximum number of WiFi networks that can be configured
# Limited to 127 because selected_sta_index_ is int8_t in C++
@@ -70,6 +82,14 @@ WIFI_POWER_SAVE_MODES = {
"LIGHT": WiFiPowerSaveMode.WIFI_POWER_SAVE_LIGHT,
"HIGH": WiFiPowerSaveMode.WIFI_POWER_SAVE_HIGH,
}
WifiMinAuthMode = wifi_ns.enum("WifiMinAuthMode")
WIFI_MIN_AUTH_MODES = {
"WPA": WifiMinAuthMode.WIFI_MIN_AUTH_MODE_WPA,
"WPA2": WifiMinAuthMode.WIFI_MIN_AUTH_MODE_WPA2,
"WPA3": WifiMinAuthMode.WIFI_MIN_AUTH_MODE_WPA3,
}
VALIDATE_WIFI_MIN_AUTH_MODE = cv.enum(WIFI_MIN_AUTH_MODES, upper=True)
WiFiConnectedCondition = wifi_ns.class_("WiFiConnectedCondition", Condition)
WiFiEnabledCondition = wifi_ns.class_("WiFiEnabledCondition", Condition)
WiFiEnableAction = wifi_ns.class_("WiFiEnableAction", automation.Action)
@@ -174,7 +194,7 @@ WIFI_NETWORK_STA = WIFI_NETWORK_BASE.extend(
{
cv.Optional(CONF_BSSID): cv.mac_address,
cv.Optional(CONF_HIDDEN): cv.boolean,
cv.Optional(CONF_PRIORITY, default=0.0): cv.float_,
cv.Optional(CONF_PRIORITY, default=0): cv.int_range(min=-128, max=127),
cv.Optional(CONF_EAP): EAP_AUTH_SCHEMA,
}
)
@@ -187,6 +207,27 @@ def validate_variant(_):
raise cv.Invalid(f"WiFi requires component esp32_hosted on {variant}")
def _apply_min_auth_mode_default(config):
"""Apply platform-specific default for min_auth_mode and warn ESP8266 users."""
# Only apply defaults for platforms that support min_auth_mode
if CONF_MIN_AUTH_MODE not in config and (CORE.is_esp8266 or CORE.is_esp32):
if CORE.is_esp8266:
_LOGGER.warning(
"The minimum WiFi authentication mode (wifi -> min_auth_mode) is not set. "
"This controls the weakest encryption your device will accept when connecting to WiFi. "
"Currently defaults to WPA (less secure), but will change to WPA2 (more secure) in 2026.6.0. "
"WPA uses TKIP encryption which has known security vulnerabilities and should be avoided. "
"WPA2 uses AES encryption which is significantly more secure. "
"To silence this warning, explicitly set min_auth_mode under 'wifi:'. "
"If your router supports WPA2 or WPA3, set 'min_auth_mode: WPA2'. "
"If your router only supports WPA, set 'min_auth_mode: WPA'."
)
config[CONF_MIN_AUTH_MODE] = VALIDATE_WIFI_MIN_AUTH_MODE("WPA")
elif CORE.is_esp32:
config[CONF_MIN_AUTH_MODE] = VALIDATE_WIFI_MIN_AUTH_MODE("WPA2")
return config
def final_validate(config):
has_sta = bool(config.get(CONF_NETWORKS, True))
has_ap = CONF_AP in config
@@ -287,6 +328,10 @@ CONFIG_SCHEMA = cv.All(
): cv.enum(WIFI_POWER_SAVE_MODES, upper=True),
cv.Optional(CONF_FAST_CONNECT, default=False): cv.boolean,
cv.Optional(CONF_USE_ADDRESS): cv.string_strict,
cv.Optional(CONF_MIN_AUTH_MODE): cv.All(
VALIDATE_WIFI_MIN_AUTH_MODE,
cv.only_on([Platform.ESP32, Platform.ESP8266]),
),
cv.SplitDefault(CONF_OUTPUT_POWER, esp8266=20.0): cv.All(
cv.decibel, cv.float_range(min=8.5, max=20.5)
),
@@ -311,6 +356,7 @@ CONFIG_SCHEMA = cv.All(
),
}
),
_apply_min_auth_mode_default,
_validate,
)
@@ -385,6 +431,8 @@ async def to_code(config):
# Track if any network uses Enterprise authentication
has_eap = False
# Track if any network uses manual IP
has_manual_ip = False
# Initialize FixedVector with the count of networks
networks = config.get(CONF_NETWORKS, [])
@@ -398,11 +446,15 @@ async def to_code(config):
for network in networks:
if CONF_EAP in network:
has_eap = True
if network.get(CONF_MANUAL_IP) or config.get(CONF_MANUAL_IP):
has_manual_ip = True
cg.with_local_variable(network[CONF_ID], WiFiAP(), add_sta, network)
if CONF_AP in config:
conf = config[CONF_AP]
ip_config = conf.get(CONF_MANUAL_IP)
if ip_config:
has_manual_ip = True
cg.with_local_variable(
conf[CONF_ID],
WiFiAP(),
@@ -418,8 +470,14 @@ async def to_code(config):
if CORE.is_esp32:
add_idf_sdkconfig_option("CONFIG_ESP_WIFI_ENTERPRISE_SUPPORT", has_eap)
# Only define USE_WIFI_MANUAL_IP if any AP uses manual IP
if has_manual_ip:
cg.add_define("USE_WIFI_MANUAL_IP")
cg.add(var.set_reboot_timeout(config[CONF_REBOOT_TIMEOUT]))
cg.add(var.set_power_save_mode(config[CONF_POWER_SAVE_MODE]))
if CONF_MIN_AUTH_MODE in config:
cg.add(var.set_min_auth_mode(config[CONF_MIN_AUTH_MODE]))
if config[CONF_FAST_CONNECT]:
cg.add_define("USE_WIFI_FAST_CONNECT")
cg.add(var.set_passive_scan(config[CONF_PASSIVE_SCAN]))
@@ -444,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:
@@ -253,17 +257,19 @@ bool WiFiComponent::ssid_was_seen_in_scan_(const std::string &ssid) const {
return false;
}
int8_t WiFiComponent::find_next_hidden_sta_(int8_t start_index, bool include_explicit_hidden) {
int8_t WiFiComponent::find_next_hidden_sta_(int8_t start_index) {
// Find next SSID that wasn't in scan results (might be hidden)
bool include_explicit_hidden = !this->went_through_explicit_hidden_phase_();
// Start searching from start_index + 1
for (size_t i = start_index + 1; i < this->sta_.size(); i++) {
const auto &sta = this->sta_[i];
// Skip networks that were already tried in EXPLICIT_HIDDEN phase
// Those are: networks marked hidden:true that appear before the first non-hidden network
// If all networks are hidden (first_non_hidden_idx == -1), skip all of them
if (!include_explicit_hidden && sta.get_hidden()) {
int8_t first_non_hidden_idx = this->find_first_non_hidden_index_();
if (first_non_hidden_idx >= 0 && static_cast<int8_t>(i) < first_non_hidden_idx) {
if (first_non_hidden_idx < 0 || static_cast<int8_t>(i) < first_non_hidden_idx) {
ESP_LOGD(TAG, "Skipping " LOG_SECRET("'%s'") " (explicit hidden, already tried)", sta.get_ssid().c_str());
continue;
}
@@ -273,7 +279,7 @@ int8_t WiFiComponent::find_next_hidden_sta_(int8_t start_index, bool include_exp
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;
@@ -287,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();
@@ -369,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_();
@@ -411,8 +417,11 @@ void WiFiComponent::start() {
void WiFiComponent::restart_adapter() {
ESP_LOGW(TAG, "Restarting adapter");
this->wifi_mode_(false, {});
delay(100); // NOLINT
// Enter cooldown state to allow WiFi hardware to stabilize after restart
// 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() {
@@ -432,20 +441,12 @@ 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) {
// After cooldown, connect based on current retry phase
this->reset_selected_ap_to_first_if_invalid_();
// Check if we need to trigger a scan first
if (this->needs_scan_results_() && !this->all_networks_hidden_()) {
// Need scan results or no matching networks found - scan/rescan
ESP_LOGD(TAG, "Scanning required for phase %s", LOG_STR_ARG(retry_phase_to_log_string(this->retry_phase_)));
this->start_scanning();
} else {
// Have everything we need to connect (or all networks are hidden, skip scanning)
WiFiAP params = this->build_params_for_current_phase_();
this->start_connecting(params, false);
}
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
// check_connecting_finished to continue the state machine.
this->check_connecting_finished();
}
break;
}
@@ -454,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;
@@ -569,6 +569,7 @@ void WiFiComponent::setup_ap_config_() {
" IP Address: %s",
this->ap_.get_ssid().c_str(), this->ap_.get_password().c_str(), ip_address.c_str());
#ifdef USE_WIFI_MANUAL_IP
auto manual_ip = this->ap_.get_manual_ip();
if (manual_ip.has_value()) {
ESP_LOGCONFIG(TAG,
@@ -578,6 +579,7 @@ void WiFiComponent::setup_ap_config_() {
manual_ip->static_ip.str().c_str(), manual_ip->gateway.str().c_str(),
manual_ip->subnet.str().c_str());
}
#endif
if (!this->has_sta()) {
this->state_ = WIFI_COMPONENT_STATE_AP;
@@ -664,10 +666,10 @@ 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;
float priority = 0.0f;
int8_t priority = 0;
if (ap.get_bssid().has_value()) {
bssid_formatted = format_mac_address_pretty(ap.get_bssid().value().data());
@@ -675,7 +677,7 @@ void WiFiComponent::start_connecting(const WiFiAP &ap, bool two) {
}
ESP_LOGI(TAG,
"Connecting to " LOG_SECRET("'%s'") " " LOG_SECRET("(%s)") " (priority %.1f, attempt %u/%u in phase %s)...",
"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_),
LOG_STR_ARG(retry_phase_to_log_string(this->retry_phase_)));
@@ -716,11 +718,14 @@ void WiFiComponent::start_connecting(const WiFiAP &ap, bool two) {
} else {
ESP_LOGV(TAG, " Channel not set");
}
#ifdef USE_WIFI_MANUAL_IP
if (ap.get_manual_ip().has_value()) {
ManualIP m = *ap.get_manual_ip();
ESP_LOGV(TAG, " Manual IP: Static IP=%s Gateway=%s Subnet=%s DNS1=%s DNS2=%s", m.static_ip.str().c_str(),
m.gateway.str().c_str(), m.subnet.str().c_str(), m.dns1.str().c_str(), m.dns2.str().c_str());
} else {
} else
#endif
{
ESP_LOGV(TAG, " Using DHCP IP");
}
ESP_LOGV(TAG, " Hidden: %s", YESNO(ap.get_hidden()));
@@ -728,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();
}
@@ -812,7 +814,7 @@ void WiFiComponent::print_connect_params_() {
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: %.1f", this->get_sta_priority(*config->get_bssid()));
ESP_LOGV(TAG, " Priority: %d", this->get_sta_priority(*config->get_bssid()));
}
#endif
#ifdef USE_WIFI_11KV_SUPPORT
@@ -933,8 +935,7 @@ __attribute__((noinline)) static void log_scan_result(const WiFiScanResult &res)
ESP_LOGI(TAG, "- '%s' %s" LOG_SECRET("(%s) ") "%s", res.get_ssid().c_str(),
res.get_is_hidden() ? LOG_STR_LITERAL("(HIDDEN) ") : LOG_STR_LITERAL(""), bssid_s,
LOG_STR_ARG(get_signal_bars(res.get_rssi())));
ESP_LOGD(TAG, " Channel: %2u, RSSI: %3d dB, Priority: %4.1f", res.get_channel(), res.get_rssi(),
res.get_priority());
ESP_LOGD(TAG, " Channel: %2u, RSSI: %3d dB, Priority: %4d", res.get_channel(), res.get_rssi(), res.get_priority());
} else {
ESP_LOGD(TAG, "- " LOG_SECRET("'%s'") " " LOG_SECRET("(%s) ") "%s", res.get_ssid().c_str(), bssid_s,
LOG_STR_ARG(get_signal_bars(res.get_rssi())));
@@ -1003,6 +1004,10 @@ void WiFiComponent::check_scanning_finished() {
// No scan results matched our configured networks - transition directly to hidden mode
// Don't call retry_connect() since we never attempted a connection (no BSSID to penalize)
this->transition_to_phase_(WiFiRetryPhase::RETRY_HIDDEN);
// If no hidden networks to try, skip connection attempt (will be handled on next loop)
if (this->selected_sta_index_ == -1) {
return;
}
// Now start connection attempt in hidden mode
} else if (this->transition_to_phase_(WiFiRetryPhase::SCAN_CONNECTING)) {
return; // scan started, wait for next loop iteration
@@ -1013,7 +1018,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() {
@@ -1063,6 +1068,9 @@ void WiFiComponent::check_connecting_finished() {
this->state_ = WIFI_COMPONENT_STATE_STA_CONNECTED;
this->num_retried_ = 0;
// Clear priority tracking if all priorities are at minimum
this->clear_priorities_if_all_min_();
#ifdef USE_WIFI_FAST_CONNECT
this->save_fast_connect_settings_();
#endif
@@ -1084,7 +1092,7 @@ void WiFiComponent::check_connecting_finished() {
}
if (this->error_from_callback_) {
ESP_LOGW(TAG, "Connecting to network failed");
ESP_LOGW(TAG, "Connecting to network failed (callback)");
this->retry_connect();
return;
}
@@ -1142,7 +1150,12 @@ WiFiRetryPhase WiFiComponent::determine_next_phase_() {
return WiFiRetryPhase::EXPLICIT_HIDDEN;
}
// No more consecutive explicitly hidden networks - proceed to scanning
// No more consecutive explicitly hidden networks
// If ALL networks are hidden, skip scanning and go directly to restart
if (this->find_first_non_hidden_index_() < 0) {
return WiFiRetryPhase::RESTARTING_ADAPTER;
}
// Otherwise proceed to scanning for non-hidden networks
return WiFiRetryPhase::SCAN_CONNECTING;
}
@@ -1160,14 +1173,12 @@ WiFiRetryPhase WiFiComponent::determine_next_phase_() {
// Its priority has been decreased, so on next scan it will be sorted lower
// and we'll try the next best BSSID.
// Check if there are any potentially hidden networks to try
if (this->find_next_hidden_sta_(-1, !this->went_through_explicit_hidden_phase_()) >= 0) {
if (this->find_next_hidden_sta_(-1) >= 0) {
return WiFiRetryPhase::RETRY_HIDDEN; // Found hidden networks to try
}
// No hidden networks - skip directly to restart/rescan
if (this->is_captive_portal_active_() || this->is_esp32_improv_active_()) {
return this->went_through_explicit_hidden_phase_() ? WiFiRetryPhase::EXPLICIT_HIDDEN
: WiFiRetryPhase::SCAN_CONNECTING;
}
// No hidden networks - always go through RESTARTING_ADAPTER phase
// This ensures num_retried_ gets reset and a fresh scan is triggered
// The actual adapter restart will be skipped if captive portal/improv is active
return WiFiRetryPhase::RESTARTING_ADAPTER;
case WiFiRetryPhase::RETRY_HIDDEN:
@@ -1179,20 +1190,18 @@ WiFiRetryPhase WiFiComponent::determine_next_phase_() {
// Exhausted retries on current SSID - check if there are more potentially hidden SSIDs to try
if (this->selected_sta_index_ < static_cast<int8_t>(this->sta_.size()) - 1) {
// More SSIDs available - stay in RETRY_HIDDEN, advance will happen in retry_connect()
return WiFiRetryPhase::RETRY_HIDDEN;
// Check if find_next_hidden_sta_() would actually find another hidden SSID
// as it might have been seen in the scan results and we want to skip those
// otherwise we will get stuck in RETRY_HIDDEN phase
if (this->find_next_hidden_sta_(this->selected_sta_index_) != -1) {
// More hidden SSIDs available - stay in RETRY_HIDDEN, advance will happen in retry_connect()
return WiFiRetryPhase::RETRY_HIDDEN;
}
}
}
// Exhausted all potentially hidden SSIDs - rescan to try next BSSID
// If captive portal/improv is active, skip adapter restart and go back to start
// Otherwise restart adapter to clear any stuck state
if (this->is_captive_portal_active_() || this->is_esp32_improv_active_()) {
// Go back to explicit hidden if we went through it initially, otherwise scan
return this->went_through_explicit_hidden_phase_() ? WiFiRetryPhase::EXPLICIT_HIDDEN
: WiFiRetryPhase::SCAN_CONNECTING;
}
// Restart adapter
// Exhausted all potentially hidden SSIDs - always go through RESTARTING_ADAPTER
// This ensures num_retried_ gets reset and a fresh scan is triggered
// The actual adapter restart will be skipped if captive portal/improv is active
return WiFiRetryPhase::RESTARTING_ADAPTER;
case WiFiRetryPhase::RESTARTING_ADAPTER:
@@ -1212,8 +1221,8 @@ WiFiRetryPhase WiFiComponent::determine_next_phase_() {
/// - Performing phase-specific initialization (e.g., advancing AP index, starting scans)
///
/// @param new_phase The phase we're transitioning TO
/// @return true if an async scan was started (caller should wait for completion)
/// false if no scan started (caller can proceed with connection attempt)
/// @return true if connection attempt should be skipped (scan started or no networks to try)
/// false if caller can proceed with connection attempt
bool WiFiComponent::transition_to_phase_(WiFiRetryPhase new_phase) {
WiFiRetryPhase old_phase = this->retry_phase_;
@@ -1271,7 +1280,7 @@ bool WiFiComponent::transition_to_phase_(WiFiRetryPhase new_phase) {
// If first network is marked hidden, we went through EXPLICIT_HIDDEN phase
// In that case, skip networks marked hidden:true (already tried)
// Otherwise, include them (they haven't been tried yet)
this->selected_sta_index_ = this->find_next_hidden_sta_(-1, !this->went_through_explicit_hidden_phase_());
this->selected_sta_index_ = this->find_next_hidden_sta_(-1);
if (this->selected_sta_index_ == -1) {
ESP_LOGD(TAG, "All SSIDs visible or already tried, skipping hidden mode");
@@ -1280,7 +1289,12 @@ bool WiFiComponent::transition_to_phase_(WiFiRetryPhase new_phase) {
break;
case WiFiRetryPhase::RESTARTING_ADAPTER:
this->restart_adapter();
// Skip actual adapter restart if captive portal/improv is active
// This allows state machine to reset num_retried_ and trigger fresh scan
// without disrupting the captive portal/improv connection
if (!this->is_captive_portal_active_() && !this->is_esp32_improv_active_()) {
this->restart_adapter();
}
// Return true to indicate we should wait (go to COOLDOWN) instead of immediately connecting
return true;
@@ -1291,6 +1305,34 @@ bool WiFiComponent::transition_to_phase_(WiFiRetryPhase new_phase) {
return false; // Did not start scan, can proceed with connection
}
/// Clear BSSID priority tracking if all priorities are at minimum (saves memory)
/// At minimum priority, all BSSIDs are equally bad, so priority tracking is useless
/// Called after successful connection or after failed connection attempts
void WiFiComponent::clear_priorities_if_all_min_() {
if (this->sta_priorities_.empty()) {
return;
}
int8_t first_priority = this->sta_priorities_[0].priority;
// Only clear if all priorities have been decremented to the minimum value
// At this point, all BSSIDs have been equally penalized and priority info is useless
if (first_priority != std::numeric_limits<int8_t>::min()) {
return;
}
for (const auto &pri : this->sta_priorities_) {
if (pri.priority != first_priority) {
return; // Not all same, nothing to do
}
}
// All priorities are at minimum - clear the vector to save memory and reset
ESP_LOGD(TAG, "Clearing BSSID priorities (all at minimum)");
this->sta_priorities_.clear();
this->sta_priorities_.shrink_to_fit();
}
/// Log failed connection attempt and decrease BSSID priority to avoid repeated failures
/// This function identifies which BSSID was attempted (from scan results or config),
/// decreases its priority by 1.0 to discourage future attempts, and logs the change.
@@ -1304,6 +1346,11 @@ bool WiFiComponent::transition_to_phase_(WiFiRetryPhase new_phase) {
/// - Other phases: Uses BSSID from config if explicitly specified by user or fast_connect
///
/// If no BSSID is available (SSID-only connection), priority adjustment is skipped.
///
/// IMPORTANT: Priority is only decreased on the LAST attempt for a BSSID in SCAN_CONNECTING phase.
/// This prevents false positives from transient WiFi stack state issues after scanning.
/// Single failures don't necessarily mean the AP is bad - two genuine failures provide
/// higher confidence before degrading priority and skipping the BSSID in future scans.
void WiFiComponent::log_and_adjust_priority_for_failed_connect_() {
// Determine which BSSID we tried to connect to
optional<bssid_t> failed_bssid;
@@ -1320,11 +1367,6 @@ void WiFiComponent::log_and_adjust_priority_for_failed_connect_() {
return; // No BSSID to penalize
}
// Decrease priority to avoid repeatedly trying the same failed BSSID
float old_priority = this->get_sta_priority(failed_bssid.value());
float new_priority = old_priority - 1.0f;
this->set_sta_priority(failed_bssid.value(), new_priority);
// Get SSID for logging
std::string ssid;
if (this->retry_phase_ == WiFiRetryPhase::SCAN_CONNECTING && !this->scan_result_.empty()) {
@@ -1333,8 +1375,27 @@ void WiFiComponent::log_and_adjust_priority_for_failed_connect_() {
ssid = config->get_ssid();
}
ESP_LOGD(TAG, "Failed " LOG_SECRET("'%s'") " " LOG_SECRET("(%s)") ", priority %.1f → %.1f", ssid.c_str(),
// Only decrease priority on the last attempt for this phase
// This prevents false positives from transient WiFi stack issues
uint8_t max_retries = get_max_retries_for_phase(this->retry_phase_);
bool is_last_attempt = (this->num_retried_ + 1 >= max_retries);
// Decrease priority only on last attempt to avoid false positives from transient failures
int8_t old_priority = this->get_sta_priority(failed_bssid.value());
int8_t new_priority = old_priority;
if (is_last_attempt) {
// Decrease priority, but clamp to int8_t::min to prevent overflow
new_priority =
(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);
// After adjusting priority, check if all priorities are now at minimum
// If so, clear the vector to save memory and reset for fresh start
this->clear_priorities_if_all_min_();
}
/// Handle target advancement or retry counter increment when staying in the same phase
@@ -1379,8 +1440,7 @@ void WiFiComponent::advance_to_next_target_or_increment_retry_() {
// If first network is marked hidden, we went through EXPLICIT_HIDDEN phase
// In that case, skip networks marked hidden:true (already tried)
// Otherwise, include them (they haven't been tried yet)
int8_t next_index =
this->find_next_hidden_sta_(this->selected_sta_index_, !this->went_through_explicit_hidden_phase_());
int8_t next_index = this->find_next_hidden_sta_(this->selected_sta_index_);
if (next_index != -1) {
// Found another potentially hidden SSID
this->selected_sta_index_ = next_index;
@@ -1408,15 +1468,13 @@ void WiFiComponent::advance_to_next_target_or_increment_retry_() {
void WiFiComponent::retry_connect() {
this->log_and_adjust_priority_for_failed_connect_();
delay(10);
// Determine next retry phase based on current state
WiFiRetryPhase current_phase = this->retry_phase_;
WiFiRetryPhase next_phase = this->determine_next_phase_();
// Handle phase transitions (transition_to_phase_ handles same-phase no-op internally)
if (this->transition_to_phase_(next_phase)) {
return; // Wait for scan to complete
return; // Scan started or adapter restarted (which sets its own state)
}
if (next_phase == current_phase) {
@@ -1425,22 +1483,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;
}
// No valid target - fall through to set state to allow phase transition
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);
}
this->state_ = WIFI_COMPONENT_STATE_COOLDOWN;
this->action_started_ = millis();
}
void WiFiComponent::set_reboot_timeout(uint32_t reboot_timeout) { this->reboot_timeout_ = reboot_timeout; }
@@ -1532,7 +1582,9 @@ void WiFiAP::set_password(const std::string &password) { this->password_ = passw
void WiFiAP::set_eap(optional<EAPAuth> eap_auth) { this->eap_ = std::move(eap_auth); }
#endif
void WiFiAP::set_channel(optional<uint8_t> channel) { this->channel_ = channel; }
#ifdef USE_WIFI_MANUAL_IP
void WiFiAP::set_manual_ip(optional<ManualIP> manual_ip) { this->manual_ip_ = manual_ip; }
#endif
void WiFiAP::set_hidden(bool hidden) { this->hidden_ = hidden; }
const std::string &WiFiAP::get_ssid() const { return this->ssid_; }
const optional<bssid_t> &WiFiAP::get_bssid() const { return this->bssid_; }
@@ -1541,15 +1593,17 @@ const std::string &WiFiAP::get_password() const { return this->password_; }
const optional<EAPAuth> &WiFiAP::get_eap() const { return this->eap_; }
#endif
const optional<uint8_t> &WiFiAP::get_channel() const { return this->channel_; }
#ifdef USE_WIFI_MANUAL_IP
const optional<ManualIP> &WiFiAP::get_manual_ip() const { return this->manual_ip_; }
#endif
bool WiFiAP::get_hidden() const { return this->hidden_; }
WiFiScanResult::WiFiScanResult(const bssid_t &bssid, std::string ssid, uint8_t channel, int8_t rssi, bool with_auth,
bool is_hidden)
: bssid_(bssid),
ssid_(std::move(ssid)),
channel_(channel),
rssi_(rssi),
ssid_(std::move(ssid)),
with_auth_(with_auth),
is_hidden_(is_hidden) {}
bool WiFiScanResult::matches(const WiFiAP &config) const {

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. */
@@ -157,8 +151,10 @@ class WiFiAP {
void set_eap(optional<EAPAuth> eap_auth);
#endif // USE_WIFI_WPA2_EAP
void set_channel(optional<uint8_t> channel);
void set_priority(float priority) { priority_ = priority; }
void set_priority(int8_t priority) { priority_ = priority; }
#ifdef USE_WIFI_MANUAL_IP
void set_manual_ip(optional<ManualIP> manual_ip);
#endif
void set_hidden(bool hidden);
const std::string &get_ssid() const;
const optional<bssid_t> &get_bssid() const;
@@ -167,8 +163,10 @@ class WiFiAP {
const optional<EAPAuth> &get_eap() const;
#endif // USE_WIFI_WPA2_EAP
const optional<uint8_t> &get_channel() const;
float get_priority() const { return priority_; }
int8_t get_priority() const { return priority_; }
#ifdef USE_WIFI_MANUAL_IP
const optional<ManualIP> &get_manual_ip() const;
#endif
bool get_hidden() const;
protected:
@@ -178,9 +176,11 @@ class WiFiAP {
#ifdef USE_WIFI_WPA2_EAP
optional<EAPAuth> eap_;
#endif // USE_WIFI_WPA2_EAP
#ifdef USE_WIFI_MANUAL_IP
optional<ManualIP> manual_ip_;
float priority_{0};
#endif
optional<uint8_t> channel_;
int8_t priority_{0};
bool hidden_{false};
};
@@ -198,17 +198,17 @@ class WiFiScanResult {
int8_t get_rssi() const;
bool get_with_auth() const;
bool get_is_hidden() const;
float get_priority() const { return priority_; }
void set_priority(float priority) { priority_ = priority; }
int8_t get_priority() const { return priority_; }
void set_priority(int8_t priority) { priority_ = priority; }
bool operator==(const WiFiScanResult &rhs) const;
protected:
bssid_t bssid_;
std::string ssid_;
float priority_{0.0f};
uint8_t channel_;
int8_t rssi_;
std::string ssid_;
int8_t priority_{0};
bool matches_{false};
bool with_auth_;
bool is_hidden_;
@@ -216,7 +216,7 @@ class WiFiScanResult {
struct WiFiSTAPriority {
bssid_t bssid;
float priority;
int8_t priority;
};
enum WiFiPowerSaveMode : uint8_t {
@@ -225,6 +225,12 @@ enum WiFiPowerSaveMode : uint8_t {
WIFI_POWER_SAVE_HIGH,
};
enum WifiMinAuthMode : uint8_t {
WIFI_MIN_AUTH_MODE_WPA = 0,
WIFI_MIN_AUTH_MODE_WPA2,
WIFI_MIN_AUTH_MODE_WPA3,
};
#ifdef USE_ESP32
struct IDFWiFiEvent;
#endif
@@ -263,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();
@@ -274,6 +282,7 @@ class WiFiComponent : public Component {
bool is_connected();
void set_power_save_mode(WiFiPowerSaveMode power_save);
void set_min_auth_mode(WifiMinAuthMode min_auth_mode) { min_auth_mode_ = min_auth_mode; }
void set_output_power(float output_power) { output_power_ = output_power; }
void set_passive_scan(bool passive);
@@ -317,14 +326,14 @@ class WiFiComponent : public Component {
}
return false;
}
float get_sta_priority(const bssid_t bssid) {
int8_t get_sta_priority(const bssid_t bssid) {
for (auto &it : this->sta_priorities_) {
if (it.bssid == bssid)
return it.priority;
}
return 0.0f;
return 0;
}
void set_sta_priority(const bssid_t bssid, float priority) {
void set_sta_priority(const bssid_t bssid, int8_t priority) {
for (auto &it : this->sta_priorities_) {
if (it.bssid == bssid) {
it.priority = priority;
@@ -379,10 +388,11 @@ class WiFiComponent : public Component {
/// Find next SSID that wasn't in scan results (might be hidden)
/// Returns index of next potentially hidden SSID, or -1 if none found
/// @param start_index Start searching from index after this (-1 to start from beginning)
/// @param include_explicit_hidden If true, include SSIDs marked hidden:true. If false, only find truly hidden SSIDs.
int8_t find_next_hidden_sta_(int8_t start_index, bool include_explicit_hidden = true);
int8_t find_next_hidden_sta_(int8_t start_index);
/// Log failed connection and decrease BSSID priority to avoid repeated attempts
void log_and_adjust_priority_for_failed_connect_();
/// Clear BSSID priority tracking if all priorities are at minimum (saves memory)
void clear_priorities_if_all_min_();
/// Advance to next target (AP/SSID) within current phase, or increment retry counter
/// Called when staying in the same phase after a failed connection attempt
void advance_to_next_target_or_increment_retry_();
@@ -489,6 +499,7 @@ class WiFiComponent : public Component {
// Group all 8-bit values together
WiFiComponentState state_{WIFI_COMPONENT_STATE_OFF};
WiFiPowerSaveMode power_save_{WIFI_POWER_SAVE_NONE};
WifiMinAuthMode min_auth_mode_{WIFI_MIN_AUTH_MODE_WPA2};
WiFiRetryPhase retry_phase_{WiFiRetryPhase::INITIAL_CONNECT};
uint8_t num_retried_{0};
// Index into sta_ array for the currently selected AP configuration (-1 = none selected)

View File

@@ -258,8 +258,17 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) {
if (ap.get_password().empty()) {
conf.threshold.authmode = AUTH_OPEN;
} else {
// Only allow auth modes with at least WPA
conf.threshold.authmode = AUTH_WPA_PSK;
// Set threshold based on configured minimum auth mode
// Note: ESP8266 doesn't support WPA3
switch (this->min_auth_mode_) {
case WIFI_MIN_AUTH_MODE_WPA:
conf.threshold.authmode = AUTH_WPA_PSK;
break;
case WIFI_MIN_AUTH_MODE_WPA2:
case WIFI_MIN_AUTH_MODE_WPA3: // Fall back to WPA2 for ESP8266
conf.threshold.authmode = AUTH_WPA2_PSK;
break;
}
}
conf.threshold.rssi = -127;
#endif
@@ -273,9 +282,15 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) {
return false;
}
#ifdef USE_WIFI_MANUAL_IP
if (!this->wifi_sta_ip_config_(ap.get_manual_ip())) {
return false;
}
#else
if (!this->wifi_sta_ip_config_({})) {
return false;
}
#endif
// setup enterprise authentication if required
#ifdef USE_WIFI_WPA2_EAP
@@ -823,10 +838,17 @@ bool WiFiComponent::wifi_start_ap_(const WiFiAP &ap) {
return false;
}
#ifdef USE_WIFI_MANUAL_IP
if (!this->wifi_ap_ip_config_(ap.get_manual_ip())) {
ESP_LOGV(TAG, "wifi_ap_ip_config_ failed");
return false;
}
#else
if (!this->wifi_ap_ip_config_({})) {
ESP_LOGV(TAG, "wifi_ap_ip_config_ failed");
return false;
}
#endif
return true;
}

View File

@@ -308,7 +308,18 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) {
if (ap.get_password().empty()) {
conf.sta.threshold.authmode = WIFI_AUTH_OPEN;
} else {
conf.sta.threshold.authmode = WIFI_AUTH_WPA_WPA2_PSK;
// Set threshold based on configured minimum auth mode
switch (this->min_auth_mode_) {
case WIFI_MIN_AUTH_MODE_WPA:
conf.sta.threshold.authmode = WIFI_AUTH_WPA_PSK;
break;
case WIFI_MIN_AUTH_MODE_WPA2:
conf.sta.threshold.authmode = WIFI_AUTH_WPA2_PSK;
break;
case WIFI_MIN_AUTH_MODE_WPA3:
conf.sta.threshold.authmode = WIFI_AUTH_WPA3_PSK;
break;
}
}
#ifdef USE_WIFI_WPA2_EAP
@@ -347,8 +358,6 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) {
// The minimum rssi to accept in the fast scan mode
conf.sta.threshold.rssi = -127;
conf.sta.threshold.authmode = WIFI_AUTH_OPEN;
wifi_config_t current_conf;
esp_err_t err;
err = esp_wifi_get_config(WIFI_IF_STA, &current_conf);
@@ -371,9 +380,15 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) {
return false;
}
#ifdef USE_WIFI_MANUAL_IP
if (!this->wifi_sta_ip_config_(ap.get_manual_ip())) {
return false;
}
#else
if (!this->wifi_sta_ip_config_({})) {
return false;
}
#endif
// setup enterprise authentication if required
#ifdef USE_WIFI_WPA2_EAP
@@ -985,10 +1000,17 @@ bool WiFiComponent::wifi_start_ap_(const WiFiAP &ap) {
return false;
}
#ifdef USE_WIFI_MANUAL_IP
if (!this->wifi_ap_ip_config_(ap.get_manual_ip())) {
ESP_LOGE(TAG, "wifi_ap_ip_config_ failed:");
return false;
}
#else
if (!this->wifi_ap_ip_config_({})) {
ESP_LOGE(TAG, "wifi_ap_ip_config_ failed:");
return false;
}
#endif
return true;
}

View File

@@ -112,9 +112,15 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) {
WiFi.disconnect();
}
#ifdef USE_WIFI_MANUAL_IP
if (!this->wifi_sta_ip_config_(ap.get_manual_ip())) {
return false;
}
#else
if (!this->wifi_sta_ip_config_({})) {
return false;
}
#endif
this->wifi_apply_hostname_();
@@ -445,10 +451,17 @@ bool WiFiComponent::wifi_start_ap_(const WiFiAP &ap) {
if (!this->wifi_mode_({}, true))
return false;
#ifdef USE_WIFI_MANUAL_IP
if (!this->wifi_ap_ip_config_(ap.get_manual_ip())) {
ESP_LOGV(TAG, "wifi_ap_ip_config_ failed");
return false;
}
#else
if (!this->wifi_ap_ip_config_({})) {
ESP_LOGV(TAG, "wifi_ap_ip_config_ failed");
return false;
}
#endif
yield();

View File

@@ -55,8 +55,13 @@ bool WiFiComponent::wifi_apply_power_save_() {
bool WiFiComponent::wifi_apply_output_power_(float output_power) { return true; }
bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) {
#ifdef USE_WIFI_MANUAL_IP
if (!this->wifi_sta_ip_config_(ap.get_manual_ip()))
return false;
#else
if (!this->wifi_sta_ip_config_({}))
return false;
#endif
auto ret = WiFi.begin(ap.get_ssid().c_str(), ap.get_password().c_str());
if (ret != WL_CONNECTED)
@@ -161,10 +166,17 @@ bool WiFiComponent::wifi_ap_ip_config_(optional<ManualIP> manual_ip) {
bool WiFiComponent::wifi_start_ap_(const WiFiAP &ap) {
if (!this->wifi_mode_({}, true))
return false;
#ifdef USE_WIFI_MANUAL_IP
if (!this->wifi_ap_ip_config_(ap.get_manual_ip())) {
ESP_LOGV(TAG, "wifi_ap_ip_config_ failed");
return false;
}
#else
if (!this->wifi_ap_ip_config_({})) {
ESP_LOGV(TAG, "wifi_ap_ip_config_ failed");
return false;
}
#endif
WiFi.beginAP(ap.get_ssid().c_str(), ap.get_password().c_str(), ap.get_channel().value_or(1));

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

@@ -144,6 +144,7 @@
#define USE_TIME_TIMEZONE
#define USE_WIFI
#define USE_WIFI_AP
#define USE_WIFI_MANUAL_IP
#define USE_WIREGUARD
#endif
@@ -215,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
@@ -288,6 +288,8 @@
#ifdef USE_NRF52
#define USE_NRF52_DFU
#define USE_NRF52_REG0_VOUT 5
#define USE_NRF52_UICR_ERASE
#define USE_SOFTDEVICE_ID 7
#define USE_SOFTDEVICE_VERSION 1
#endif

View File

@@ -6,3 +6,7 @@
#ifdef USE_ARDUINO
#include <Arduino.h>
#endif
#ifdef USE_ZEPHYR
#define M_PI 3.14159265358979323846
#endif

View File

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

View File

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

View File

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

View File

@@ -15,3 +15,6 @@ nrf52:
inverted: true
mode:
output: true
reg0:
voltage: 2.1V
uicr_erase: true

View File

@@ -0,0 +1,4 @@
nrf52:
reg0:
voltage: 3.3V
uicr_erase: true

View File

@@ -5,3 +5,5 @@ nrf52:
inverted: true
mode:
output: true
reg0:
voltage: 1.8V

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,7 @@
substitutions:
reset_pin: P0.10
packages:
i2c: !include ../../test_build_components/common/i2c/nrf52.yaml
<<: !include common.yaml

View File

@@ -15,5 +15,10 @@ wifi:
networks:
- ssid: MySSID
password: password1
priority: 10
- ssid: MySSID2
password: password2
priority: 5
- ssid: MySSID3
password: password3
priority: 0

View File

@@ -2,6 +2,22 @@ psram:
wifi:
use_psram: true
min_auth_mode: WPA
manual_ip:
static_ip: 192.168.1.100
gateway: 192.168.1.1
subnet: 255.255.255.0
dns1: 1.1.1.1
dns2: 8.8.8.8
ap:
ssid: Fallback AP
password: fallback_password
manual_ip:
static_ip: 192.168.4.1
gateway: 192.168.4.1
subnet: 255.255.255.0
captive_portal:
packages:
- !include common.yaml

View File

@@ -1 +1,5 @@
<<: !include common.yaml
wifi:
min_auth_mode: WPA2
packages:
- !include common.yaml