Compare commits

..

9 Commits

Author SHA1 Message Date
J. Nick Koston
d4110bf650 [lock] Store state strings in flash and avoid heap allocation in set_state (#13729) 2026-02-03 05:29:24 +01:00
Andrew Gillis
ff6f7d3248 [mipi_dsi] Add WAVESHARE-ESP32-P4-WIFI6-TOUCH-LCD-7B (#13608) 2026-02-03 14:59:51 +11:00
Roger Fachini
a430b3a426 [speaker.media_player]: Add verbose error message for puremagic parsing (#13725)
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-02-03 03:46:46 +00:00
J. Nick Koston
fbeb0e8e54 [opentherm] Fix ESP-IDF build by re-enabling legacy driver component (#13732) 2026-02-03 03:40:44 +00:00
J. Nick Koston
9d63642bdb [media_player] Store command strings in flash and avoid heap allocation in set_command (#13731) 2026-02-03 04:29:43 +01:00
J. Nick Koston
8cb701e412 [water_heater] Store mode strings in flash and avoid heap allocation in set_mode (#13728) 2026-02-03 04:29:31 +01:00
J. Nick Koston
d41c84d624 [wifi] Conditionally compile on_connect/on_disconnect triggers (#13684) 2026-02-03 04:29:18 +01:00
J. Nick Koston
9f1a427ce2 [preferences] Use static storage for singletons and flash buffer (#13727) 2026-02-03 04:03:52 +01:00
J. Nick Koston
ae71f07abb [http_request] Fix requests taking full timeout when response is already complete (#13649) 2026-02-03 03:19:38 +01:00
41 changed files with 257 additions and 476 deletions

View File

@@ -89,9 +89,8 @@ async def to_code(config):
var.set_state_save_interval(config[CONF_STATE_SAVE_INTERVAL].total_milliseconds)
)
# Although this component does not use SPI/Wire directly, the BSEC library requires them
# Although this component does not use SPI, the BSEC library requires the SPI library
cg.add_library("SPI", None)
cg.add_library("Wire", None)
cg.add_define("USE_BSEC")
cg.add_library("boschsensortec/BSEC Software Library", "1.6.1480")

View File

@@ -5,7 +5,6 @@ import logging
import os
from pathlib import Path
import re
import shutil
from esphome import yaml_util
import esphome.codegen as cg
@@ -51,7 +50,6 @@ from esphome.writer import clean_cmake_cache
from .boards import BOARDS, STANDARD_BOARDS
from .const import ( # noqa
KEY_ARDUINO_LIBRARIES,
KEY_BOARD,
KEY_COMPONENTS,
KEY_ESP32,
@@ -154,168 +152,6 @@ DEFAULT_EXCLUDED_IDF_COMPONENTS = (
"wifi_provisioning", # WiFi provisioning - ESPHome uses its own improv implementation
)
# Additional IDF managed components to exclude for Arduino framework builds
# These are pulled in by the Arduino framework's idf_component.yml but not used by ESPHome
# Note: Component names include the namespace prefix (e.g., "espressif__cbor") because
# that's how managed components are registered in the IDF build system
# List includes direct dependencies from arduino-esp32/idf_component.yml
# plus transitive dependencies from RainMaker/Insights (except espressif/mdns which we need)
ARDUINO_EXCLUDED_IDF_COMPONENTS = (
"chmorgan__esp-libhelix-mp3", # MP3 decoder - not used
"espressif__cbor", # CBOR library - only used by RainMaker/Insights
"espressif__esp-dsp", # DSP library - not used
"espressif__esp-modbus", # Modbus - ESPHome has its own
"espressif__esp-sr", # Speech recognition - not used
"espressif__esp-zboss-lib", # Zigbee ZBOSS library - not used
"espressif__esp-zigbee-lib", # Zigbee library - not used
"espressif__esp_diag_data_store", # Diagnostics - not used
"espressif__esp_diagnostics", # Diagnostics - not used
"espressif__esp_hosted", # ESP hosted - only for ESP32-P4
"espressif__esp_insights", # ESP Insights - not used
"espressif__esp_modem", # Modem library - not used
"espressif__esp_rainmaker", # RainMaker - not used
"espressif__esp_rcp_update", # RCP update - RainMaker transitive dep
"espressif__esp_schedule", # Schedule - RainMaker transitive dep
"espressif__esp_secure_cert_mgr", # Secure cert - RainMaker transitive dep
"espressif__esp_wifi_remote", # WiFi remote - only for ESP32-P4
"espressif__json_generator", # JSON generator - RainMaker transitive dep
"espressif__json_parser", # JSON parser - RainMaker transitive dep
"espressif__lan867x", # Ethernet PHY - ESPHome uses ESP-IDF ethernet directly
"espressif__libsodium", # Crypto - ESPHome uses its own noise-c library
"espressif__network_provisioning", # Network provisioning - not used
"espressif__qrcode", # QR code - not used
"espressif__rmaker_common", # RainMaker common - not used
"joltwallet__littlefs", # LittleFS - ESPHome doesn't use filesystem
)
# Mapping of Arduino libraries to IDF managed components they require
# When an Arduino library is enabled via cg.add_library(), these components
# are automatically un-stubbed from ARDUINO_EXCLUDED_IDF_COMPONENTS.
#
# Note: Some libraries (Matter, LittleFS, ESP_SR, WiFiProv, ArduinoOTA) already have
# conditional maybe_add_component() calls in arduino-esp32/CMakeLists.txt that handle
# their managed component dependencies. Our mapping is primarily needed for libraries
# that don't have such conditionals (Ethernet, PPP, Zigbee, RainMaker, Insights, etc.)
# and to ensure the stubs are removed from our idf_component.yml overrides.
ARDUINO_LIBRARY_IDF_COMPONENTS: dict[str, tuple[str, ...]] = {
"BLE": ("esp_driver_gptimer",),
"BluetoothSerial": ("esp_driver_gptimer",),
"ESP_HostedOTA": ("espressif__esp_hosted", "espressif__esp_wifi_remote"),
"ESP_SR": ("espressif__esp-sr",),
"Ethernet": ("espressif__lan867x",),
"FFat": ("fatfs",),
"Insights": (
"espressif__cbor",
"espressif__esp_insights",
"espressif__esp_diagnostics",
"espressif__esp_diag_data_store",
"espressif__rmaker_common", # Transitive dep from esp_insights
),
"LittleFS": ("joltwallet__littlefs",),
"Matter": ("espressif__esp_matter",),
"PPP": ("espressif__esp_modem",),
"RainMaker": (
# Direct deps from idf_component.yml
"espressif__cbor",
"espressif__esp_rainmaker",
"espressif__esp_insights",
"espressif__esp_diagnostics",
"espressif__esp_diag_data_store",
"espressif__rmaker_common",
"espressif__qrcode",
# Transitive deps from esp_rainmaker
"espressif__esp_rcp_update",
"espressif__esp_schedule",
"espressif__esp_secure_cert_mgr",
"espressif__json_generator",
"espressif__json_parser",
"espressif__network_provisioning",
),
"SD": ("fatfs",),
"SD_MMC": ("fatfs",),
"SPIFFS": ("spiffs",),
"WiFiProv": ("espressif__network_provisioning", "espressif__qrcode"),
"Zigbee": ("espressif__esp-zigbee-lib", "espressif__esp-zboss-lib"),
}
# Arduino library to Arduino library dependencies
# When enabling one library, also enable its dependencies
# Kconfig "select" statements don't work with CONFIG_ARDUINO_SELECTIVE_COMPILATION
ARDUINO_LIBRARY_DEPENDENCIES: dict[str, tuple[str, ...]] = {
"Ethernet": ("Network",),
"WiFi": ("Network",),
}
def _idf_component_stub_name(component: str) -> str:
"""Get stub directory name from IDF component name.
Component names are typically namespace__name (e.g., espressif__cbor).
Returns just the name part (e.g., cbor). If no namespace is present,
returns the original component name.
"""
_prefix, sep, suffix = component.partition("__")
return suffix if sep else component
def _idf_component_dep_name(component: str) -> str:
"""Convert IDF component name to dependency format.
Converts espressif__cbor to espressif/cbor.
"""
return component.replace("__", "/")
# Arduino libraries to disable by default when using Arduino framework
# ESPHome uses ESP-IDF APIs directly; we only need the Arduino core
# (HardwareSerial, Print, Stream, GPIO functions which are always compiled)
# Components use cg.add_library() which auto-enables any they need
# This list must match ARDUINO_ALL_LIBRARIES from arduino-esp32/CMakeLists.txt
ARDUINO_DISABLED_LIBRARIES: frozenset[str] = frozenset(
{
"ArduinoOTA",
"AsyncUDP",
"BLE",
"BluetoothSerial",
"DNSServer",
"EEPROM",
"ESP_HostedOTA",
"ESP_I2S",
"ESP_NOW",
"ESP_SR",
"ESPmDNS",
"Ethernet",
"FFat",
"FS",
"Hash",
"HTTPClient",
"HTTPUpdate",
"Insights",
"LittleFS",
"Matter",
"NetBIOS",
"Network",
"NetworkClientSecure",
"OpenThread",
"PPP",
"Preferences",
"RainMaker",
"SD",
"SD_MMC",
"SimpleBLE",
"SPI",
"SPIFFS",
"Ticker",
"Update",
"USB",
"WebServer",
"WiFi",
"WiFiProv",
"Wire",
"Zigbee",
}
)
# ESP32 (original) chip revision options
# Setting minimum revision to 3.0 or higher:
# - Reduces flash size by excluding workaround code for older chip bugs
@@ -407,13 +243,7 @@ def set_core_data(config):
CORE.data[KEY_ESP32][KEY_COMPONENTS] = {}
# Initialize with default exclusions - components can call include_builtin_idf_component()
# to re-enable any they need
excluded = set(DEFAULT_EXCLUDED_IDF_COMPONENTS)
# Add Arduino-specific managed component exclusions when using Arduino framework
if conf[CONF_TYPE] == FRAMEWORK_ARDUINO:
excluded.update(ARDUINO_EXCLUDED_IDF_COMPONENTS)
CORE.data[KEY_ESP32][KEY_EXCLUDE_COMPONENTS] = excluded
# Initialize Arduino library tracking - cg.add_library() auto-enables libraries
CORE.data[KEY_ESP32][KEY_ARDUINO_LIBRARIES] = set()
CORE.data[KEY_ESP32][KEY_EXCLUDE_COMPONENTS] = set(DEFAULT_EXCLUDED_IDF_COMPONENTS)
CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION] = cv.Version.parse(
config[CONF_FRAMEWORK][CONF_VERSION]
)
@@ -561,26 +391,6 @@ def include_builtin_idf_component(name: str) -> None:
CORE.data[KEY_ESP32][KEY_EXCLUDE_COMPONENTS].discard(name)
def _enable_arduino_library(name: str) -> None:
"""Enable an Arduino library that is disabled by default.
This is called automatically by CORE.add_library() when a component adds
an Arduino library via cg.add_library(). Components should not call this
directly - just use cg.add_library("LibName", None).
Args:
name: The library name (e.g., "Wire", "SPI", "WiFi")
"""
enabled_libs: set[str] = CORE.data[KEY_ESP32][KEY_ARDUINO_LIBRARIES]
enabled_libs.add(name)
# Also enable any required Arduino library dependencies
for dep_lib in ARDUINO_LIBRARY_DEPENDENCIES.get(name, ()):
enabled_libs.add(dep_lib)
# Also enable any required IDF components
for idf_component in ARDUINO_LIBRARY_IDF_COMPONENTS.get(name, ()):
include_builtin_idf_component(idf_component)
def add_extra_script(stage: str, filename: str, path: Path):
"""Add an extra script to the project."""
key = f"{stage}:{filename}"
@@ -1322,27 +1132,6 @@ async def _write_exclude_components() -> None:
)
@coroutine_with_priority(CoroPriority.FINAL)
async def _write_arduino_libraries_sdkconfig() -> None:
"""Write Arduino selective compilation sdkconfig after all components have added libraries.
This must run at FINAL priority so that all components have had a chance to call
cg.add_library() which auto-enables Arduino libraries via _enable_arduino_library().
"""
if KEY_ESP32 not in CORE.data:
return
# Enable Arduino selective compilation to disable unused Arduino libraries
# ESPHome uses ESP-IDF APIs directly; we only need the Arduino core
# (HardwareSerial, Print, Stream, GPIO functions which are always compiled)
# cg.add_library() auto-enables needed libraries; users can also add
# libraries via esphome: libraries: config which calls cg.add_library()
add_idf_sdkconfig_option("CONFIG_ARDUINO_SELECTIVE_COMPILATION", True)
enabled_libs = CORE.data[KEY_ESP32].get(KEY_ARDUINO_LIBRARIES, set())
for lib in ARDUINO_DISABLED_LIBRARIES:
# Enable if explicitly requested, disable otherwise
add_idf_sdkconfig_option(f"CONFIG_ARDUINO_SELECTIVE_{lib}", lib in enabled_libs)
@coroutine_with_priority(CoroPriority.FINAL)
async def _add_yaml_idf_components(components: list[ConfigType]):
"""Add IDF components from YAML config with final priority to override code-added components."""
@@ -1761,11 +1550,6 @@ async def to_code(config):
# Default exclusions are added in set_core_data() during config validation.
CORE.add_job(_write_exclude_components)
# Write Arduino selective compilation sdkconfig at FINAL priority after all
# components have had a chance to call cg.add_library() to enable libraries they need.
if conf[CONF_TYPE] == FRAMEWORK_ARDUINO:
CORE.add_job(_write_arduino_libraries_sdkconfig)
APP_PARTITION_SIZES = {
"2MB": 0x0C0000, # 768 KB
@@ -1846,49 +1630,11 @@ def _write_sdkconfig():
def _write_idf_component_yml():
yml_path = CORE.relative_build_path("src/idf_component.yml")
dependencies: dict[str, dict] = {}
# For Arduino builds, override unused managed components from the Arduino framework
# by pointing them to empty stub directories using override_path
# This prevents the IDF component manager from downloading the real components
if CORE.using_arduino:
# Determine which IDF components are needed by enabled Arduino libraries
enabled_libs = CORE.data[KEY_ESP32].get(KEY_ARDUINO_LIBRARIES, set())
required_idf_components = {
comp
for lib in enabled_libs
for comp in ARDUINO_LIBRARY_IDF_COMPONENTS.get(lib, ())
}
# Only stub components that are not required by any enabled Arduino library
components_to_stub = (
set(ARDUINO_EXCLUDED_IDF_COMPONENTS) - required_idf_components
)
stubs_dir = CORE.relative_build_path("component_stubs")
stubs_dir.mkdir(exist_ok=True)
for component_name in components_to_stub:
# Create stub directory with minimal CMakeLists.txt
stub_path = stubs_dir / _idf_component_stub_name(component_name)
stub_path.mkdir(exist_ok=True)
stub_cmake = stub_path / "CMakeLists.txt"
if not stub_cmake.exists():
stub_cmake.write_text("idf_component_register()\n")
dependencies[_idf_component_dep_name(component_name)] = {
"version": "*",
"override_path": str(stub_path),
}
# Remove stubs for components that are now required by enabled libraries
for component_name in required_idf_components:
stub_path = stubs_dir / _idf_component_stub_name(component_name)
if stub_path.exists():
shutil.rmtree(stub_path)
if CORE.data[KEY_ESP32][KEY_COMPONENTS]:
components: dict = CORE.data[KEY_ESP32][KEY_COMPONENTS]
dependencies = {}
for name, component in components.items():
dependency: dict[str, str] = {}
dependency = {}
if component[KEY_REF]:
dependency["version"] = component[KEY_REF]
if component[KEY_REPO]:
@@ -1896,8 +1642,9 @@ def _write_idf_component_yml():
if component[KEY_PATH]:
dependency["path"] = component[KEY_PATH]
dependencies[name] = dependency
contents = yaml_util.dump({"dependencies": dependencies}) if dependencies else ""
contents = yaml_util.dump({"dependencies": dependencies})
else:
contents = ""
if write_file_if_changed(yml_path, contents):
dependencies_lock = CORE.relative_build_path("dependencies.lock")
if dependencies_lock.is_file():

View File

@@ -7,7 +7,6 @@ KEY_VARIANT = "variant"
KEY_SDKCONFIG_OPTIONS = "sdkconfig_options"
KEY_COMPONENTS = "components"
KEY_EXCLUDE_COMPONENTS = "exclude_components"
KEY_ARDUINO_LIBRARIES = "arduino_libraries"
KEY_REPO = "repo"
KEY_REF = "ref"
KEY_REFRESH = "refresh"

View File

@@ -203,10 +203,11 @@ class ESP32Preferences : public ESPPreferences {
}
};
static ESP32Preferences s_preferences; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
void setup_preferences() {
auto *prefs = new ESP32Preferences(); // NOLINT(cppcoreguidelines-owning-memory)
prefs->open();
global_preferences = prefs;
s_preferences.open();
global_preferences = &s_preferences;
}
} // namespace esp32

View File

@@ -211,11 +211,14 @@ bool Esp32HostedUpdate::fetch_manifest_() {
int read_or_error = container->read(buf, sizeof(buf));
App.feed_wdt();
yield();
auto result = http_request::http_read_loop_result(read_or_error, last_data_time, read_timeout);
auto result =
http_request::http_read_loop_result(read_or_error, last_data_time, read_timeout, container->is_read_complete());
if (result == http_request::HttpReadLoopResult::RETRY)
continue;
// Note: COMPLETE is currently unreachable since the loop condition checks bytes_read < content_length,
// but this is defensive code in case chunked transfer encoding support is added in the future.
if (result != http_request::HttpReadLoopResult::DATA)
break; // ERROR or TIMEOUT
break; // COMPLETE, ERROR, or TIMEOUT
json_str.append(reinterpret_cast<char *>(buf), read_or_error);
}
container->end();
@@ -336,9 +339,14 @@ bool Esp32HostedUpdate::stream_firmware_to_coprocessor_() {
App.feed_wdt();
yield();
auto result = http_request::http_read_loop_result(read_or_error, last_data_time, read_timeout);
auto result =
http_request::http_read_loop_result(read_or_error, last_data_time, read_timeout, container->is_read_complete());
if (result == http_request::HttpReadLoopResult::RETRY)
continue;
// Note: COMPLETE is currently unreachable since the loop condition checks bytes_read < content_length,
// but this is defensive code in case chunked transfer encoding support is added in the future.
if (result == http_request::HttpReadLoopResult::COMPLETE)
break;
if (result != http_request::HttpReadLoopResult::DATA) {
if (result == http_request::HttpReadLoopResult::TIMEOUT) {
ESP_LOGE(TAG, "Timeout reading firmware data");

View File

@@ -17,10 +17,6 @@ namespace esphome::esp8266 {
static const char *const TAG = "esp8266.preferences";
static uint32_t *s_flash_storage = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
static bool s_prevent_write = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
static bool s_flash_dirty = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
static constexpr uint32_t ESP_RTC_USER_MEM_START = 0x60001200;
static constexpr uint32_t ESP_RTC_USER_MEM_SIZE_WORDS = 128;
static constexpr uint32_t ESP_RTC_USER_MEM_SIZE_BYTES = ESP_RTC_USER_MEM_SIZE_WORDS * 4;
@@ -43,6 +39,11 @@ static constexpr uint32_t ESP8266_FLASH_STORAGE_SIZE = 128;
static constexpr uint32_t ESP8266_FLASH_STORAGE_SIZE = 64;
#endif
static uint32_t
s_flash_storage[ESP8266_FLASH_STORAGE_SIZE]; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
static bool s_prevent_write = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
static bool s_flash_dirty = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
static inline bool esp_rtc_user_mem_read(uint32_t index, uint32_t *dest) {
if (index >= ESP_RTC_USER_MEM_SIZE_WORDS) {
return false;
@@ -180,7 +181,6 @@ class ESP8266Preferences : public ESPPreferences {
uint32_t current_flash_offset = 0; // in words
void setup() {
s_flash_storage = new uint32_t[ESP8266_FLASH_STORAGE_SIZE]; // NOLINT
ESP_LOGVV(TAG, "Loading preferences from flash");
{
@@ -283,10 +283,11 @@ class ESP8266Preferences : public ESPPreferences {
}
};
static ESP8266Preferences s_preferences; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
void setup_preferences() {
auto *pref = new ESP8266Preferences(); // NOLINT(cppcoreguidelines-owning-memory)
pref->setup();
global_preferences = pref;
s_preferences.setup();
global_preferences = &s_preferences;
}
void preferences_prevent_write(bool prevent) { s_prevent_write = prevent; }

View File

@@ -13,7 +13,7 @@ from esphome.const import (
CONF_TRIGGER_ID,
CONF_WIFI,
)
from esphome.core import HexInt
from esphome.core import CORE, HexInt
from esphome.types import ConfigType
CODEOWNERS = ["@jesserockz"]
@@ -124,6 +124,9 @@ async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
if CORE.using_arduino:
cg.add_library("WiFi", None)
# ESP-NOW uses wake_loop_threadsafe() to wake the main loop from ESP-NOW callbacks
# This enables low-latency event processing instead of waiting for select() timeout
socket.require_wake_loop_threadsafe()

View File

@@ -431,6 +431,9 @@ async def to_code(config):
# Add LAN867x 10BASE-T1S PHY support component
add_idf_component(name="espressif/lan867x", ref="2.0.0")
if CORE.using_arduino:
cg.add_library("WiFi", None)
if on_connect_config := config.get(CONF_ON_CONNECT):
cg.add_define("USE_ETHERNET_CONNECT_TRIGGER")
await automation.build_automation(

View File

@@ -66,10 +66,11 @@ ESPPreferenceObject HostPreferences::make_preference(size_t length, uint32_t typ
return ESPPreferenceObject(backend);
};
static HostPreferences s_preferences; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
void setup_preferences() {
auto *pref = new HostPreferences(); // NOLINT(cppcoreguidelines-owning-memory)
host_preferences = pref;
global_preferences = pref;
host_preferences = &s_preferences;
global_preferences = &s_preferences;
}
bool HostPreferenceBackend::save(const uint8_t *data, size_t len) {

View File

@@ -26,6 +26,7 @@ struct Header {
enum HttpStatus {
HTTP_STATUS_OK = 200,
HTTP_STATUS_NO_CONTENT = 204,
HTTP_STATUS_RESET_CONTENT = 205,
HTTP_STATUS_PARTIAL_CONTENT = 206,
/* 3xx - Redirection */
@@ -126,19 +127,21 @@ struct HttpReadResult {
/// Result of processing a non-blocking read with timeout (for manual loops)
enum class HttpReadLoopResult : uint8_t {
DATA, ///< Data was read, process it
RETRY, ///< No data yet, already delayed, caller should continue loop
ERROR, ///< Read error, caller should exit loop
TIMEOUT, ///< Timeout waiting for data, caller should exit loop
DATA, ///< Data was read, process it
COMPLETE, ///< All content has been read, caller should exit loop
RETRY, ///< No data yet, already delayed, caller should continue loop
ERROR, ///< Read error, caller should exit loop
TIMEOUT, ///< Timeout waiting for data, caller should exit loop
};
/// Process a read result with timeout tracking and delay handling
/// @param bytes_read_or_error Return value from read() - positive for bytes read, negative for error
/// @param last_data_time Time of last successful read, updated when data received
/// @param timeout_ms Maximum time to wait for data
/// @return DATA if data received, RETRY if should continue loop, ERROR/TIMEOUT if should exit
inline HttpReadLoopResult http_read_loop_result(int bytes_read_or_error, uint32_t &last_data_time,
uint32_t timeout_ms) {
/// @param is_read_complete Whether all expected content has been read (from HttpContainer::is_read_complete())
/// @return How the caller should proceed - see HttpReadLoopResult enum
inline HttpReadLoopResult http_read_loop_result(int bytes_read_or_error, uint32_t &last_data_time, uint32_t timeout_ms,
bool is_read_complete) {
if (bytes_read_or_error > 0) {
last_data_time = millis();
return HttpReadLoopResult::DATA;
@@ -146,7 +149,10 @@ inline HttpReadLoopResult http_read_loop_result(int bytes_read_or_error, uint32_
if (bytes_read_or_error < 0) {
return HttpReadLoopResult::ERROR;
}
// bytes_read_or_error == 0: no data available yet
// bytes_read_or_error == 0: either "no data yet" or "all content read"
if (is_read_complete) {
return HttpReadLoopResult::COMPLETE;
}
if (millis() - last_data_time >= timeout_ms) {
return HttpReadLoopResult::TIMEOUT;
}
@@ -159,9 +165,9 @@ class HttpRequestComponent;
class HttpContainer : public Parented<HttpRequestComponent> {
public:
virtual ~HttpContainer() = default;
size_t content_length;
int status_code;
uint32_t duration_ms;
size_t content_length{0};
int status_code{-1}; ///< -1 indicates no response received yet
uint32_t duration_ms{0};
/**
* @brief Read data from the HTTP response body.
@@ -194,9 +200,24 @@ class HttpContainer : public Parented<HttpRequestComponent> {
virtual void end() = 0;
void set_secure(bool secure) { this->secure_ = secure; }
void set_chunked(bool chunked) { this->is_chunked_ = chunked; }
size_t get_bytes_read() const { return this->bytes_read_; }
/// Check if all expected content has been read
/// For chunked responses, returns false (completion detected via read() returning error/EOF)
bool is_read_complete() const {
// Per RFC 9112, these responses have no body:
// - 1xx (Informational), 204 No Content, 205 Reset Content, 304 Not Modified
if ((this->status_code >= 100 && this->status_code < 200) || this->status_code == HTTP_STATUS_NO_CONTENT ||
this->status_code == HTTP_STATUS_RESET_CONTENT || this->status_code == HTTP_STATUS_NOT_MODIFIED) {
return true;
}
// For non-chunked responses, complete when bytes_read >= content_length
// This handles both Content-Length: 0 and Content-Length: N cases
return !this->is_chunked_ && this->bytes_read_ >= this->content_length;
}
/**
* @brief Get response headers.
*
@@ -209,6 +230,7 @@ class HttpContainer : public Parented<HttpRequestComponent> {
protected:
size_t bytes_read_{0};
bool secure_{false};
bool is_chunked_{false}; ///< True if response uses chunked transfer encoding
std::map<std::string, std::list<std::string>> response_headers_{};
};
@@ -219,7 +241,7 @@ class HttpContainer : public Parented<HttpRequestComponent> {
/// @param total_size Total bytes to read
/// @param chunk_size Maximum bytes per read call
/// @param timeout_ms Read timeout in milliseconds
/// @return HttpReadResult with status and error_code on failure
/// @return HttpReadResult with status and error_code on failure; use container->get_bytes_read() for total bytes read
inline HttpReadResult http_read_fully(HttpContainer *container, uint8_t *buffer, size_t total_size, size_t chunk_size,
uint32_t timeout_ms) {
size_t read_index = 0;
@@ -231,9 +253,11 @@ inline HttpReadResult http_read_fully(HttpContainer *container, uint8_t *buffer,
App.feed_wdt();
yield();
auto result = http_read_loop_result(read_bytes_or_error, last_data_time, timeout_ms);
auto result = http_read_loop_result(read_bytes_or_error, last_data_time, timeout_ms, container->is_read_complete());
if (result == HttpReadLoopResult::RETRY)
continue;
if (result == HttpReadLoopResult::COMPLETE)
break; // Server sent less data than requested, but transfer is complete
if (result == HttpReadLoopResult::ERROR)
return {HttpReadStatus::ERROR, read_bytes_or_error};
if (result == HttpReadLoopResult::TIMEOUT)
@@ -393,11 +417,12 @@ template<typename... Ts> class HttpRequestSendAction : public Action<Ts...> {
int read_or_error = container->read(buf + read_index, std::min<size_t>(max_length - read_index, 512));
App.feed_wdt();
yield();
auto result = http_read_loop_result(read_or_error, last_data_time, read_timeout);
auto result =
http_read_loop_result(read_or_error, last_data_time, read_timeout, container->is_read_complete());
if (result == HttpReadLoopResult::RETRY)
continue;
if (result != HttpReadLoopResult::DATA)
break; // ERROR or TIMEOUT
break; // COMPLETE, ERROR, or TIMEOUT
read_index += read_or_error;
}
response_body.reserve(read_index);

View File

@@ -135,9 +135,23 @@ std::shared_ptr<HttpContainer> HttpRequestArduino::perform(const std::string &ur
// When cast to size_t, -1 becomes SIZE_MAX (4294967295 on 32-bit).
// The read() method handles this: bytes_read_ can never reach SIZE_MAX, so the
// early return check (bytes_read_ >= content_length) will never trigger.
//
// TODO: Chunked transfer encoding is NOT properly supported on Arduino.
// The implementation in #7884 was incomplete - it only works correctly on ESP-IDF where
// esp_http_client_read() decodes chunks internally. On Arduino, using getStreamPtr()
// returns raw TCP data with chunk framing (e.g., "12a\r\n{json}\r\n0\r\n\r\n") instead
// of decoded content. This wasn't noticed because requests would complete and payloads
// were only examined on IDF. The long transfer times were also masked by the misleading
// "HTTP on Arduino version >= 3.1 is **very** slow" warning above. This causes two issues:
// 1. Response body is corrupted - contains chunk size headers mixed with data
// 2. Cannot detect end of transfer - connection stays open (keep-alive), causing timeout
// The proper fix would be to use getString() for chunked responses, which decodes chunks
// internally, but this buffers the entire response in memory.
int content_length = container->client_.getSize();
ESP_LOGD(TAG, "Content-Length: %d", content_length);
container->content_length = (size_t) content_length;
// -1 (SIZE_MAX when cast to size_t) means chunked transfer encoding
container->set_chunked(content_length == -1);
container->duration_ms = millis() - start;
return container;
@@ -178,9 +192,9 @@ int HttpContainerArduino::read(uint8_t *buf, size_t max_len) {
if (bufsize == 0) {
this->duration_ms += (millis() - start);
// Check if we've read all expected content (only valid when content_length is known and not SIZE_MAX)
// For chunked encoding (content_length == SIZE_MAX), we can't use this check
if (this->content_length > 0 && this->bytes_read_ >= this->content_length) {
// Check if we've read all expected content (non-chunked only)
// For chunked encoding (content_length == SIZE_MAX), is_read_complete() returns false
if (this->is_read_complete()) {
return 0; // All content read successfully
}
// No data available - check if connection is still open

View File

@@ -160,6 +160,7 @@ std::shared_ptr<HttpContainer> HttpRequestIDF::perform(const std::string &url, c
// esp_http_client_fetch_headers() returns 0 for chunked transfer encoding (no Content-Length header).
// The read() method handles content_length == 0 specially to support chunked responses.
container->content_length = esp_http_client_fetch_headers(client);
container->set_chunked(esp_http_client_is_chunked_response(client));
container->feed_wdt();
container->status_code = esp_http_client_get_status_code(client);
container->feed_wdt();
@@ -195,6 +196,7 @@ std::shared_ptr<HttpContainer> HttpRequestIDF::perform(const std::string &url, c
container->feed_wdt();
container->content_length = esp_http_client_fetch_headers(client);
container->set_chunked(esp_http_client_is_chunked_response(client));
container->feed_wdt();
container->status_code = esp_http_client_get_status_code(client);
container->feed_wdt();
@@ -239,10 +241,9 @@ int HttpContainerIDF::read(uint8_t *buf, size_t max_len) {
const uint32_t start = millis();
watchdog::WatchdogManager wdm(this->parent_->get_watchdog_timeout());
// Check if we've already read all expected content
// Skip this check when content_length is 0 (chunked transfer encoding or unknown length)
// For chunked responses, esp_http_client_read() will return 0 when all data is received
if (this->content_length > 0 && this->bytes_read_ >= this->content_length) {
// Check if we've already read all expected content (non-chunked only)
// For chunked responses (content_length == 0), esp_http_client_read() handles EOF
if (this->is_read_complete()) {
return 0; // All content read successfully
}

View File

@@ -130,9 +130,13 @@ uint8_t OtaHttpRequestComponent::do_ota_() {
App.feed_wdt();
yield();
auto result = http_read_loop_result(bufsize_or_error, last_data_time, read_timeout);
auto result = http_read_loop_result(bufsize_or_error, last_data_time, read_timeout, container->is_read_complete());
if (result == HttpReadLoopResult::RETRY)
continue;
// Note: COMPLETE is currently unreachable since the loop condition checks bytes_read < content_length,
// but this is defensive code in case chunked transfer encoding support is added for OTA in the future.
if (result == HttpReadLoopResult::COMPLETE)
break;
if (result != HttpReadLoopResult::DATA) {
if (result == HttpReadLoopResult::TIMEOUT) {
ESP_LOGE(TAG, "Timeout reading data");

View File

@@ -114,7 +114,6 @@ async def to_code(config):
cg.add(var.set_external_dac_channels(2 if config[CONF_MODE] == "stereo" else 1))
cg.add(var.set_i2s_comm_fmt_lsb(config[CONF_I2S_COMM_FMT] == "lsb"))
cg.add_library("WiFi", None)
cg.add_library("NetworkClientSecure", None)
cg.add_library("HTTPClient", None)
cg.add_library("esphome/ESP32-audioI2S", "2.3.0")

View File

@@ -189,10 +189,11 @@ class LibreTinyPreferences : public ESPPreferences {
}
};
static LibreTinyPreferences s_preferences; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
void setup_preferences() {
auto *prefs = new LibreTinyPreferences(); // NOLINT(cppcoreguidelines-owning-memory)
prefs->open();
global_preferences = prefs;
s_preferences.open();
global_preferences = &s_preferences;
}
} // namespace libretiny

View File

@@ -2,6 +2,7 @@
#include "esphome/core/defines.h"
#include "esphome/core/controller_registry.h"
#include "esphome/core/log.h"
#include "esphome/core/progmem.h"
namespace esphome::lock {
@@ -84,21 +85,21 @@ LockCall &LockCall::set_state(optional<LockState> state) {
this->state_ = state;
return *this;
}
LockCall &LockCall::set_state(const std::string &state) {
if (str_equals_case_insensitive(state, "LOCKED")) {
LockCall &LockCall::set_state(const char *state) {
if (ESPHOME_strcasecmp_P(state, ESPHOME_PSTR("LOCKED")) == 0) {
this->set_state(LOCK_STATE_LOCKED);
} else if (str_equals_case_insensitive(state, "UNLOCKED")) {
} else if (ESPHOME_strcasecmp_P(state, ESPHOME_PSTR("UNLOCKED")) == 0) {
this->set_state(LOCK_STATE_UNLOCKED);
} else if (str_equals_case_insensitive(state, "JAMMED")) {
} else if (ESPHOME_strcasecmp_P(state, ESPHOME_PSTR("JAMMED")) == 0) {
this->set_state(LOCK_STATE_JAMMED);
} else if (str_equals_case_insensitive(state, "LOCKING")) {
} else if (ESPHOME_strcasecmp_P(state, ESPHOME_PSTR("LOCKING")) == 0) {
this->set_state(LOCK_STATE_LOCKING);
} else if (str_equals_case_insensitive(state, "UNLOCKING")) {
} else if (ESPHOME_strcasecmp_P(state, ESPHOME_PSTR("UNLOCKING")) == 0) {
this->set_state(LOCK_STATE_UNLOCKING);
} else if (str_equals_case_insensitive(state, "NONE")) {
} else if (ESPHOME_strcasecmp_P(state, ESPHOME_PSTR("NONE")) == 0) {
this->set_state(LOCK_STATE_NONE);
} else {
ESP_LOGW(TAG, "'%s' - Unrecognized state %s", this->parent_->get_name().c_str(), state.c_str());
ESP_LOGW(TAG, "'%s' - Unrecognized state %s", this->parent_->get_name().c_str(), state);
}
return *this;
}

View File

@@ -83,7 +83,8 @@ class LockCall {
/// Set the state of the lock device.
LockCall &set_state(optional<LockState> state);
/// Set the state of the lock device based on a string.
LockCall &set_state(const std::string &state);
LockCall &set_state(const char *state);
LockCall &set_state(const std::string &state) { return this->set_state(state.c_str()); }
void perform();

View File

@@ -2,6 +2,7 @@
#include "esphome/core/defines.h"
#include "esphome/core/controller_registry.h"
#include "esphome/core/log.h"
#include "esphome/core/progmem.h"
namespace esphome {
namespace media_player {
@@ -107,25 +108,25 @@ MediaPlayerCall &MediaPlayerCall::set_command(optional<MediaPlayerCommand> comma
this->command_ = command;
return *this;
}
MediaPlayerCall &MediaPlayerCall::set_command(const std::string &command) {
if (str_equals_case_insensitive(command, "PLAY")) {
MediaPlayerCall &MediaPlayerCall::set_command(const char *command) {
if (ESPHOME_strcasecmp_P(command, ESPHOME_PSTR("PLAY")) == 0) {
this->set_command(MEDIA_PLAYER_COMMAND_PLAY);
} else if (str_equals_case_insensitive(command, "PAUSE")) {
} else if (ESPHOME_strcasecmp_P(command, ESPHOME_PSTR("PAUSE")) == 0) {
this->set_command(MEDIA_PLAYER_COMMAND_PAUSE);
} else if (str_equals_case_insensitive(command, "STOP")) {
} else if (ESPHOME_strcasecmp_P(command, ESPHOME_PSTR("STOP")) == 0) {
this->set_command(MEDIA_PLAYER_COMMAND_STOP);
} else if (str_equals_case_insensitive(command, "MUTE")) {
} else if (ESPHOME_strcasecmp_P(command, ESPHOME_PSTR("MUTE")) == 0) {
this->set_command(MEDIA_PLAYER_COMMAND_MUTE);
} else if (str_equals_case_insensitive(command, "UNMUTE")) {
} else if (ESPHOME_strcasecmp_P(command, ESPHOME_PSTR("UNMUTE")) == 0) {
this->set_command(MEDIA_PLAYER_COMMAND_UNMUTE);
} else if (str_equals_case_insensitive(command, "TOGGLE")) {
} else if (ESPHOME_strcasecmp_P(command, ESPHOME_PSTR("TOGGLE")) == 0) {
this->set_command(MEDIA_PLAYER_COMMAND_TOGGLE);
} else if (str_equals_case_insensitive(command, "TURN_ON")) {
} else if (ESPHOME_strcasecmp_P(command, ESPHOME_PSTR("TURN_ON")) == 0) {
this->set_command(MEDIA_PLAYER_COMMAND_TURN_ON);
} else if (str_equals_case_insensitive(command, "TURN_OFF")) {
} else if (ESPHOME_strcasecmp_P(command, ESPHOME_PSTR("TURN_OFF")) == 0) {
this->set_command(MEDIA_PLAYER_COMMAND_TURN_OFF);
} else {
ESP_LOGW(TAG, "'%s' - Unrecognized command %s", this->parent_->get_name().c_str(), command.c_str());
ESP_LOGW(TAG, "'%s' - Unrecognized command %s", this->parent_->get_name().c_str(), command);
}
return *this;
}

View File

@@ -114,7 +114,8 @@ class MediaPlayerCall {
MediaPlayerCall &set_command(MediaPlayerCommand command);
MediaPlayerCall &set_command(optional<MediaPlayerCommand> command);
MediaPlayerCall &set_command(const std::string &command);
MediaPlayerCall &set_command(const char *command);
MediaPlayerCall &set_command(const std::string &command) { return this->set_command(command.c_str()); }
MediaPlayerCall &set_media_url(const std::string &url);

View File

@@ -30,7 +30,7 @@ from esphome.const import (
UNIT_PERCENT,
UNIT_WATT,
)
from esphome.core import CORE, coroutine
from esphome.core import coroutine
CODEOWNERS = ["@dudanov"]
DEPENDENCIES = ["climate", "uart"]
@@ -290,7 +290,4 @@ async def to_code(config):
if CONF_HUMIDITY_SETPOINT in config:
sens = await sensor.new_sensor(config[CONF_HUMIDITY_SETPOINT])
cg.add(var.set_humidity_setpoint_sensor(sens))
# MideaUART library requires WiFi (WiFi auto-enables Network via dependency mapping)
if CORE.is_esp32:
cg.add_library("WiFi", None)
cg.add_library("dudanov/MideaUART", "1.1.9")

View File

@@ -94,3 +94,29 @@ DriverChip(
(0x29, 0x00),
],
)
DriverChip(
"WAVESHARE-ESP32-P4-WIFI6-TOUCH-LCD-7B",
height=600,
width=1024,
hsync_back_porch=160,
hsync_pulse_width=10,
hsync_front_porch=160,
vsync_back_porch=23,
vsync_pulse_width=1,
vsync_front_porch=12,
pclk_frequency="52MHz",
lane_bit_rate="900Mbps",
no_transform=True,
color_order="RGB",
initsequence=[
(0x80, 0x8B),
(0x81, 0x78),
(0x82, 0x84),
(0x83, 0x88),
(0x84, 0xA8),
(0x85, 0xE3),
(0x86, 0x88),
(0xB2, 0x10),
],
)

View File

@@ -137,7 +137,8 @@ CONFIG_SCHEMA = cv.Schema(
@coroutine_with_priority(CoroPriority.NETWORK)
async def to_code(config):
cg.add_define("USE_NETWORK")
# ESP32 with Arduino uses ESP-IDF network APIs directly, no Arduino Network library needed
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

View File

@@ -4,8 +4,10 @@ from typing import Any
from esphome import automation, pins
import esphome.codegen as cg
from esphome.components import sensor
from esphome.components.esp32 import include_builtin_idf_component
import esphome.config_validation as cv
from esphome.const import CONF_ID, CONF_TRIGGER_ID, PLATFORM_ESP32, PLATFORM_ESP8266
from esphome.core import CORE
from . import const, generate, schema, validate
@@ -83,6 +85,12 @@ CONFIG_SCHEMA = cv.All(
async def to_code(config: dict[str, Any]) -> None:
if CORE.is_esp32:
# Re-enable ESP-IDF's legacy driver component (excluded by default to save compile time)
# Provides driver/timer.h header for hardware timer API
# TODO: Remove this once opentherm migrates to GPTimer API (driver/gptimer.h)
include_builtin_idf_component("driver")
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)

View File

@@ -8,6 +8,8 @@
#include "opentherm.h"
#include "esphome/core/helpers.h"
#include <cinttypes>
// TODO: Migrate from legacy timer API (driver/timer.h) to GPTimer API (driver/gptimer.h)
// The legacy timer API is deprecated in ESP-IDF 5.x. See opentherm.h for details.
#ifdef USE_ESP32
#include "driver/timer.h"
#include "esp_err.h"

View File

@@ -12,6 +12,10 @@
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
// TODO: Migrate from legacy timer API (driver/timer.h) to GPTimer API (driver/gptimer.h)
// The legacy timer API is deprecated in ESP-IDF 5.x. Migration would allow removing the
// "driver" IDF component dependency. See:
// https://docs.espressif.com/projects/esp-idf/en/latest/esp32/migration-guides/release-5.x/5.0/peripherals.html#id4
#ifdef USE_ESP32
#include "driver/timer.h"
#endif

View File

@@ -18,11 +18,12 @@ namespace rp2040 {
static const char *const TAG = "rp2040.preferences";
static bool s_prevent_write = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
static uint8_t *s_flash_storage = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
static bool s_flash_dirty = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
static constexpr uint32_t RP2040_FLASH_STORAGE_SIZE = 512;
static const uint32_t RP2040_FLASH_STORAGE_SIZE = 512;
static bool s_prevent_write = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
static uint8_t
s_flash_storage[RP2040_FLASH_STORAGE_SIZE]; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
static bool s_flash_dirty = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
// Stack buffer size for preferences - covers virtually all real-world preferences without heap allocation
static constexpr size_t PREF_BUFFER_SIZE = 64;
@@ -91,7 +92,6 @@ class RP2040Preferences : public ESPPreferences {
RP2040Preferences() : eeprom_sector_(&_EEPROM_start) {}
void setup() {
s_flash_storage = new uint8_t[RP2040_FLASH_STORAGE_SIZE]; // NOLINT
ESP_LOGVV(TAG, "Loading preferences from flash");
memcpy(s_flash_storage, this->eeprom_sector_, RP2040_FLASH_STORAGE_SIZE);
}
@@ -149,10 +149,11 @@ class RP2040Preferences : public ESPPreferences {
uint8_t *eeprom_sector_;
};
static RP2040Preferences s_preferences; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
void setup_preferences() {
auto *prefs = new RP2040Preferences(); // NOLINT(cppcoreguidelines-owning-memory)
prefs->setup();
global_preferences = prefs;
s_preferences.setup();
global_preferences = &s_preferences;
}
void preferences_prevent_write(bool prevent) { s_prevent_write = prevent; }

View File

@@ -157,8 +157,14 @@ def _read_audio_file_and_type(file_config):
import puremagic
file_type: str = puremagic.from_string(data)
file_type = file_type.removeprefix(".")
try:
file_type: str = puremagic.from_string(data)
file_type = file_type.removeprefix(".")
except puremagic.PureError as e:
raise cv.Invalid(
f"Unable to determine audio file type of '{path}'. "
f"Try re-encoding the file into a supported format. Details: {e}"
)
media_file_type = audio.AUDIO_FILE_TYPE_ENUM["NONE"]
if file_type in ("wav"):

View File

@@ -2,6 +2,7 @@
#include "esphome/core/log.h"
#include "esphome/core/application.h"
#include "esphome/core/controller_registry.h"
#include "esphome/core/progmem.h"
#include <cmath>
@@ -22,23 +23,23 @@ WaterHeaterCall &WaterHeaterCall::set_mode(WaterHeaterMode mode) {
return *this;
}
WaterHeaterCall &WaterHeaterCall::set_mode(const std::string &mode) {
if (str_equals_case_insensitive(mode, "OFF")) {
WaterHeaterCall &WaterHeaterCall::set_mode(const char *mode) {
if (ESPHOME_strcasecmp_P(mode, ESPHOME_PSTR("OFF")) == 0) {
this->set_mode(WATER_HEATER_MODE_OFF);
} else if (str_equals_case_insensitive(mode, "ECO")) {
} else if (ESPHOME_strcasecmp_P(mode, ESPHOME_PSTR("ECO")) == 0) {
this->set_mode(WATER_HEATER_MODE_ECO);
} else if (str_equals_case_insensitive(mode, "ELECTRIC")) {
} else if (ESPHOME_strcasecmp_P(mode, ESPHOME_PSTR("ELECTRIC")) == 0) {
this->set_mode(WATER_HEATER_MODE_ELECTRIC);
} else if (str_equals_case_insensitive(mode, "PERFORMANCE")) {
} else if (ESPHOME_strcasecmp_P(mode, ESPHOME_PSTR("PERFORMANCE")) == 0) {
this->set_mode(WATER_HEATER_MODE_PERFORMANCE);
} else if (str_equals_case_insensitive(mode, "HIGH_DEMAND")) {
} else if (ESPHOME_strcasecmp_P(mode, ESPHOME_PSTR("HIGH_DEMAND")) == 0) {
this->set_mode(WATER_HEATER_MODE_HIGH_DEMAND);
} else if (str_equals_case_insensitive(mode, "HEAT_PUMP")) {
} else if (ESPHOME_strcasecmp_P(mode, ESPHOME_PSTR("HEAT_PUMP")) == 0) {
this->set_mode(WATER_HEATER_MODE_HEAT_PUMP);
} else if (str_equals_case_insensitive(mode, "GAS")) {
} else if (ESPHOME_strcasecmp_P(mode, ESPHOME_PSTR("GAS")) == 0) {
this->set_mode(WATER_HEATER_MODE_GAS);
} else {
ESP_LOGW(TAG, "'%s' - Unrecognized mode %s", this->parent_->get_name().c_str(), mode.c_str());
ESP_LOGW(TAG, "'%s' - Unrecognized mode %s", this->parent_->get_name().c_str(), mode);
}
return *this;
}

View File

@@ -75,7 +75,8 @@ class WaterHeaterCall {
WaterHeaterCall(WaterHeater *parent);
WaterHeaterCall &set_mode(WaterHeaterMode mode);
WaterHeaterCall &set_mode(const std::string &mode);
WaterHeaterCall &set_mode(const char *mode);
WaterHeaterCall &set_mode(const std::string &mode) { return this->set_mode(mode.c_str()); }
WaterHeaterCall &set_target_temperature(float temperature);
WaterHeaterCall &set_target_temperature_low(float temperature);
WaterHeaterCall &set_target_temperature_high(float temperature);

View File

@@ -38,8 +38,11 @@ async def to_code(config):
cg.add_define("WEB_SERVER_DEFAULT_HEADERS_COUNT", 1)
return
# ESP32 uses IDF web server (early return above), so this is for other Arduino platforms
if CORE.using_arduino:
if CORE.is_esp32:
cg.add_library("WiFi", None)
cg.add_library("FS", None)
cg.add_library("Update", None)
if CORE.is_esp8266:
cg.add_library("ESP8266WiFi", None)
if CORE.is_libretiny:

View File

@@ -585,11 +585,13 @@ async def to_code(config):
await cg.past_safe_mode()
if on_connect_config := config.get(CONF_ON_CONNECT):
cg.add_define("USE_WIFI_CONNECT_TRIGGER")
await automation.build_automation(
var.get_connect_trigger(), [], on_connect_config
)
if on_disconnect_config := config.get(CONF_ON_DISCONNECT):
cg.add_define("USE_WIFI_DISCONNECT_TRIGGER")
await automation.build_automation(
var.get_disconnect_trigger(), [], on_disconnect_config
)

View File

@@ -48,7 +48,7 @@ template<typename... Ts> class WiFiConfigureAction : public Action<Ts...>, publi
char ssid_buf[SSID_BUFFER_SIZE];
if (strcmp(global_wifi_component->wifi_ssid_to(ssid_buf), ssid.c_str()) == 0) {
// Callback to notify the user that the connection was successful
this->connect_trigger_->trigger();
this->connect_trigger_.trigger();
return;
}
// Create a new WiFiAP object with the new SSID and password
@@ -79,13 +79,13 @@ template<typename... Ts> class WiFiConfigureAction : public Action<Ts...>, publi
// Start a timeout for the fallback if the connection to the old AP fails
this->set_timeout("wifi-fallback-timeout", this->connection_timeout_.value(x...), [this]() {
this->connecting_ = false;
this->error_trigger_->trigger();
this->error_trigger_.trigger();
});
});
}
Trigger<> *get_connect_trigger() const { return this->connect_trigger_; }
Trigger<> *get_error_trigger() const { return this->error_trigger_; }
Trigger<> *get_connect_trigger() { return &this->connect_trigger_; }
Trigger<> *get_error_trigger() { return &this->error_trigger_; }
void loop() override {
if (!this->connecting_)
@@ -98,10 +98,10 @@ template<typename... Ts> class WiFiConfigureAction : public Action<Ts...>, publi
char ssid_buf[SSID_BUFFER_SIZE];
if (strcmp(global_wifi_component->wifi_ssid_to(ssid_buf), this->new_sta_.get_ssid().c_str()) == 0) {
// Callback to notify the user that the connection was successful
this->connect_trigger_->trigger();
this->connect_trigger_.trigger();
} else {
// Callback to notify the user that the connection failed
this->error_trigger_->trigger();
this->error_trigger_.trigger();
}
}
}
@@ -110,8 +110,8 @@ template<typename... Ts> class WiFiConfigureAction : public Action<Ts...>, publi
bool connecting_{false};
WiFiAP new_sta_;
WiFiAP old_sta_;
Trigger<> *connect_trigger_{new Trigger<>()};
Trigger<> *error_trigger_{new Trigger<>()};
Trigger<> connect_trigger_;
Trigger<> error_trigger_;
};
} // namespace esphome::wifi

View File

@@ -651,14 +651,21 @@ void WiFiComponent::loop() {
const uint32_t now = App.get_loop_component_start_time();
if (this->has_sta()) {
#if defined(USE_WIFI_CONNECT_TRIGGER) || defined(USE_WIFI_DISCONNECT_TRIGGER)
if (this->is_connected() != this->handled_connected_state_) {
#ifdef USE_WIFI_DISCONNECT_TRIGGER
if (this->handled_connected_state_) {
this->disconnect_trigger_->trigger();
} else {
this->connect_trigger_->trigger();
this->disconnect_trigger_.trigger();
}
#endif
#ifdef USE_WIFI_CONNECT_TRIGGER
if (!this->handled_connected_state_) {
this->connect_trigger_.trigger();
}
#endif
this->handled_connected_state_ = this->is_connected();
}
#endif // USE_WIFI_CONNECT_TRIGGER || USE_WIFI_DISCONNECT_TRIGGER
switch (this->state_) {
case WIFI_COMPONENT_STATE_COOLDOWN: {

View File

@@ -454,8 +454,12 @@ class WiFiComponent : public Component {
void set_keep_scan_results(bool keep_scan_results) { this->keep_scan_results_ = keep_scan_results; }
void set_post_connect_roaming(bool enabled) { this->post_connect_roaming_ = enabled; }
Trigger<> *get_connect_trigger() const { return this->connect_trigger_; };
Trigger<> *get_disconnect_trigger() const { return this->disconnect_trigger_; };
#ifdef USE_WIFI_CONNECT_TRIGGER
Trigger<> *get_connect_trigger() { return &this->connect_trigger_; }
#endif
#ifdef USE_WIFI_DISCONNECT_TRIGGER
Trigger<> *get_disconnect_trigger() { return &this->disconnect_trigger_; }
#endif
int32_t get_wifi_channel();
@@ -706,7 +710,9 @@ class WiFiComponent : public Component {
// Group all boolean values together
bool has_ap_{false};
#if defined(USE_WIFI_CONNECT_TRIGGER) || defined(USE_WIFI_DISCONNECT_TRIGGER)
bool handled_connected_state_{false};
#endif
bool error_from_callback_{false};
bool scan_done_{false};
bool ap_setup_{false};
@@ -733,9 +739,12 @@ class WiFiComponent : public Component {
SemaphoreHandle_t high_performance_semaphore_{nullptr};
#endif
// Pointers at the end (naturally aligned)
Trigger<> *connect_trigger_{new Trigger<>()};
Trigger<> *disconnect_trigger_{new Trigger<>()};
#ifdef USE_WIFI_CONNECT_TRIGGER
Trigger<> connect_trigger_;
#endif
#ifdef USE_WIFI_DISCONNECT_TRIGGER
Trigger<> disconnect_trigger_;
#endif
private:
// Stores a pointer to a string literal (static storage duration).

View File

@@ -3,7 +3,6 @@ from esphome.components.light.effects import register_addressable_effect
from esphome.components.light.types import AddressableLightEffect
import esphome.config_validation as cv
from esphome.const import CONF_NAME, CONF_PORT
from esphome.core import CORE
wled_ns = cg.esphome_ns.namespace("wled")
WLEDLightEffect = wled_ns.class_("WLEDLightEffect", AddressableLightEffect)
@@ -28,6 +27,4 @@ async def wled_light_effect_to_code(config, effect_id):
cg.add(effect.set_port(config[CONF_PORT]))
cg.add(effect.set_sync_group_mask(config[CONF_SYNC_GROUP_MASK]))
cg.add(effect.set_blank_on_start(config[CONF_BLANK_ON_START]))
if CORE.is_esp32:
cg.add_library("WiFi", None)
return effect

View File

@@ -152,10 +152,11 @@ class ZephyrPreferences : public ESPPreferences {
}
};
static ZephyrPreferences s_preferences; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
void setup_preferences() {
auto *prefs = new ZephyrPreferences(); // NOLINT(cppcoreguidelines-owning-memory)
global_preferences = prefs;
prefs->open();
global_preferences = &s_preferences;
s_preferences.open();
}
} // namespace zephyr

View File

@@ -892,16 +892,6 @@ class EsphomeCore:
library.name if "/" not in library.name else library.name.split("/")[-1]
)
# Auto-enable Arduino libraries on ESP32 Arduino builds
if self.is_esp32 and self.using_arduino:
from esphome.components.esp32 import (
ARDUINO_DISABLED_LIBRARIES,
_enable_arduino_library,
)
if short_name in ARDUINO_DISABLED_LIBRARIES:
_enable_arduino_library(short_name)
if short_name not in self.platformio_libraries:
_LOGGER.debug("Adding library: %s", library)
self.platformio_libraries[short_name] = library

View File

@@ -227,6 +227,8 @@
#define USE_WIFI_SCAN_RESULTS_LISTENERS
#define USE_WIFI_CONNECT_STATE_LISTENERS
#define USE_WIFI_POWER_SAVE_LISTENERS
#define USE_WIFI_CONNECT_TRIGGER
#define USE_WIFI_DISCONNECT_TRIGGER
#define ESPHOME_WIFI_IP_STATE_LISTENERS 2
#define ESPHOME_WIFI_SCAN_RESULTS_LISTENERS 2
#define ESPHOME_WIFI_CONNECT_STATE_LISTENERS 2

View File

@@ -1,16 +0,0 @@
substitutions:
i2s_bclk_pin: GPIO15
i2s_lrclk_pin: GPIO4
i2s_mclk_pin: GPIO5
<<: !include common.yaml
wifi:
ssid: test
password: test1234
media_player:
- platform: i2s_audio
name: Test Media Player
dac_type: internal
mode: stereo

View File

@@ -26,3 +26,7 @@ wifi:
- ssid: MySSID3
password: password3
priority: 0
on_connect:
- logger.log: "WiFi connected!"
on_disconnect:
- logger.log: "WiFi disconnected!"

View File

@@ -780,78 +780,3 @@ class TestEsphomeCore:
target.config = {const.CONF_ESPHOME: {"name": "test"}, "logger": {}}
assert target.has_networking is False
def test_add_library__esp32_arduino_enables_disabled_library(self, target):
"""Test add_library auto-enables Arduino libraries on ESP32 Arduino builds."""
target.data[const.KEY_CORE] = {
const.KEY_TARGET_PLATFORM: "esp32",
const.KEY_TARGET_FRAMEWORK: "arduino",
}
library = core.Library("WiFi", None)
with patch("esphome.components.esp32._enable_arduino_library") as mock_enable:
target.add_library(library)
mock_enable.assert_called_once_with("WiFi")
assert "WiFi" in target.platformio_libraries
def test_add_library__esp32_arduino_ignores_non_arduino_library(self, target):
"""Test add_library doesn't enable libraries not in ARDUINO_DISABLED_LIBRARIES."""
target.data[const.KEY_CORE] = {
const.KEY_TARGET_PLATFORM: "esp32",
const.KEY_TARGET_FRAMEWORK: "arduino",
}
library = core.Library("SomeOtherLib", "1.0.0")
with patch("esphome.components.esp32._enable_arduino_library") as mock_enable:
target.add_library(library)
mock_enable.assert_not_called()
assert "SomeOtherLib" in target.platformio_libraries
def test_add_library__esp32_idf_does_not_enable_arduino_library(self, target):
"""Test add_library doesn't auto-enable Arduino libraries on ESP32 IDF builds."""
target.data[const.KEY_CORE] = {
const.KEY_TARGET_PLATFORM: "esp32",
const.KEY_TARGET_FRAMEWORK: "esp-idf",
}
library = core.Library("WiFi", None)
with patch("esphome.components.esp32._enable_arduino_library") as mock_enable:
target.add_library(library)
mock_enable.assert_not_called()
assert "WiFi" in target.platformio_libraries
def test_add_library__esp8266_does_not_enable_arduino_library(self, target):
"""Test add_library doesn't auto-enable Arduino libraries on ESP8266."""
target.data[const.KEY_CORE] = {
const.KEY_TARGET_PLATFORM: "esp8266",
const.KEY_TARGET_FRAMEWORK: "arduino",
}
library = core.Library("WiFi", None)
with patch("esphome.components.esp32._enable_arduino_library") as mock_enable:
target.add_library(library)
mock_enable.assert_not_called()
assert "WiFi" in target.platformio_libraries
def test_add_library__extracts_short_name_from_path(self, target):
"""Test add_library extracts short name from library paths like owner/lib."""
target.data[const.KEY_CORE] = {
const.KEY_TARGET_PLATFORM: "esp32",
const.KEY_TARGET_FRAMEWORK: "arduino",
}
library = core.Library("arduino/Wire", None)
with patch("esphome.components.esp32._enable_arduino_library") as mock_enable:
target.add_library(library)
mock_enable.assert_called_once_with("Wire")
assert "Wire" in target.platformio_libraries