mirror of
https://github.com/esphome/esphome.git
synced 2025-07-28 14:16:40 +00:00
commit
1a9f02fa63
2
Doxyfile
2
Doxyfile
@ -48,7 +48,7 @@ PROJECT_NAME = ESPHome
|
|||||||
# could be handy for archiving the generated documentation or if some version
|
# could be handy for archiving the generated documentation or if some version
|
||||||
# control system is used.
|
# control system is used.
|
||||||
|
|
||||||
PROJECT_NUMBER = 2025.7.0
|
PROJECT_NUMBER = 2025.7.1
|
||||||
|
|
||||||
# Using the PROJECT_BRIEF tag one can provide an optional one line description
|
# 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
|
# for a project that appears at the top of each page and should give viewer a
|
||||||
|
@ -11,6 +11,15 @@ namespace esphome {
|
|||||||
namespace api {
|
namespace api {
|
||||||
|
|
||||||
template<typename... X> class TemplatableStringValue : public TemplatableValue<std::string, X...> {
|
template<typename... X> class TemplatableStringValue : public TemplatableValue<std::string, X...> {
|
||||||
|
private:
|
||||||
|
// Helper to convert value to string - handles the case where value is already a string
|
||||||
|
template<typename T> static std::string value_to_string(T &&val) { return to_string(std::forward<T>(val)); }
|
||||||
|
|
||||||
|
// Overloads for string types - needed because std::to_string doesn't support them
|
||||||
|
static std::string value_to_string(const char *val) { return std::string(val); } // For lambdas returning .c_str()
|
||||||
|
static std::string value_to_string(const std::string &val) { return val; }
|
||||||
|
static std::string value_to_string(std::string &&val) { return std::move(val); }
|
||||||
|
|
||||||
public:
|
public:
|
||||||
TemplatableStringValue() : TemplatableValue<std::string, X...>() {}
|
TemplatableStringValue() : TemplatableValue<std::string, X...>() {}
|
||||||
|
|
||||||
@ -19,7 +28,7 @@ template<typename... X> class TemplatableStringValue : public TemplatableValue<s
|
|||||||
|
|
||||||
template<typename F, enable_if_t<is_invocable<F, X...>::value, int> = 0>
|
template<typename F, enable_if_t<is_invocable<F, X...>::value, int> = 0>
|
||||||
TemplatableStringValue(F f)
|
TemplatableStringValue(F f)
|
||||||
: TemplatableValue<std::string, X...>([f](X... x) -> std::string { return to_string(f(x...)); }) {}
|
: TemplatableValue<std::string, X...>([f](X... x) -> std::string { return value_to_string(f(x...)); }) {}
|
||||||
};
|
};
|
||||||
|
|
||||||
template<typename... Ts> class TemplatableKeyValuePair {
|
template<typename... Ts> class TemplatableKeyValuePair {
|
||||||
|
@ -4,6 +4,7 @@
|
|||||||
#include "esphome/components/network/ip_address.h"
|
#include "esphome/components/network/ip_address.h"
|
||||||
#include "esphome/core/log.h"
|
#include "esphome/core/log.h"
|
||||||
#include "esphome/core/util.h"
|
#include "esphome/core/util.h"
|
||||||
|
#include "esphome/core/helpers.h"
|
||||||
|
|
||||||
#include <lwip/igmp.h>
|
#include <lwip/igmp.h>
|
||||||
#include <lwip/init.h>
|
#include <lwip/init.h>
|
||||||
@ -71,7 +72,11 @@ bool E131Component::join_igmp_groups_() {
|
|||||||
ip4_addr_t multicast_addr =
|
ip4_addr_t multicast_addr =
|
||||||
network::IPAddress(239, 255, ((universe.first >> 8) & 0xff), ((universe.first >> 0) & 0xff));
|
network::IPAddress(239, 255, ((universe.first >> 8) & 0xff), ((universe.first >> 0) & 0xff));
|
||||||
|
|
||||||
auto err = igmp_joingroup(IP4_ADDR_ANY4, &multicast_addr);
|
err_t err;
|
||||||
|
{
|
||||||
|
LwIPLock lock;
|
||||||
|
err = igmp_joingroup(IP4_ADDR_ANY4, &multicast_addr);
|
||||||
|
}
|
||||||
|
|
||||||
if (err) {
|
if (err) {
|
||||||
ESP_LOGW(TAG, "IGMP join for %d universe of E1.31 failed. Multicast might not work.", universe.first);
|
ESP_LOGW(TAG, "IGMP join for %d universe of E1.31 failed. Multicast might not work.", universe.first);
|
||||||
@ -104,6 +109,7 @@ void E131Component::leave_(int universe) {
|
|||||||
if (listen_method_ == E131_MULTICAST) {
|
if (listen_method_ == E131_MULTICAST) {
|
||||||
ip4_addr_t multicast_addr = network::IPAddress(239, 255, ((universe >> 8) & 0xff), ((universe >> 0) & 0xff));
|
ip4_addr_t multicast_addr = network::IPAddress(239, 255, ((universe >> 8) & 0xff), ((universe >> 0) & 0xff));
|
||||||
|
|
||||||
|
LwIPLock lock;
|
||||||
igmp_leavegroup(IP4_ADDR_ANY4, &multicast_addr);
|
igmp_leavegroup(IP4_ADDR_ANY4, &multicast_addr);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
#include "esphome/core/helpers.h"
|
#include "esphome/core/helpers.h"
|
||||||
|
#include "esphome/core/defines.h"
|
||||||
|
|
||||||
#ifdef USE_ESP32
|
#ifdef USE_ESP32
|
||||||
|
|
||||||
@ -30,6 +31,45 @@ void Mutex::unlock() { xSemaphoreGive(this->handle_); }
|
|||||||
IRAM_ATTR InterruptLock::InterruptLock() { portDISABLE_INTERRUPTS(); }
|
IRAM_ATTR InterruptLock::InterruptLock() { portDISABLE_INTERRUPTS(); }
|
||||||
IRAM_ATTR InterruptLock::~InterruptLock() { portENABLE_INTERRUPTS(); }
|
IRAM_ATTR InterruptLock::~InterruptLock() { portENABLE_INTERRUPTS(); }
|
||||||
|
|
||||||
|
#ifdef CONFIG_LWIP_TCPIP_CORE_LOCKING
|
||||||
|
#include "lwip/priv/tcpip_priv.h"
|
||||||
|
#endif
|
||||||
|
|
||||||
|
LwIPLock::LwIPLock() {
|
||||||
|
#ifdef CONFIG_LWIP_TCPIP_CORE_LOCKING
|
||||||
|
// When CONFIG_LWIP_TCPIP_CORE_LOCKING is enabled, lwIP uses a global mutex to protect
|
||||||
|
// its internal state. Any thread can take this lock to safely access lwIP APIs.
|
||||||
|
//
|
||||||
|
// sys_thread_tcpip(LWIP_CORE_LOCK_QUERY_HOLDER) returns true if the current thread
|
||||||
|
// already holds the lwIP core lock. This prevents recursive locking attempts and
|
||||||
|
// allows nested LwIPLock instances to work correctly.
|
||||||
|
//
|
||||||
|
// If we don't already hold the lock, acquire it. This will block until the lock
|
||||||
|
// is available if another thread currently holds it.
|
||||||
|
if (!sys_thread_tcpip(LWIP_CORE_LOCK_QUERY_HOLDER)) {
|
||||||
|
LOCK_TCPIP_CORE();
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
LwIPLock::~LwIPLock() {
|
||||||
|
#ifdef CONFIG_LWIP_TCPIP_CORE_LOCKING
|
||||||
|
// Only release the lwIP core lock if this thread currently holds it.
|
||||||
|
//
|
||||||
|
// sys_thread_tcpip(LWIP_CORE_LOCK_QUERY_HOLDER) queries lwIP's internal lock
|
||||||
|
// ownership tracking. It returns true only if the current thread is registered
|
||||||
|
// as the lock holder.
|
||||||
|
//
|
||||||
|
// This check is essential because:
|
||||||
|
// 1. We may not have acquired the lock in the constructor (if we already held it)
|
||||||
|
// 2. The lock might have been released by other means between constructor and destructor
|
||||||
|
// 3. Calling UNLOCK_TCPIP_CORE() without holding the lock causes undefined behavior
|
||||||
|
if (sys_thread_tcpip(LWIP_CORE_LOCK_QUERY_HOLDER)) {
|
||||||
|
UNLOCK_TCPIP_CORE();
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
void get_mac_address_raw(uint8_t *mac) { // NOLINT(readability-non-const-parameter)
|
void get_mac_address_raw(uint8_t *mac) { // NOLINT(readability-non-const-parameter)
|
||||||
#if defined(CONFIG_SOC_IEEE802154_SUPPORTED)
|
#if defined(CONFIG_SOC_IEEE802154_SUPPORTED)
|
||||||
// When CONFIG_SOC_IEEE802154_SUPPORTED is defined, esp_efuse_mac_get_default
|
// When CONFIG_SOC_IEEE802154_SUPPORTED is defined, esp_efuse_mac_get_default
|
||||||
|
@ -22,6 +22,10 @@ void Mutex::unlock() {}
|
|||||||
IRAM_ATTR InterruptLock::InterruptLock() { state_ = xt_rsil(15); }
|
IRAM_ATTR InterruptLock::InterruptLock() { state_ = xt_rsil(15); }
|
||||||
IRAM_ATTR InterruptLock::~InterruptLock() { xt_wsr_ps(state_); }
|
IRAM_ATTR InterruptLock::~InterruptLock() { xt_wsr_ps(state_); }
|
||||||
|
|
||||||
|
// ESP8266 doesn't support lwIP core locking, so this is a no-op
|
||||||
|
LwIPLock::LwIPLock() {}
|
||||||
|
LwIPLock::~LwIPLock() {}
|
||||||
|
|
||||||
void get_mac_address_raw(uint8_t *mac) { // NOLINT(readability-non-const-parameter)
|
void get_mac_address_raw(uint8_t *mac) { // NOLINT(readability-non-const-parameter)
|
||||||
wifi_get_macaddr(STATION_IF, mac);
|
wifi_get_macaddr(STATION_IF, mac);
|
||||||
}
|
}
|
||||||
|
@ -420,6 +420,7 @@ network::IPAddresses EthernetComponent::get_ip_addresses() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
network::IPAddress EthernetComponent::get_dns_address(uint8_t num) {
|
network::IPAddress EthernetComponent::get_dns_address(uint8_t num) {
|
||||||
|
LwIPLock lock;
|
||||||
const ip_addr_t *dns_ip = dns_getserver(num);
|
const ip_addr_t *dns_ip = dns_getserver(num);
|
||||||
return dns_ip;
|
return dns_ip;
|
||||||
}
|
}
|
||||||
@ -527,6 +528,7 @@ void EthernetComponent::start_connect_() {
|
|||||||
ESPHL_ERROR_CHECK(err, "DHCPC set IP info error");
|
ESPHL_ERROR_CHECK(err, "DHCPC set IP info error");
|
||||||
|
|
||||||
if (this->manual_ip_.has_value()) {
|
if (this->manual_ip_.has_value()) {
|
||||||
|
LwIPLock lock;
|
||||||
if (this->manual_ip_->dns1.is_set()) {
|
if (this->manual_ip_->dns1.is_set()) {
|
||||||
ip_addr_t d;
|
ip_addr_t d;
|
||||||
d = this->manual_ip_->dns1;
|
d = this->manual_ip_->dns1;
|
||||||
@ -559,8 +561,13 @@ bool EthernetComponent::is_connected() { return this->state_ == EthernetComponen
|
|||||||
void EthernetComponent::dump_connect_params_() {
|
void EthernetComponent::dump_connect_params_() {
|
||||||
esp_netif_ip_info_t ip;
|
esp_netif_ip_info_t ip;
|
||||||
esp_netif_get_ip_info(this->eth_netif_, &ip);
|
esp_netif_get_ip_info(this->eth_netif_, &ip);
|
||||||
const ip_addr_t *dns_ip1 = dns_getserver(0);
|
const ip_addr_t *dns_ip1;
|
||||||
const ip_addr_t *dns_ip2 = dns_getserver(1);
|
const ip_addr_t *dns_ip2;
|
||||||
|
{
|
||||||
|
LwIPLock lock;
|
||||||
|
dns_ip1 = dns_getserver(0);
|
||||||
|
dns_ip2 = dns_getserver(1);
|
||||||
|
}
|
||||||
|
|
||||||
ESP_LOGCONFIG(TAG,
|
ESP_LOGCONFIG(TAG,
|
||||||
" IP Address: %s\n"
|
" IP Address: %s\n"
|
||||||
|
@ -26,6 +26,10 @@ void Mutex::unlock() { xSemaphoreGive(this->handle_); }
|
|||||||
IRAM_ATTR InterruptLock::InterruptLock() { portDISABLE_INTERRUPTS(); }
|
IRAM_ATTR InterruptLock::InterruptLock() { portDISABLE_INTERRUPTS(); }
|
||||||
IRAM_ATTR InterruptLock::~InterruptLock() { portENABLE_INTERRUPTS(); }
|
IRAM_ATTR InterruptLock::~InterruptLock() { portENABLE_INTERRUPTS(); }
|
||||||
|
|
||||||
|
// LibreTiny doesn't support lwIP core locking, so this is a no-op
|
||||||
|
LwIPLock::LwIPLock() {}
|
||||||
|
LwIPLock::~LwIPLock() {}
|
||||||
|
|
||||||
void get_mac_address_raw(uint8_t *mac) { // NOLINT(readability-non-const-parameter)
|
void get_mac_address_raw(uint8_t *mac) { // NOLINT(readability-non-const-parameter)
|
||||||
WiFi.macAddress(mac);
|
WiFi.macAddress(mac);
|
||||||
}
|
}
|
||||||
|
@ -264,7 +264,7 @@ class MeterType(WidgetType):
|
|||||||
color_start,
|
color_start,
|
||||||
color_end,
|
color_end,
|
||||||
v[CONF_LOCAL],
|
v[CONF_LOCAL],
|
||||||
size.process(v[CONF_WIDTH]),
|
await size.process(v[CONF_WIDTH]),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
if t == CONF_IMAGE:
|
if t == CONF_IMAGE:
|
||||||
|
@ -193,13 +193,17 @@ void MQTTClientComponent::start_dnslookup_() {
|
|||||||
this->dns_resolve_error_ = false;
|
this->dns_resolve_error_ = false;
|
||||||
this->dns_resolved_ = false;
|
this->dns_resolved_ = false;
|
||||||
ip_addr_t addr;
|
ip_addr_t addr;
|
||||||
|
err_t err;
|
||||||
|
{
|
||||||
|
LwIPLock lock;
|
||||||
#if USE_NETWORK_IPV6
|
#if USE_NETWORK_IPV6
|
||||||
err_t err = dns_gethostbyname_addrtype(this->credentials_.address.c_str(), &addr,
|
err = dns_gethostbyname_addrtype(this->credentials_.address.c_str(), &addr, MQTTClientComponent::dns_found_callback,
|
||||||
MQTTClientComponent::dns_found_callback, this, LWIP_DNS_ADDRTYPE_IPV6_IPV4);
|
this, LWIP_DNS_ADDRTYPE_IPV6_IPV4);
|
||||||
#else
|
#else
|
||||||
err_t err = dns_gethostbyname_addrtype(this->credentials_.address.c_str(), &addr,
|
err = dns_gethostbyname_addrtype(this->credentials_.address.c_str(), &addr, MQTTClientComponent::dns_found_callback,
|
||||||
MQTTClientComponent::dns_found_callback, this, LWIP_DNS_ADDRTYPE_IPV4);
|
this, LWIP_DNS_ADDRTYPE_IPV4);
|
||||||
#endif /* USE_NETWORK_IPV6 */
|
#endif /* USE_NETWORK_IPV6 */
|
||||||
|
}
|
||||||
switch (err) {
|
switch (err) {
|
||||||
case ERR_OK: {
|
case ERR_OK: {
|
||||||
// Got IP immediately
|
// Got IP immediately
|
||||||
|
@ -44,6 +44,10 @@ void Mutex::unlock() {}
|
|||||||
IRAM_ATTR InterruptLock::InterruptLock() { state_ = save_and_disable_interrupts(); }
|
IRAM_ATTR InterruptLock::InterruptLock() { state_ = save_and_disable_interrupts(); }
|
||||||
IRAM_ATTR InterruptLock::~InterruptLock() { restore_interrupts(state_); }
|
IRAM_ATTR InterruptLock::~InterruptLock() { restore_interrupts(state_); }
|
||||||
|
|
||||||
|
// RP2040 doesn't support lwIP core locking, so this is a no-op
|
||||||
|
LwIPLock::LwIPLock() {}
|
||||||
|
LwIPLock::~LwIPLock() {}
|
||||||
|
|
||||||
void get_mac_address_raw(uint8_t *mac) { // NOLINT(readability-non-const-parameter)
|
void get_mac_address_raw(uint8_t *mac) { // NOLINT(readability-non-const-parameter)
|
||||||
#ifdef USE_WIFI
|
#ifdef USE_WIFI
|
||||||
WiFi.macAddress(mac);
|
WiFi.macAddress(mac);
|
||||||
|
@ -74,13 +74,14 @@ def validate_local(config: ConfigType) -> ConfigType:
|
|||||||
return config
|
return config
|
||||||
|
|
||||||
|
|
||||||
def validate_ota_removed(config: ConfigType) -> ConfigType:
|
def validate_ota(config: ConfigType) -> ConfigType:
|
||||||
# Only raise error if OTA is explicitly enabled (True)
|
# The OTA option only accepts False to explicitly disable OTA for web_server
|
||||||
# If it's False or not specified, we can safely ignore it
|
# IMPORTANT: Setting ota: false ONLY affects the web_server component
|
||||||
if config.get(CONF_OTA):
|
# The captive_portal component will still be able to perform OTA updates
|
||||||
|
if CONF_OTA in config and config[CONF_OTA] is not False:
|
||||||
raise cv.Invalid(
|
raise cv.Invalid(
|
||||||
f"The '{CONF_OTA}' option has been removed from 'web_server'. "
|
f"The '{CONF_OTA}' option in 'web_server' only accepts 'false' to disable OTA. "
|
||||||
f"Please use the new OTA platform structure instead:\n\n"
|
f"To enable OTA, please use the new OTA platform structure instead:\n\n"
|
||||||
f"ota:\n"
|
f"ota:\n"
|
||||||
f" - platform: web_server\n\n"
|
f" - platform: web_server\n\n"
|
||||||
f"See https://esphome.io/components/ota for more information."
|
f"See https://esphome.io/components/ota for more information."
|
||||||
@ -185,7 +186,7 @@ CONFIG_SCHEMA = cv.All(
|
|||||||
web_server_base.WebServerBase
|
web_server_base.WebServerBase
|
||||||
),
|
),
|
||||||
cv.Optional(CONF_INCLUDE_INTERNAL, default=False): cv.boolean,
|
cv.Optional(CONF_INCLUDE_INTERNAL, default=False): cv.boolean,
|
||||||
cv.Optional(CONF_OTA, default=False): cv.boolean,
|
cv.Optional(CONF_OTA): cv.boolean,
|
||||||
cv.Optional(CONF_LOG, default=True): cv.boolean,
|
cv.Optional(CONF_LOG, default=True): cv.boolean,
|
||||||
cv.Optional(CONF_LOCAL): cv.boolean,
|
cv.Optional(CONF_LOCAL): cv.boolean,
|
||||||
cv.Optional(CONF_SORTING_GROUPS): cv.ensure_list(sorting_group),
|
cv.Optional(CONF_SORTING_GROUPS): cv.ensure_list(sorting_group),
|
||||||
@ -203,7 +204,7 @@ CONFIG_SCHEMA = cv.All(
|
|||||||
default_url,
|
default_url,
|
||||||
validate_local,
|
validate_local,
|
||||||
validate_sorting_groups,
|
validate_sorting_groups,
|
||||||
validate_ota_removed,
|
validate_ota,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -288,7 +289,11 @@ async def to_code(config):
|
|||||||
cg.add(var.set_css_url(config[CONF_CSS_URL]))
|
cg.add(var.set_css_url(config[CONF_CSS_URL]))
|
||||||
cg.add(var.set_js_url(config[CONF_JS_URL]))
|
cg.add(var.set_js_url(config[CONF_JS_URL]))
|
||||||
# OTA is now handled by the web_server OTA platform
|
# OTA is now handled by the web_server OTA platform
|
||||||
# The CONF_OTA option is kept only for backwards compatibility validation
|
# The CONF_OTA option is kept to allow explicitly disabling OTA for web_server
|
||||||
|
# IMPORTANT: This ONLY affects the web_server component, NOT captive_portal
|
||||||
|
# Captive portal will still be able to perform OTA updates even when this is set
|
||||||
|
if config.get(CONF_OTA) is False:
|
||||||
|
cg.add_define("USE_WEBSERVER_OTA_DISABLED")
|
||||||
cg.add(var.set_expose_log(config[CONF_LOG]))
|
cg.add(var.set_expose_log(config[CONF_LOG]))
|
||||||
if config[CONF_ENABLE_PRIVATE_NETWORK_ACCESS]:
|
if config[CONF_ENABLE_PRIVATE_NETWORK_ACCESS]:
|
||||||
cg.add_define("USE_WEBSERVER_PRIVATE_NETWORK_ACCESS")
|
cg.add_define("USE_WEBSERVER_PRIVATE_NETWORK_ACCESS")
|
||||||
|
@ -5,6 +5,10 @@
|
|||||||
#include "esphome/core/application.h"
|
#include "esphome/core/application.h"
|
||||||
#include "esphome/core/log.h"
|
#include "esphome/core/log.h"
|
||||||
|
|
||||||
|
#ifdef USE_CAPTIVE_PORTAL
|
||||||
|
#include "esphome/components/captive_portal/captive_portal.h"
|
||||||
|
#endif
|
||||||
|
|
||||||
#ifdef USE_ARDUINO
|
#ifdef USE_ARDUINO
|
||||||
#ifdef USE_ESP8266
|
#ifdef USE_ESP8266
|
||||||
#include <Updater.h>
|
#include <Updater.h>
|
||||||
@ -25,7 +29,22 @@ class OTARequestHandler : public AsyncWebHandler {
|
|||||||
void handleUpload(AsyncWebServerRequest *request, const String &filename, size_t index, uint8_t *data, size_t len,
|
void handleUpload(AsyncWebServerRequest *request, const String &filename, size_t index, uint8_t *data, size_t len,
|
||||||
bool final) override;
|
bool final) override;
|
||||||
bool canHandle(AsyncWebServerRequest *request) const override {
|
bool canHandle(AsyncWebServerRequest *request) const override {
|
||||||
return request->url() == "/update" && request->method() == HTTP_POST;
|
// Check if this is an OTA update request
|
||||||
|
bool is_ota_request = request->url() == "/update" && request->method() == HTTP_POST;
|
||||||
|
|
||||||
|
#if defined(USE_WEBSERVER_OTA_DISABLED) && defined(USE_CAPTIVE_PORTAL)
|
||||||
|
// IMPORTANT: USE_WEBSERVER_OTA_DISABLED only disables OTA for the web_server component
|
||||||
|
// Captive portal can still perform OTA updates - check if request is from active captive portal
|
||||||
|
// Note: global_captive_portal is the standard way components communicate in ESPHome
|
||||||
|
return is_ota_request && captive_portal::global_captive_portal != nullptr &&
|
||||||
|
captive_portal::global_captive_portal->is_active();
|
||||||
|
#elif defined(USE_WEBSERVER_OTA_DISABLED)
|
||||||
|
// OTA disabled for web_server and no captive portal compiled in
|
||||||
|
return false;
|
||||||
|
#else
|
||||||
|
// OTA enabled for web_server
|
||||||
|
return is_ota_request;
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
// NOLINTNEXTLINE(readability-identifier-naming)
|
// NOLINTNEXTLINE(readability-identifier-naming)
|
||||||
@ -152,7 +171,7 @@ void OTARequestHandler::handleUpload(AsyncWebServerRequest *request, const Strin
|
|||||||
|
|
||||||
// Finalize
|
// Finalize
|
||||||
if (final) {
|
if (final) {
|
||||||
ESP_LOGD(TAG, "OTA final chunk: index=%u, len=%u, total_read=%u, contentLength=%u", index, len,
|
ESP_LOGD(TAG, "OTA final chunk: index=%zu, len=%zu, total_read=%u, contentLength=%zu", index, len,
|
||||||
this->ota_read_length_, request->contentLength());
|
this->ota_read_length_, request->contentLength());
|
||||||
|
|
||||||
// For Arduino framework, the Update library tracks expected size from firmware header
|
// For Arduino framework, the Update library tracks expected size from firmware header
|
||||||
|
@ -268,10 +268,10 @@ std::string WebServer::get_config_json() {
|
|||||||
return json::build_json([this](JsonObject root) {
|
return json::build_json([this](JsonObject root) {
|
||||||
root["title"] = App.get_friendly_name().empty() ? App.get_name() : App.get_friendly_name();
|
root["title"] = App.get_friendly_name().empty() ? App.get_name() : App.get_friendly_name();
|
||||||
root["comment"] = App.get_comment();
|
root["comment"] = App.get_comment();
|
||||||
#ifdef USE_WEBSERVER_OTA
|
#if defined(USE_WEBSERVER_OTA_DISABLED) || !defined(USE_WEBSERVER_OTA)
|
||||||
root["ota"] = true; // web_server OTA platform is configured
|
root["ota"] = false; // Note: USE_WEBSERVER_OTA_DISABLED only affects web_server, not captive_portal
|
||||||
#else
|
#else
|
||||||
root["ota"] = false;
|
root["ota"] = true;
|
||||||
#endif
|
#endif
|
||||||
root["log"] = this->expose_log_;
|
root["log"] = this->expose_log_;
|
||||||
root["lang"] = "en";
|
root["lang"] = "en";
|
||||||
|
@ -192,7 +192,9 @@ void WebServer::handle_index_request(AsyncWebServerRequest *request) {
|
|||||||
|
|
||||||
stream->print(F("</tbody></table><p>See <a href=\"https://esphome.io/web-api/index.html\">ESPHome Web API</a> for "
|
stream->print(F("</tbody></table><p>See <a href=\"https://esphome.io/web-api/index.html\">ESPHome Web API</a> for "
|
||||||
"REST API documentation.</p>"));
|
"REST API documentation.</p>"));
|
||||||
#ifdef USE_WEBSERVER_OTA
|
#if defined(USE_WEBSERVER_OTA) && !defined(USE_WEBSERVER_OTA_DISABLED)
|
||||||
|
// Show OTA form only if web_server OTA is not explicitly disabled
|
||||||
|
// Note: USE_WEBSERVER_OTA_DISABLED only affects web_server, not captive_portal
|
||||||
stream->print(F("<h2>OTA Update</h2><form method=\"POST\" action=\"/update\" enctype=\"multipart/form-data\"><input "
|
stream->print(F("<h2>OTA Update</h2><form method=\"POST\" action=\"/update\" enctype=\"multipart/form-data\"><input "
|
||||||
"type=\"file\" name=\"update\"><input type=\"submit\" value=\"Update\"></form>"));
|
"type=\"file\" name=\"update\"><input type=\"submit\" value=\"Update\"></form>"));
|
||||||
#endif
|
#endif
|
||||||
|
@ -20,10 +20,6 @@
|
|||||||
#include "lwip/dns.h"
|
#include "lwip/dns.h"
|
||||||
#include "lwip/err.h"
|
#include "lwip/err.h"
|
||||||
|
|
||||||
#ifdef CONFIG_LWIP_TCPIP_CORE_LOCKING
|
|
||||||
#include "lwip/priv/tcpip_priv.h"
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#include "esphome/core/application.h"
|
#include "esphome/core/application.h"
|
||||||
#include "esphome/core/hal.h"
|
#include "esphome/core/hal.h"
|
||||||
#include "esphome/core/helpers.h"
|
#include "esphome/core/helpers.h"
|
||||||
@ -295,25 +291,16 @@ bool WiFiComponent::wifi_sta_ip_config_(optional<ManualIP> manual_ip) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!manual_ip.has_value()) {
|
if (!manual_ip.has_value()) {
|
||||||
// sntp_servermode_dhcp lwip/sntp.c (Required to lock TCPIP core functionality!)
|
// sntp_servermode_dhcp lwip/sntp.c (Required to lock TCPIP core functionality!)
|
||||||
// https://github.com/esphome/issues/issues/6591
|
// https://github.com/esphome/issues/issues/6591
|
||||||
// https://github.com/espressif/arduino-esp32/issues/10526
|
// https://github.com/espressif/arduino-esp32/issues/10526
|
||||||
#ifdef CONFIG_LWIP_TCPIP_CORE_LOCKING
|
{
|
||||||
if (!sys_thread_tcpip(LWIP_CORE_LOCK_QUERY_HOLDER)) {
|
LwIPLock lock;
|
||||||
LOCK_TCPIP_CORE();
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|
||||||
// lwIP starts the SNTP client if it gets an SNTP server from DHCP. We don't need the time, and more importantly,
|
// lwIP starts the SNTP client if it gets an SNTP server from DHCP. We don't need the time, and more importantly,
|
||||||
// the built-in SNTP client has a memory leak in certain situations. Disable this feature.
|
// the built-in SNTP client has a memory leak in certain situations. Disable this feature.
|
||||||
// https://github.com/esphome/issues/issues/2299
|
// https://github.com/esphome/issues/issues/2299
|
||||||
sntp_servermode_dhcp(false);
|
sntp_servermode_dhcp(false);
|
||||||
|
|
||||||
#ifdef CONFIG_LWIP_TCPIP_CORE_LOCKING
|
|
||||||
if (sys_thread_tcpip(LWIP_CORE_LOCK_QUERY_HOLDER)) {
|
|
||||||
UNLOCK_TCPIP_CORE();
|
|
||||||
}
|
}
|
||||||
#endif
|
|
||||||
|
|
||||||
// No manual IP is set; use DHCP client
|
// No manual IP is set; use DHCP client
|
||||||
if (dhcp_status != ESP_NETIF_DHCP_STARTED) {
|
if (dhcp_status != ESP_NETIF_DHCP_STARTED) {
|
||||||
|
@ -4,7 +4,7 @@ from enum import Enum
|
|||||||
|
|
||||||
from esphome.enum import StrEnum
|
from esphome.enum import StrEnum
|
||||||
|
|
||||||
__version__ = "2025.7.0"
|
__version__ = "2025.7.1"
|
||||||
|
|
||||||
ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_"
|
ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_"
|
||||||
VALID_SUBSTITUTIONS_CHARACTERS = (
|
VALID_SUBSTITUTIONS_CHARACTERS = (
|
||||||
|
@ -683,6 +683,23 @@ class InterruptLock {
|
|||||||
#endif
|
#endif
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** Helper class to lock the lwIP TCPIP core when making lwIP API calls from non-TCPIP threads.
|
||||||
|
*
|
||||||
|
* This is needed on multi-threaded platforms (ESP32) when CONFIG_LWIP_TCPIP_CORE_LOCKING is enabled.
|
||||||
|
* It ensures thread-safe access to lwIP APIs.
|
||||||
|
*
|
||||||
|
* @note This follows the same pattern as InterruptLock - platform-specific implementations in helpers.cpp
|
||||||
|
*/
|
||||||
|
class LwIPLock {
|
||||||
|
public:
|
||||||
|
LwIPLock();
|
||||||
|
~LwIPLock();
|
||||||
|
|
||||||
|
// Delete copy constructor and copy assignment operator to prevent accidental copying
|
||||||
|
LwIPLock(const LwIPLock &) = delete;
|
||||||
|
LwIPLock &operator=(const LwIPLock &) = delete;
|
||||||
|
};
|
||||||
|
|
||||||
/** Helper class to request `loop()` to be called as fast as possible.
|
/** Helper class to request `loop()` to be called as fast as possible.
|
||||||
*
|
*
|
||||||
* Usually the ESPHome main loop runs at 60 Hz, sleeping in between invocations of `loop()` if necessary. When a higher
|
* Usually the ESPHome main loop runs at 60 Hz, sleeping in between invocations of `loop()` if necessary. When a higher
|
||||||
|
@ -147,6 +147,13 @@ class RedirectText:
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
self._write_color_replace(line)
|
self._write_color_replace(line)
|
||||||
|
# Check for flash size error and provide helpful guidance
|
||||||
|
if (
|
||||||
|
"Error: The program size" in line
|
||||||
|
and "is greater than maximum allowed" in line
|
||||||
|
and (help_msg := get_esp32_arduino_flash_error_help())
|
||||||
|
):
|
||||||
|
self._write_color_replace(help_msg)
|
||||||
else:
|
else:
|
||||||
self._write_color_replace(s)
|
self._write_color_replace(s)
|
||||||
|
|
||||||
@ -309,3 +316,34 @@ def get_serial_ports() -> list[SerialPort]:
|
|||||||
|
|
||||||
result.sort(key=lambda x: x.path)
|
result.sort(key=lambda x: x.path)
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def get_esp32_arduino_flash_error_help() -> str | None:
|
||||||
|
"""Returns helpful message when ESP32 with Arduino runs out of flash space."""
|
||||||
|
from esphome.core import CORE
|
||||||
|
|
||||||
|
if not (CORE.is_esp32 and CORE.using_arduino):
|
||||||
|
return None
|
||||||
|
|
||||||
|
from esphome.log import AnsiFore, color
|
||||||
|
|
||||||
|
return (
|
||||||
|
"\n"
|
||||||
|
+ color(
|
||||||
|
AnsiFore.YELLOW,
|
||||||
|
"💡 TIP: Your ESP32 with Arduino framework has run out of flash space.\n",
|
||||||
|
)
|
||||||
|
+ "\n"
|
||||||
|
+ "To fix this, switch to the ESP-IDF framework which is more memory efficient:\n"
|
||||||
|
+ "\n"
|
||||||
|
+ "1. In your YAML configuration, modify the framework section:\n"
|
||||||
|
+ "\n"
|
||||||
|
+ " esp32:\n"
|
||||||
|
+ " framework:\n"
|
||||||
|
+ " type: esp-idf\n"
|
||||||
|
+ "\n"
|
||||||
|
+ "2. Clean build files and compile again\n"
|
||||||
|
+ "\n"
|
||||||
|
+ "Note: ESP-IDF uses less flash space and provides better performance.\n"
|
||||||
|
+ "Some Arduino-specific libraries may need alternatives.\n\n"
|
||||||
|
)
|
||||||
|
@ -8,31 +8,31 @@ from esphome.types import ConfigType
|
|||||||
|
|
||||||
def test_web_server_ota_true_fails_validation() -> None:
|
def test_web_server_ota_true_fails_validation() -> None:
|
||||||
"""Test that web_server with ota: true fails validation with helpful message."""
|
"""Test that web_server with ota: true fails validation with helpful message."""
|
||||||
from esphome.components.web_server import validate_ota_removed
|
from esphome.components.web_server import validate_ota
|
||||||
|
|
||||||
# Config with ota: true should fail
|
# Config with ota: true should fail
|
||||||
config: ConfigType = {"ota": True}
|
config: ConfigType = {"ota": True}
|
||||||
|
|
||||||
with pytest.raises(cv.Invalid) as exc_info:
|
with pytest.raises(cv.Invalid) as exc_info:
|
||||||
validate_ota_removed(config)
|
validate_ota(config)
|
||||||
|
|
||||||
# Check error message contains migration instructions
|
# Check error message contains migration instructions
|
||||||
error_msg = str(exc_info.value)
|
error_msg = str(exc_info.value)
|
||||||
assert "has been removed from 'web_server'" in error_msg
|
assert "only accepts 'false' to disable OTA" in error_msg
|
||||||
assert "platform: web_server" in error_msg
|
assert "platform: web_server" in error_msg
|
||||||
assert "ota:" in error_msg
|
assert "ota:" in error_msg
|
||||||
|
|
||||||
|
|
||||||
def test_web_server_ota_false_passes_validation() -> None:
|
def test_web_server_ota_false_passes_validation() -> None:
|
||||||
"""Test that web_server with ota: false passes validation."""
|
"""Test that web_server with ota: false passes validation."""
|
||||||
from esphome.components.web_server import validate_ota_removed
|
from esphome.components.web_server import validate_ota
|
||||||
|
|
||||||
# Config with ota: false should pass
|
# Config with ota: false should pass
|
||||||
config: ConfigType = {"ota": False}
|
config: ConfigType = {"ota": False}
|
||||||
result = validate_ota_removed(config)
|
result = validate_ota(config)
|
||||||
assert result == config
|
assert result == config
|
||||||
|
|
||||||
# Config without ota should also pass
|
# Config without ota should also pass
|
||||||
config: ConfigType = {}
|
config: ConfigType = {}
|
||||||
result = validate_ota_removed(config)
|
result = validate_ota(config)
|
||||||
assert result == config
|
assert result == config
|
||||||
|
@ -928,6 +928,12 @@ lvgl:
|
|||||||
angle_range: 360
|
angle_range: 360
|
||||||
rotation: !lambda return 2700;
|
rotation: !lambda return 2700;
|
||||||
indicators:
|
indicators:
|
||||||
|
- tick_style:
|
||||||
|
start_value: 0
|
||||||
|
end_value: 60
|
||||||
|
color_start: 0x0000bd
|
||||||
|
color_end: 0xbd0000
|
||||||
|
width: !lambda return 1;
|
||||||
- line:
|
- line:
|
||||||
opa: 50%
|
opa: 50%
|
||||||
id: minute_hand
|
id: minute_hand
|
||||||
|
64
tests/integration/fixtures/api_string_lambda.yaml
Normal file
64
tests/integration/fixtures/api_string_lambda.yaml
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
esphome:
|
||||||
|
name: api-string-lambda-test
|
||||||
|
host:
|
||||||
|
|
||||||
|
api:
|
||||||
|
actions:
|
||||||
|
# Service that tests string lambda functionality
|
||||||
|
- action: test_string_lambda
|
||||||
|
variables:
|
||||||
|
input_string: string
|
||||||
|
then:
|
||||||
|
# Log the input to verify service was called
|
||||||
|
- logger.log:
|
||||||
|
format: "Service called with string: %s"
|
||||||
|
args: [input_string.c_str()]
|
||||||
|
|
||||||
|
# This is the key test - using a lambda that returns x.c_str()
|
||||||
|
# where x is already a string. This would fail to compile in 2025.7.0b5
|
||||||
|
# with "no matching function for call to 'to_string(std::string)'"
|
||||||
|
# This is the exact case from issue #9539
|
||||||
|
- homeassistant.tag_scanned: !lambda 'return input_string.c_str();'
|
||||||
|
|
||||||
|
# Also test with homeassistant.event to verify our fix works with data fields
|
||||||
|
- homeassistant.event:
|
||||||
|
event: esphome.test_string_lambda
|
||||||
|
data:
|
||||||
|
value: !lambda 'return input_string.c_str();'
|
||||||
|
|
||||||
|
# Service that tests int lambda functionality
|
||||||
|
- action: test_int_lambda
|
||||||
|
variables:
|
||||||
|
input_number: int
|
||||||
|
then:
|
||||||
|
# Log the input to verify service was called
|
||||||
|
- logger.log:
|
||||||
|
format: "Service called with int: %d"
|
||||||
|
args: [input_number]
|
||||||
|
|
||||||
|
# Test that int lambdas still work correctly with to_string
|
||||||
|
# The TemplatableStringValue should automatically convert int to string
|
||||||
|
- homeassistant.event:
|
||||||
|
event: esphome.test_int_lambda
|
||||||
|
data:
|
||||||
|
value: !lambda 'return input_number;'
|
||||||
|
|
||||||
|
# Service that tests float lambda functionality
|
||||||
|
- action: test_float_lambda
|
||||||
|
variables:
|
||||||
|
input_float: float
|
||||||
|
then:
|
||||||
|
# Log the input to verify service was called
|
||||||
|
- logger.log:
|
||||||
|
format: "Service called with float: %.2f"
|
||||||
|
args: [input_float]
|
||||||
|
|
||||||
|
# Test that float lambdas still work correctly with to_string
|
||||||
|
# The TemplatableStringValue should automatically convert float to string
|
||||||
|
- homeassistant.event:
|
||||||
|
event: esphome.test_float_lambda
|
||||||
|
data:
|
||||||
|
value: !lambda 'return input_float;'
|
||||||
|
|
||||||
|
logger:
|
||||||
|
level: DEBUG
|
85
tests/integration/test_api_string_lambda.py
Normal file
85
tests/integration/test_api_string_lambda.py
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
"""Integration test for TemplatableStringValue with string lambdas."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import re
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from .types import APIClientConnectedFactory, RunCompiledFunction
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_api_string_lambda(
|
||||||
|
yaml_config: str,
|
||||||
|
run_compiled: RunCompiledFunction,
|
||||||
|
api_client_connected: APIClientConnectedFactory,
|
||||||
|
) -> None:
|
||||||
|
"""Test TemplatableStringValue works with lambdas that return different types."""
|
||||||
|
loop = asyncio.get_running_loop()
|
||||||
|
|
||||||
|
# Track log messages for all three service calls
|
||||||
|
string_called_future = loop.create_future()
|
||||||
|
int_called_future = loop.create_future()
|
||||||
|
float_called_future = loop.create_future()
|
||||||
|
|
||||||
|
# Patterns to match in logs - confirms the lambdas compiled and executed
|
||||||
|
string_pattern = re.compile(r"Service called with string: STRING_FROM_LAMBDA")
|
||||||
|
int_pattern = re.compile(r"Service called with int: 42")
|
||||||
|
float_pattern = re.compile(r"Service called with float: 3\.14")
|
||||||
|
|
||||||
|
def check_output(line: str) -> None:
|
||||||
|
"""Check log output for expected messages."""
|
||||||
|
if not string_called_future.done() and string_pattern.search(line):
|
||||||
|
string_called_future.set_result(True)
|
||||||
|
if not int_called_future.done() and int_pattern.search(line):
|
||||||
|
int_called_future.set_result(True)
|
||||||
|
if not float_called_future.done() and float_pattern.search(line):
|
||||||
|
float_called_future.set_result(True)
|
||||||
|
|
||||||
|
# Run with log monitoring
|
||||||
|
async with (
|
||||||
|
run_compiled(yaml_config, line_callback=check_output),
|
||||||
|
api_client_connected() as client,
|
||||||
|
):
|
||||||
|
# Verify device info
|
||||||
|
device_info = await client.device_info()
|
||||||
|
assert device_info is not None
|
||||||
|
assert device_info.name == "api-string-lambda-test"
|
||||||
|
|
||||||
|
# List services to find our test services
|
||||||
|
_, services = await client.list_entities_services()
|
||||||
|
|
||||||
|
# Find all test services
|
||||||
|
string_service = next(
|
||||||
|
(s for s in services if s.name == "test_string_lambda"), None
|
||||||
|
)
|
||||||
|
assert string_service is not None, "test_string_lambda service not found"
|
||||||
|
|
||||||
|
int_service = next((s for s in services if s.name == "test_int_lambda"), None)
|
||||||
|
assert int_service is not None, "test_int_lambda service not found"
|
||||||
|
|
||||||
|
float_service = next(
|
||||||
|
(s for s in services if s.name == "test_float_lambda"), None
|
||||||
|
)
|
||||||
|
assert float_service is not None, "test_float_lambda service not found"
|
||||||
|
|
||||||
|
# Execute all three services to test different lambda return types
|
||||||
|
client.execute_service(string_service, {"input_string": "STRING_FROM_LAMBDA"})
|
||||||
|
client.execute_service(int_service, {"input_number": 42})
|
||||||
|
client.execute_service(float_service, {"input_float": 3.14})
|
||||||
|
|
||||||
|
# Wait for all service log messages
|
||||||
|
# This confirms the lambdas compiled successfully and executed
|
||||||
|
try:
|
||||||
|
await asyncio.wait_for(
|
||||||
|
asyncio.gather(
|
||||||
|
string_called_future, int_called_future, float_called_future
|
||||||
|
),
|
||||||
|
timeout=5.0,
|
||||||
|
)
|
||||||
|
except TimeoutError:
|
||||||
|
pytest.fail(
|
||||||
|
"One or more service log messages not received - lambda may have failed to compile or execute"
|
||||||
|
)
|
Loading…
x
Reference in New Issue
Block a user