From eae9335894cc3a7a2d567bb888572a095a7aa9ed Mon Sep 17 00:00:00 2001 From: kbx81 Date: Mon, 15 Sep 2025 18:35:25 -0500 Subject: [PATCH 001/415] [wifi_info] Use callbacks instead of polling --- esphome/components/wifi/automation.h | 113 ++++++++++++++++ esphome/components/wifi/wifi_component.h | 123 ++++-------------- .../wifi/wifi_component_esp32_arduino.cpp | 6 +- .../wifi/wifi_component_esp8266.cpp | 7 + .../wifi/wifi_component_esp_idf.cpp | 5 + .../wifi/wifi_component_libretiny.cpp | 6 +- .../components/wifi/wifi_component_pico_w.cpp | 3 +- esphome/components/wifi_info/text_sensor.py | 24 ++-- .../wifi_info/wifi_info_text_sensor.cpp | 108 ++++++++++++++- .../wifi_info/wifi_info_text_sensor.h | 92 +++---------- 10 files changed, 288 insertions(+), 199 deletions(-) create mode 100644 esphome/components/wifi/automation.h diff --git a/esphome/components/wifi/automation.h b/esphome/components/wifi/automation.h new file mode 100644 index 0000000000..0651eafca2 --- /dev/null +++ b/esphome/components/wifi/automation.h @@ -0,0 +1,113 @@ +#pragma once + +#include "esphome/core/defines.h" +#ifdef USE_WIFI +#include "wifi_component.h" + +namespace esphome { +namespace wifi { + +template class WiFiConnectedCondition : public Condition { + public: + bool check(Ts... x) override { return global_wifi_component->is_connected(); } +}; + +template class WiFiEnabledCondition : public Condition { + public: + bool check(Ts... x) override { return !global_wifi_component->is_disabled(); } +}; + +template class WiFiEnableAction : public Action { + public: + void play(Ts... x) override { global_wifi_component->enable(); } +}; + +template class WiFiDisableAction : public Action { + public: + void play(Ts... x) override { global_wifi_component->disable(); } +}; + +template class WiFiConfigureAction : public Action, public Component { + public: + TEMPLATABLE_VALUE(std::string, ssid) + TEMPLATABLE_VALUE(std::string, password) + TEMPLATABLE_VALUE(bool, save) + TEMPLATABLE_VALUE(uint32_t, connection_timeout) + + void play(Ts... x) override { + auto ssid = this->ssid_.value(x...); + auto password = this->password_.value(x...); + // Avoid multiple calls + if (this->connecting_) + return; + // If already connected to the same AP, do nothing + if (global_wifi_component->wifi_ssid() == ssid) { + // Callback to notify the user that the connection was successful + this->connect_trigger_->trigger(); + return; + } + // Create a new WiFiAP object with the new SSID and password + this->new_sta_.set_ssid(ssid); + this->new_sta_.set_password(password); + // Save the current STA + this->old_sta_ = global_wifi_component->get_sta(); + // Disable WiFi + global_wifi_component->disable(); + // Set the state to connecting + this->connecting_ = true; + // Store the new STA so once the WiFi is enabled, it will connect to it + // This is necessary because the WiFiComponent will raise an error and fallback to the saved STA + // if trying to connect to a new STA while already connected to another one + if (this->save_.value(x...)) { + global_wifi_component->save_wifi_sta(new_sta_.get_ssid(), new_sta_.get_password()); + } else { + global_wifi_component->set_sta(new_sta_); + } + // Enable WiFi + global_wifi_component->enable(); + // Set timeout for the connection + this->set_timeout("wifi-connect-timeout", this->connection_timeout_.value(x...), [this, x...]() { + // If the timeout is reached, stop connecting and revert to the old AP + global_wifi_component->disable(); + global_wifi_component->save_wifi_sta(old_sta_.get_ssid(), old_sta_.get_password()); + global_wifi_component->enable(); + // 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(); + }); + }); + } + + Trigger<> *get_connect_trigger() const { return this->connect_trigger_; } + Trigger<> *get_error_trigger() const { return this->error_trigger_; } + + void loop() override { + if (!this->connecting_) + return; + if (global_wifi_component->is_connected()) { + // The WiFi is connected, stop the timeout and reset the connecting flag + this->cancel_timeout("wifi-connect-timeout"); + this->cancel_timeout("wifi-fallback-timeout"); + this->connecting_ = false; + if (global_wifi_component->wifi_ssid() == this->new_sta_.get_ssid()) { + // Callback to notify the user that the connection was successful + this->connect_trigger_->trigger(); + } else { + // Callback to notify the user that the connection failed + this->error_trigger_->trigger(); + } + } + } + + protected: + bool connecting_{false}; + WiFiAP new_sta_; + WiFiAP old_sta_; + Trigger<> *connect_trigger_{new Trigger<>()}; + Trigger<> *error_trigger_{new Trigger<>()}; +}; + +} // namespace wifi +} // namespace esphome +#endif diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index bbe1bbb874..a07c284448 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -322,6 +322,25 @@ class WiFiComponent : public Component { int32_t get_wifi_channel(); + /// Add a callback that will be called on configuration changes (IP change, SSID change, etc.) + /// @param callback The callback to be called; template arguments are: + /// - IP addresses + /// - DNS address 1 + /// - DNS address 2 + void add_on_ip_state_callback( + std::function &&callback) { + this->ip_state_callback_.add(std::move(callback)); + } + /// - Wi-Fi scan results + void add_on_wifi_scan_state_callback(std::function)> &&callback) { + this->wifi_scan_state_callback_.add(std::move(callback)); + } + /// - Wi-Fi SSID + /// - Wi-Fi BSSID + void add_on_wifi_connect_state_callback(std::function &&callback) { + this->wifi_connect_state_callback_.add(std::move(callback)); + } + protected: #ifdef USE_WIFI_AP void setup_ap_config_(); @@ -389,6 +408,9 @@ class WiFiComponent : public Component { WiFiAP selected_ap_; WiFiAP ap_; optional output_power_; + CallbackManager ip_state_callback_; + CallbackManager)> wifi_scan_state_callback_; + CallbackManager wifi_connect_state_callback_; ESPPreferenceObject pref_; ESPPreferenceObject fast_connect_pref_; @@ -432,107 +454,6 @@ class WiFiComponent : public Component { extern WiFiComponent *global_wifi_component; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) -template class WiFiConnectedCondition : public Condition { - public: - bool check(Ts... x) override { return global_wifi_component->is_connected(); } -}; - -template class WiFiEnabledCondition : public Condition { - public: - bool check(Ts... x) override { return !global_wifi_component->is_disabled(); } -}; - -template class WiFiEnableAction : public Action { - public: - void play(Ts... x) override { global_wifi_component->enable(); } -}; - -template class WiFiDisableAction : public Action { - public: - void play(Ts... x) override { global_wifi_component->disable(); } -}; - -template class WiFiConfigureAction : public Action, public Component { - public: - TEMPLATABLE_VALUE(std::string, ssid) - TEMPLATABLE_VALUE(std::string, password) - TEMPLATABLE_VALUE(bool, save) - TEMPLATABLE_VALUE(uint32_t, connection_timeout) - - void play(Ts... x) override { - auto ssid = this->ssid_.value(x...); - auto password = this->password_.value(x...); - // Avoid multiple calls - if (this->connecting_) - return; - // If already connected to the same AP, do nothing - if (global_wifi_component->wifi_ssid() == ssid) { - // Callback to notify the user that the connection was successful - this->connect_trigger_->trigger(); - return; - } - // Create a new WiFiAP object with the new SSID and password - this->new_sta_.set_ssid(ssid); - this->new_sta_.set_password(password); - // Save the current STA - this->old_sta_ = global_wifi_component->get_sta(); - // Disable WiFi - global_wifi_component->disable(); - // Set the state to connecting - this->connecting_ = true; - // Store the new STA so once the WiFi is enabled, it will connect to it - // This is necessary because the WiFiComponent will raise an error and fallback to the saved STA - // if trying to connect to a new STA while already connected to another one - if (this->save_.value(x...)) { - global_wifi_component->save_wifi_sta(new_sta_.get_ssid(), new_sta_.get_password()); - } else { - global_wifi_component->set_sta(new_sta_); - } - // Enable WiFi - global_wifi_component->enable(); - // Set timeout for the connection - this->set_timeout("wifi-connect-timeout", this->connection_timeout_.value(x...), [this, x...]() { - // If the timeout is reached, stop connecting and revert to the old AP - global_wifi_component->disable(); - global_wifi_component->save_wifi_sta(old_sta_.get_ssid(), old_sta_.get_password()); - global_wifi_component->enable(); - // 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(); - }); - }); - } - - Trigger<> *get_connect_trigger() const { return this->connect_trigger_; } - Trigger<> *get_error_trigger() const { return this->error_trigger_; } - - void loop() override { - if (!this->connecting_) - return; - if (global_wifi_component->is_connected()) { - // The WiFi is connected, stop the timeout and reset the connecting flag - this->cancel_timeout("wifi-connect-timeout"); - this->cancel_timeout("wifi-fallback-timeout"); - this->connecting_ = false; - if (global_wifi_component->wifi_ssid() == this->new_sta_.get_ssid()) { - // Callback to notify the user that the connection was successful - this->connect_trigger_->trigger(); - } else { - // Callback to notify the user that the connection failed - this->error_trigger_->trigger(); - } - } - } - - protected: - bool connecting_{false}; - WiFiAP new_sta_; - WiFiAP old_sta_; - Trigger<> *connect_trigger_{new Trigger<>()}; - Trigger<> *error_trigger_{new Trigger<>()}; -}; - } // namespace wifi } // namespace esphome #endif diff --git a/esphome/components/wifi/wifi_component_esp32_arduino.cpp b/esphome/components/wifi/wifi_component_esp32_arduino.cpp index 89298e07c7..1e97935b70 100644 --- a/esphome/components/wifi/wifi_component_esp32_arduino.cpp +++ b/esphome/components/wifi/wifi_component_esp32_arduino.cpp @@ -559,7 +559,7 @@ void WiFiComponent::wifi_event_callback_(esphome_wifi_event_id_t event, esphome_ #if USE_NETWORK_IPV6 this->set_timeout(100, [] { WiFi.enableIPv6(); }); #endif /* USE_NETWORK_IPV6 */ - + this->wifi_connect_state_callback_.call(this->wifi_ssid(), this->wifi_bssid()); break; } case ESPHOME_EVENT_ID_WIFI_STA_DISCONNECTED: { @@ -586,6 +586,7 @@ void WiFiComponent::wifi_event_callback_(esphome_wifi_event_id_t event, esphome_ } s_sta_connecting = false; + this->wifi_connect_state_callback_.call("", bssid_t({0, 0, 0, 0, 0, 0})); break; } case ESPHOME_EVENT_ID_WIFI_STA_AUTHMODE_CHANGE: { @@ -614,6 +615,7 @@ void WiFiComponent::wifi_event_callback_(esphome_wifi_event_id_t event, esphome_ #else s_sta_connecting = false; #endif /* USE_NETWORK_IPV6 */ + this->ip_state_callback_.call(this->wifi_sta_ip_addresses(), this->get_dns_address(0), this->get_dns_address(1)); break; } #if USE_NETWORK_IPV6 @@ -622,6 +624,7 @@ void WiFiComponent::wifi_event_callback_(esphome_wifi_event_id_t event, esphome_ ESP_LOGV(TAG, "IPv6 address=" IPV6STR, IPV62STR(it.ip)); this->num_ipv6_addresses_++; s_sta_connecting = !(this->got_ipv4_address_ & (this->num_ipv6_addresses_ >= USE_NETWORK_MIN_IPV6_ADDR_COUNT)); + this->ip_state_callback_.call(this->wifi_sta_ip_addresses(), this->get_dns_address(0), this->get_dns_address(1)); break; } #endif /* USE_NETWORK_IPV6 */ @@ -715,6 +718,7 @@ void WiFiComponent::wifi_scan_done_callback_() { } WiFi.scanDelete(); this->scan_done_ = true; + this->wifi_scan_state_callback_.call(this->scan_result_); } #ifdef USE_WIFI_AP diff --git a/esphome/components/wifi/wifi_component_esp8266.cpp b/esphome/components/wifi/wifi_component_esp8266.cpp index ae1daed8b5..9bb74bc425 100644 --- a/esphome/components/wifi/wifi_component_esp8266.cpp +++ b/esphome/components/wifi/wifi_component_esp8266.cpp @@ -499,6 +499,8 @@ void WiFiComponent::wifi_event_callback(System_Event_t *event) { ESP_LOGV(TAG, "Connected ssid='%s' bssid=%s channel=%u", buf, format_mac_address_pretty(it.bssid).c_str(), it.channel); s_sta_connected = true; + global_wifi_component->wifi_connect_state_callback_.call(global_wifi_component->wifi_ssid(), + global_wifi_component->wifi_bssid()); break; } case EVENT_STAMODE_DISCONNECTED: { @@ -516,6 +518,7 @@ void WiFiComponent::wifi_event_callback(System_Event_t *event) { } s_sta_connected = false; s_sta_connecting = false; + global_wifi_component->wifi_connect_state_callback_.call("", bssid_t({0, 0, 0, 0, 0, 0})); break; } case EVENT_STAMODE_AUTHMODE_CHANGE: { @@ -538,6 +541,9 @@ void WiFiComponent::wifi_event_callback(System_Event_t *event) { ESP_LOGV(TAG, "static_ip=%s gateway=%s netmask=%s", format_ip_addr(it.ip).c_str(), format_ip_addr(it.gw).c_str(), format_ip_addr(it.mask).c_str()); s_sta_got_ip = true; + global_wifi_component->ip_state_callback_.call(global_wifi_component->wifi_sta_ip_addresses(), + global_wifi_component->get_dns_address(0), + global_wifi_component->get_dns_address(1)); break; } case EVENT_STAMODE_DHCP_TIMEOUT: { @@ -704,6 +710,7 @@ void WiFiComponent::wifi_scan_done_callback_(void *arg, STATUS status) { this->scan_result_.push_back(res); } this->scan_done_ = true; + global_wifi_component->wifi_scan_state_callback_.call(global_wifi_component->scan_result_); } #ifdef USE_WIFI_AP diff --git a/esphome/components/wifi/wifi_component_esp_idf.cpp b/esphome/components/wifi/wifi_component_esp_idf.cpp index 31ee712a48..25325f4428 100644 --- a/esphome/components/wifi/wifi_component_esp_idf.cpp +++ b/esphome/components/wifi/wifi_component_esp_idf.cpp @@ -713,6 +713,7 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) { ESP_LOGV(TAG, "Connected ssid='%s' bssid=" LOG_SECRET("%s") " channel=%u, authmode=%s", buf, format_mac_address_pretty(it.bssid).c_str(), it.channel, get_auth_mode_str(it.authmode)); s_sta_connected = true; + this->wifi_connect_state_callback_.call(this->wifi_ssid(), this->wifi_bssid()); } else if (data->event_base == WIFI_EVENT && data->event_id == WIFI_EVENT_STA_DISCONNECTED) { const auto &it = data->data.sta_disconnected; @@ -734,6 +735,7 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) { s_sta_connected = false; s_sta_connecting = false; error_from_callback_ = true; + this->wifi_connect_state_callback_.call("", bssid_t({0, 0, 0, 0, 0, 0})); } else if (data->event_base == IP_EVENT && data->event_id == IP_EVENT_STA_GOT_IP) { const auto &it = data->data.ip_got_ip; @@ -743,12 +745,14 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) { ESP_LOGV(TAG, "static_ip=%s gateway=%s", format_ip4_addr(it.ip_info.ip).c_str(), format_ip4_addr(it.ip_info.gw).c_str()); this->got_ipv4_address_ = true; + this->ip_state_callback_.call(this->wifi_sta_ip_addresses(), this->get_dns_address(0), this->get_dns_address(1)); #if USE_NETWORK_IPV6 } else if (data->event_base == IP_EVENT && data->event_id == IP_EVENT_GOT_IP6) { const auto &it = data->data.ip_got_ip6; ESP_LOGV(TAG, "IPv6 address=%s", format_ip6_addr(it.ip6_info.ip).c_str()); this->num_ipv6_addresses_++; + this->ip_state_callback_.call(this->wifi_sta_ip_addresses(), this->get_dns_address(0), this->get_dns_address(1)); #endif /* USE_NETWORK_IPV6 */ } else if (data->event_base == IP_EVENT && data->event_id == IP_EVENT_STA_LOST_IP) { @@ -789,6 +793,7 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) { WiFiScanResult result(bssid, ssid, record.primary, record.rssi, record.authmode != WIFI_AUTH_OPEN, ssid.empty()); scan_result_.push_back(result); } + this->wifi_scan_state_callback_.call(this->scan_result_); } else if (data->event_base == WIFI_EVENT && data->event_id == WIFI_EVENT_AP_START) { ESP_LOGV(TAG, "AP start"); diff --git a/esphome/components/wifi/wifi_component_libretiny.cpp b/esphome/components/wifi/wifi_component_libretiny.cpp index b15f710150..1b79c3f729 100644 --- a/esphome/components/wifi/wifi_component_libretiny.cpp +++ b/esphome/components/wifi/wifi_component_libretiny.cpp @@ -282,7 +282,7 @@ void WiFiComponent::wifi_event_callback_(esphome_wifi_event_id_t event, esphome_ buf[it.ssid_len] = '\0'; ESP_LOGV(TAG, "Connected ssid='%s' bssid=" LOG_SECRET("%s") " channel=%u, authmode=%s", buf, format_mac_address_pretty(it.bssid).c_str(), it.channel, get_auth_mode_str(it.authmode)); - + this->wifi_connect_state_callback_.call(this->wifi_ssid(), this->wifi_bssid()); break; } case ESPHOME_EVENT_ID_WIFI_STA_DISCONNECTED: { @@ -306,6 +306,7 @@ void WiFiComponent::wifi_event_callback_(esphome_wifi_event_id_t event, esphome_ } s_sta_connecting = false; + this->wifi_connect_state_callback_.call("", bssid_t({0, 0, 0, 0, 0, 0})); break; } case ESPHOME_EVENT_ID_WIFI_STA_AUTHMODE_CHANGE: { @@ -327,11 +328,13 @@ void WiFiComponent::wifi_event_callback_(esphome_wifi_event_id_t event, esphome_ ESP_LOGV(TAG, "static_ip=%s gateway=%s", format_ip4_addr(WiFi.localIP()).c_str(), format_ip4_addr(WiFi.gatewayIP()).c_str()); s_sta_connecting = false; + this->ip_state_callback_.call(this->wifi_sta_ip_addresses(), this->get_dns_address(0), this->get_dns_address(1)); break; } case ESPHOME_EVENT_ID_WIFI_STA_GOT_IP6: { // auto it = info.got_ip.ip_info; ESP_LOGV(TAG, "Got IPv6"); + this->ip_state_callback_.call(this->wifi_sta_ip_addresses(), this->get_dns_address(0), this->get_dns_address(1)); break; } case ESPHOME_EVENT_ID_WIFI_STA_LOST_IP: { @@ -425,6 +428,7 @@ void WiFiComponent::wifi_scan_done_callback_() { } WiFi.scanDelete(); this->scan_done_ = true; + this->wifi_scan_state_callback_.call(this->scan_result_); } #ifdef USE_WIFI_AP diff --git a/esphome/components/wifi/wifi_component_pico_w.cpp b/esphome/components/wifi/wifi_component_pico_w.cpp index bf15892cd5..c5c847be20 100644 --- a/esphome/components/wifi/wifi_component_pico_w.cpp +++ b/esphome/components/wifi/wifi_component_pico_w.cpp @@ -51,7 +51,7 @@ bool WiFiComponent::wifi_apply_power_save_() { return ret == 0; } -// TODO: The driver doesnt seem to have an API for this +// TODO: The driver doesn't seem to have an API for this bool WiFiComponent::wifi_apply_output_power_(float output_power) { return true; } bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) { @@ -210,6 +210,7 @@ void WiFiComponent::wifi_loop_() { if (this->state_ == WIFI_COMPONENT_STATE_STA_SCANNING && !cyw43_wifi_scan_active(&cyw43_state)) { this->scan_done_ = true; ESP_LOGV(TAG, "Scan done"); + this->wifi_scan_state_callback_.call(this->scan_result_); } } diff --git a/esphome/components/wifi_info/text_sensor.py b/esphome/components/wifi_info/text_sensor.py index 4ceb73a695..a91b1a971d 100644 --- a/esphome/components/wifi_info/text_sensor.py +++ b/esphome/components/wifi_info/text_sensor.py @@ -15,31 +15,27 @@ DEPENDENCIES = ["wifi"] wifi_info_ns = cg.esphome_ns.namespace("wifi_info") IPAddressWiFiInfo = wifi_info_ns.class_( - "IPAddressWiFiInfo", text_sensor.TextSensor, cg.PollingComponent + "IPAddressWiFiInfo", text_sensor.TextSensor, cg.Component ) ScanResultsWiFiInfo = wifi_info_ns.class_( - "ScanResultsWiFiInfo", text_sensor.TextSensor, cg.PollingComponent -) -SSIDWiFiInfo = wifi_info_ns.class_( - "SSIDWiFiInfo", text_sensor.TextSensor, cg.PollingComponent + "ScanResultsWiFiInfo", text_sensor.TextSensor, cg.Component ) +SSIDWiFiInfo = wifi_info_ns.class_("SSIDWiFiInfo", text_sensor.TextSensor, cg.Component) BSSIDWiFiInfo = wifi_info_ns.class_( - "BSSIDWiFiInfo", text_sensor.TextSensor, cg.PollingComponent + "BSSIDWiFiInfo", text_sensor.TextSensor, cg.Component ) MacAddressWifiInfo = wifi_info_ns.class_( "MacAddressWifiInfo", text_sensor.TextSensor, cg.Component ) DNSAddressWifiInfo = wifi_info_ns.class_( - "DNSAddressWifiInfo", text_sensor.TextSensor, cg.PollingComponent + "DNSAddressWifiInfo", text_sensor.TextSensor, cg.Component ) CONFIG_SCHEMA = cv.Schema( { cv.Optional(CONF_IP_ADDRESS): text_sensor.text_sensor_schema( IPAddressWiFiInfo, entity_category=ENTITY_CATEGORY_DIAGNOSTIC - ) - .extend(cv.polling_component_schema("1s")) - .extend( + ).extend( { cv.Optional(f"address_{x}"): text_sensor.text_sensor_schema( entity_category=ENTITY_CATEGORY_DIAGNOSTIC, @@ -49,19 +45,19 @@ CONFIG_SCHEMA = cv.Schema( ), cv.Optional(CONF_SCAN_RESULTS): text_sensor.text_sensor_schema( ScanResultsWiFiInfo, entity_category=ENTITY_CATEGORY_DIAGNOSTIC - ).extend(cv.polling_component_schema("60s")), + ), cv.Optional(CONF_SSID): text_sensor.text_sensor_schema( SSIDWiFiInfo, entity_category=ENTITY_CATEGORY_DIAGNOSTIC - ).extend(cv.polling_component_schema("1s")), + ), cv.Optional(CONF_BSSID): text_sensor.text_sensor_schema( BSSIDWiFiInfo, entity_category=ENTITY_CATEGORY_DIAGNOSTIC - ).extend(cv.polling_component_schema("1s")), + ), cv.Optional(CONF_MAC_ADDRESS): text_sensor.text_sensor_schema( MacAddressWifiInfo, entity_category=ENTITY_CATEGORY_DIAGNOSTIC ), cv.Optional(CONF_DNS_ADDRESS): text_sensor.text_sensor_schema( DNSAddressWifiInfo, entity_category=ENTITY_CATEGORY_DIAGNOSTIC - ).extend(cv.polling_component_schema("1s")), + ), } ) diff --git a/esphome/components/wifi_info/wifi_info_text_sensor.cpp b/esphome/components/wifi_info/wifi_info_text_sensor.cpp index 2612e4af8d..7a3e33b145 100644 --- a/esphome/components/wifi_info/wifi_info_text_sensor.cpp +++ b/esphome/components/wifi_info/wifi_info_text_sensor.cpp @@ -7,13 +7,113 @@ namespace wifi_info { static const char *const TAG = "wifi_info"; +/******************** + * IPAddressWiFiInfo + *******************/ + +void IPAddressWiFiInfo::setup() { + wifi::global_wifi_component->add_on_ip_state_callback( + [this](network::IPAddresses ips, network::IPAddress dns1_ip, network::IPAddress dns2_ip) { + this->state_callback_(ips); + }); +} + void IPAddressWiFiInfo::dump_config() { LOG_TEXT_SENSOR("", "IP Address", this); } -void ScanResultsWiFiInfo::dump_config() { LOG_TEXT_SENSOR("", "Scan Results", this); } -void SSIDWiFiInfo::dump_config() { LOG_TEXT_SENSOR("", "SSID", this); } -void BSSIDWiFiInfo::dump_config() { LOG_TEXT_SENSOR("", "BSSID", this); } -void MacAddressWifiInfo::dump_config() { LOG_TEXT_SENSOR("", "MAC Address", this); } + +void IPAddressWiFiInfo::state_callback_(network::IPAddresses ips) { + this->publish_state(ips[0].str()); + uint8_t sensor = 0; + for (auto &ip : ips) { + if (ip.is_set()) { + if (this->ip_sensors_[sensor] != nullptr) { + this->ip_sensors_[sensor]->publish_state(ip.str()); + } + sensor++; + } + } +} + +/********************* + * DNSAddressWifiInfo + ********************/ + +void DNSAddressWifiInfo::setup() { + wifi::global_wifi_component->add_on_ip_state_callback( + [this](network::IPAddresses ips, network::IPAddress dns1_ip, network::IPAddress dns2_ip) { + this->state_callback_(dns1_ip, dns2_ip); + }); +} + void DNSAddressWifiInfo::dump_config() { LOG_TEXT_SENSOR("", "DNS Address", this); } +void DNSAddressWifiInfo::state_callback_(network::IPAddress dns1_ip, network::IPAddress dns2_ip) { + std::string dns_results = dns1_ip.str() + " " + dns2_ip.str(); + this->publish_state(dns_results); +} + +/********************** + * ScanResultsWiFiInfo + *********************/ + +void ScanResultsWiFiInfo::setup() { + wifi::global_wifi_component->add_on_wifi_scan_state_callback( + [this](const std::vector &results) { this->state_callback_(results); }); +} + +void ScanResultsWiFiInfo::dump_config() { LOG_TEXT_SENSOR("", "Scan Results", this); } + +void ScanResultsWiFiInfo::state_callback_(const std::vector &results) { + std::string scan_results; + for (auto scan : results) { + if (scan.get_is_hidden()) + continue; + + scan_results += scan.get_ssid(); + scan_results += ": "; + scan_results += esphome::to_string(scan.get_rssi()); + scan_results += "dB\n"; + } + // There's a limit of 255 characters per state; longer states just don't get sent so we truncate it + this->publish_state(scan_results.substr(0, 255)); +} + +/*************** + * SSIDWiFiInfo + **************/ + +void SSIDWiFiInfo::setup() { + wifi::global_wifi_component->add_on_wifi_connect_state_callback( + [this](std::string ssid, wifi::bssid_t bssid) { this->state_callback_(ssid); }); +} + +void SSIDWiFiInfo::dump_config() { LOG_TEXT_SENSOR("", "SSID", this); } + +void SSIDWiFiInfo::state_callback_(std::string &ssid) { this->publish_state(ssid); } + +/**************** + * BSSIDWiFiInfo + ***************/ + +void BSSIDWiFiInfo::setup() { + wifi::global_wifi_component->add_on_wifi_connect_state_callback( + [this](std::string ssid, wifi::bssid_t bssid) { this->state_callback_(bssid); }); +} + +void BSSIDWiFiInfo::dump_config() { LOG_TEXT_SENSOR("", "BSSID", this); } + +void BSSIDWiFiInfo::state_callback_(wifi::bssid_t bssid) { + char buf[18] = "unknown"; + if (mac_address_is_valid(bssid.data())) { + format_mac_addr_upper(bssid.data(), buf); + } + this->publish_state(buf); +} +/********************* + * MacAddressWifiInfo + ********************/ + +void MacAddressWifiInfo::dump_config() { LOG_TEXT_SENSOR("", "MAC Address", this); } + } // namespace wifi_info } // namespace esphome #endif diff --git a/esphome/components/wifi_info/wifi_info_text_sensor.h b/esphome/components/wifi_info/wifi_info_text_sensor.h index 2cb96123a0..70d1e0dfc2 100644 --- a/esphome/components/wifi_info/wifi_info_text_sensor.h +++ b/esphome/components/wifi_info/wifi_info_text_sensor.h @@ -10,113 +10,51 @@ namespace esphome { namespace wifi_info { -class IPAddressWiFiInfo : public PollingComponent, public text_sensor::TextSensor { +class IPAddressWiFiInfo : public Component, public text_sensor::TextSensor { public: - void update() override { - auto ips = wifi::global_wifi_component->wifi_sta_ip_addresses(); - if (ips != this->last_ips_) { - this->last_ips_ = ips; - this->publish_state(ips[0].str()); - uint8_t sensor = 0; - for (auto &ip : ips) { - if (ip.is_set()) { - if (this->ip_sensors_[sensor] != nullptr) { - this->ip_sensors_[sensor]->publish_state(ip.str()); - } - sensor++; - } - } - } - } - float get_setup_priority() const override { return setup_priority::AFTER_WIFI; } + void setup() override; void dump_config() override; void add_ip_sensors(uint8_t index, text_sensor::TextSensor *s) { this->ip_sensors_[index] = s; } protected: - network::IPAddresses last_ips_; + void state_callback_(network::IPAddresses ips); std::array ip_sensors_; }; -class DNSAddressWifiInfo : public PollingComponent, public text_sensor::TextSensor { +class DNSAddressWifiInfo : public Component, public text_sensor::TextSensor { public: - void update() override { - auto dns_one = wifi::global_wifi_component->get_dns_address(0); - auto dns_two = wifi::global_wifi_component->get_dns_address(1); - - std::string dns_results = dns_one.str() + " " + dns_two.str(); - - if (dns_results != this->last_results_) { - this->last_results_ = dns_results; - this->publish_state(dns_results); - } - } - float get_setup_priority() const override { return setup_priority::AFTER_WIFI; } + void setup() override; void dump_config() override; protected: - std::string last_results_; + void state_callback_(network::IPAddress dns1_ip, network::IPAddress dns2_ip); }; -class ScanResultsWiFiInfo : public PollingComponent, public text_sensor::TextSensor { +class ScanResultsWiFiInfo : public Component, public text_sensor::TextSensor { public: - void update() override { - std::string scan_results; - for (auto &scan : wifi::global_wifi_component->get_scan_result()) { - if (scan.get_is_hidden()) - continue; - - scan_results += scan.get_ssid(); - scan_results += ": "; - scan_results += esphome::to_string(scan.get_rssi()); - scan_results += "dB\n"; - } - - if (this->last_scan_results_ != scan_results) { - this->last_scan_results_ = scan_results; - // There's a limit of 255 characters per state. - // Longer states just don't get sent so we truncate it. - this->publish_state(scan_results.substr(0, 255)); - } - } - float get_setup_priority() const override { return setup_priority::AFTER_WIFI; } + void setup() override; void dump_config() override; protected: - std::string last_scan_results_; + void state_callback_(const std::vector &results); }; -class SSIDWiFiInfo : public PollingComponent, public text_sensor::TextSensor { +class SSIDWiFiInfo : public Component, public text_sensor::TextSensor { public: - void update() override { - std::string ssid = wifi::global_wifi_component->wifi_ssid(); - if (this->last_ssid_ != ssid) { - this->last_ssid_ = ssid; - this->publish_state(this->last_ssid_); - } - } - float get_setup_priority() const override { return setup_priority::AFTER_WIFI; } + void setup() override; void dump_config() override; protected: - std::string last_ssid_; + void state_callback_(std::string &ssid); }; -class BSSIDWiFiInfo : public PollingComponent, public text_sensor::TextSensor { +class BSSIDWiFiInfo : public Component, public text_sensor::TextSensor { public: - void update() override { - wifi::bssid_t bssid = wifi::global_wifi_component->wifi_bssid(); - if (memcmp(bssid.data(), last_bssid_.data(), 6) != 0) { - std::copy(bssid.begin(), bssid.end(), last_bssid_.begin()); - char buf[18]; - format_mac_addr_upper(bssid.data(), buf); - this->publish_state(buf); - } - } - float get_setup_priority() const override { return setup_priority::AFTER_WIFI; } + void setup() override; void dump_config() override; protected: - wifi::bssid_t last_bssid_; + void state_callback_(wifi::bssid_t bssid); }; class MacAddressWifiInfo : public Component, public text_sensor::TextSensor { From 38719aaef88fef32f59070a15b4e1c5a85c16e0a Mon Sep 17 00:00:00 2001 From: kbx81 Date: Tue, 16 Sep 2025 17:31:30 -0500 Subject: [PATCH 002/415] tidy --- esphome/components/wifi_info/wifi_info_text_sensor.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/wifi_info/wifi_info_text_sensor.cpp b/esphome/components/wifi_info/wifi_info_text_sensor.cpp index 7a3e33b145..0f2f6ef162 100644 --- a/esphome/components/wifi_info/wifi_info_text_sensor.cpp +++ b/esphome/components/wifi_info/wifi_info_text_sensor.cpp @@ -64,7 +64,7 @@ void ScanResultsWiFiInfo::dump_config() { LOG_TEXT_SENSOR("", "Scan Results", th void ScanResultsWiFiInfo::state_callback_(const std::vector &results) { std::string scan_results; - for (auto scan : results) { + for (const auto &scan : results) { if (scan.get_is_hidden()) continue; @@ -96,7 +96,7 @@ void SSIDWiFiInfo::state_callback_(std::string &ssid) { this->publish_state(ssid void BSSIDWiFiInfo::setup() { wifi::global_wifi_component->add_on_wifi_connect_state_callback( - [this](std::string ssid, wifi::bssid_t bssid) { this->state_callback_(bssid); }); + [this](const std::string &ssid, wifi::bssid_t bssid) { this->state_callback_(bssid); }); } void BSSIDWiFiInfo::dump_config() { LOG_TEXT_SENSOR("", "BSSID", this); } From 27714e052cb6ec25926b841e53b7644bda4120f1 Mon Sep 17 00:00:00 2001 From: kbx81 Date: Tue, 21 Oct 2025 03:30:41 -0500 Subject: [PATCH 003/415] fix --- esphome/components/wifi/wifi_component.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index 7ef176dc4f..d33210e6ee 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -340,7 +340,7 @@ class WiFiComponent : public Component { this->ip_state_callback_.add(std::move(callback)); } /// - Wi-Fi scan results - void add_on_wifi_scan_state_callback(std::function)> &&callback) { + void add_on_wifi_scan_state_callback(std::function &)> &&callback) { this->wifi_scan_state_callback_.add(std::move(callback)); } /// - Wi-Fi SSID @@ -419,7 +419,7 @@ class WiFiComponent : public Component { WiFiAP ap_; optional output_power_; CallbackManager ip_state_callback_; - CallbackManager)> wifi_scan_state_callback_; + CallbackManager &)> wifi_scan_state_callback_; CallbackManager wifi_connect_state_callback_; ESPPreferenceObject pref_; #ifdef USE_WIFI_FAST_CONNECT From ddef1f9ecd5a852807f0ed8db1ae952ea3971b7a Mon Sep 17 00:00:00 2001 From: kbx81 Date: Tue, 21 Oct 2025 03:55:22 -0500 Subject: [PATCH 004/415] fix --- esphome/components/wifi_info/wifi_info_text_sensor.cpp | 4 ++-- esphome/components/wifi_info/wifi_info_text_sensor.h | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/esphome/components/wifi_info/wifi_info_text_sensor.cpp b/esphome/components/wifi_info/wifi_info_text_sensor.cpp index 0f2f6ef162..a1d15cfead 100644 --- a/esphome/components/wifi_info/wifi_info_text_sensor.cpp +++ b/esphome/components/wifi_info/wifi_info_text_sensor.cpp @@ -57,12 +57,12 @@ void DNSAddressWifiInfo::state_callback_(network::IPAddress dns1_ip, network::IP void ScanResultsWiFiInfo::setup() { wifi::global_wifi_component->add_on_wifi_scan_state_callback( - [this](const std::vector &results) { this->state_callback_(results); }); + [this](const wifi::wifi_scan_vector_t &results) { this->state_callback_(results); }); } void ScanResultsWiFiInfo::dump_config() { LOG_TEXT_SENSOR("", "Scan Results", this); } -void ScanResultsWiFiInfo::state_callback_(const std::vector &results) { +void ScanResultsWiFiInfo::state_callback_(const wifi::wifi_scan_vector_t &results) { std::string scan_results; for (const auto &scan : results) { if (scan.get_is_hidden()) diff --git a/esphome/components/wifi_info/wifi_info_text_sensor.h b/esphome/components/wifi_info/wifi_info_text_sensor.h index 70d1e0dfc2..6178c3b3e3 100644 --- a/esphome/components/wifi_info/wifi_info_text_sensor.h +++ b/esphome/components/wifi_info/wifi_info_text_sensor.h @@ -36,7 +36,7 @@ class ScanResultsWiFiInfo : public Component, public text_sensor::TextSensor { void dump_config() override; protected: - void state_callback_(const std::vector &results); + void state_callback_(const wifi::wifi_scan_vector_t &results); }; class SSIDWiFiInfo : public Component, public text_sensor::TextSensor { From e9ff4d3c4ec8242216001da42ffc71d31d0b182e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 15 Nov 2025 11:47:35 -0600 Subject: [PATCH 005/415] handle static --- esphome/cpp_generator.py | 46 ++++++++++++++-- tests/unit_tests/test_lambda_dedup.py | 75 +++++++++++++++++++++++++++ 2 files changed, 117 insertions(+), 4 deletions(-) diff --git a/esphome/cpp_generator.py b/esphome/cpp_generator.py index 2904cae796..4f91696ca1 100644 --- a/esphome/cpp_generator.py +++ b/esphome/cpp_generator.py @@ -29,6 +29,11 @@ from esphome.yaml_util import ESPHomeDataBase _KEY_LAMBDA_DEDUP = "lambda_dedup" _KEY_LAMBDA_DEDUP_DECLARATIONS = "lambda_dedup_declarations" +# Regex patterns for static variable detection (compiled once) +_RE_CPP_SINGLE_LINE_COMMENT = re.compile(r"//.*?$", re.MULTILINE) +_RE_CPP_MULTI_LINE_COMMENT = re.compile(r"/\*.*?\*/", re.DOTALL) +_RE_STATIC_VARIABLE = re.compile(r"\bstatic\s+(?!cast|assert|pointer_cast)\w+\s+\w+") + class RawExpression(Expression): __slots__ = ("text",) @@ -716,20 +721,51 @@ async def get_variable_with_full_id(id_: ID) -> tuple[ID, "MockObj"]: return await CORE.get_variable_with_full_id(id_) -def _get_shared_lambda_name(lambda_expr: LambdaExpression) -> str: +def _has_static_variables(code: str) -> bool: + """Check if code contains static variable definitions. + + Static variables in lambdas should not be deduplicated because each lambda + instance should have its own static variable state. + + Args: + code: The lambda body code to check + + Returns: + True if code contains static variable definitions + """ + # Remove C++ comments to avoid false positives + # Remove single-line comments (// ...) + code_no_comments = _RE_CPP_SINGLE_LINE_COMMENT.sub("", code) + # Remove multi-line comments (/* ... */) + code_no_comments = _RE_CPP_MULTI_LINE_COMMENT.sub("", code_no_comments) + + # Match: static + # But not: static_cast, static_assert, static_pointer_cast + return bool(_RE_STATIC_VARIABLE.search(code_no_comments)) + + +def _get_shared_lambda_name(lambda_expr: LambdaExpression) -> str | None: """Get the shared function name for a lambda expression. If an identical lambda was already generated, returns the existing shared function name. Otherwise, creates a new shared function and returns its name. + Lambdas with static variables are not deduplicated to preserve their + independent state. + Args: lambda_expr: The lambda expression to deduplicate Returns: - The name of the shared function for this lambda (either existing or newly created) + The name of the shared function for this lambda (either existing or newly created), + or None if the lambda should not be deduplicated (e.g., contains static variables) """ # Create a unique key from the lambda content, parameters, and return type content = lambda_expr.content + + # Don't deduplicate lambdas with static variables - each instance needs its own state + if _has_static_variables(content): + return None param_str = str(lambda_expr.parameters) return_str = ( str(lambda_expr.return_type) if lambda_expr.return_type is not None else "void" @@ -832,13 +868,15 @@ async def process_lambda( # Lambda deduplication: Only deduplicate stateless lambdas (empty capture). # Stateful lambdas cannot be shared as they capture different contexts. + # Lambdas with static variables are also not deduplicated to preserve independent state. if capture == "": lambda_expr = LambdaExpression( parts, parameters, capture, return_type, location ) func_name = _get_shared_lambda_name(lambda_expr) - # Return a shared function reference instead of inline lambda - return SharedFunctionLambdaExpression(func_name, parameters, return_type) + if func_name is not None: + # Return a shared function reference instead of inline lambda + return SharedFunctionLambdaExpression(func_name, parameters, return_type) return LambdaExpression(parts, parameters, capture, return_type, location) diff --git a/tests/unit_tests/test_lambda_dedup.py b/tests/unit_tests/test_lambda_dedup.py index ebc217f308..a0691e29dc 100644 --- a/tests/unit_tests/test_lambda_dedup.py +++ b/tests/unit_tests/test_lambda_dedup.py @@ -192,3 +192,78 @@ def test_stateful_lambdas_not_deduplicated() -> None: # We verify the lambda has a non-empty capture assert stateful_lambda.capture != "" assert stateful_lambda.capture == "=" + + +def test_static_variable_detection() -> None: + """Test detection of static variables in lambda code.""" + # Should detect static variables + assert cg._has_static_variables("static int counter = 0;") + assert cg._has_static_variables("static bool flag = false; return flag;") + assert cg._has_static_variables(" static float value = 1.0; ") + + # Should NOT detect static_cast, static_assert, etc. + assert not cg._has_static_variables("return static_cast(value);") + assert not cg._has_static_variables("static_assert(sizeof(int) == 4);") + assert not cg._has_static_variables("auto ptr = static_pointer_cast(bar);") + + # Should NOT detect in comments + assert not cg._has_static_variables("// static int x = 0;\nreturn 42;") + assert not cg._has_static_variables("/* static int y = 0; */ return 42;") + + # Should detect even with comments elsewhere + assert cg._has_static_variables("// comment\nstatic int x = 0;\nreturn x;") + + # Should NOT detect non-static code + assert not cg._has_static_variables("int counter = 0; return counter++;") + assert not cg._has_static_variables("return 42;") + + +def test_lambdas_with_static_not_deduplicated() -> None: + """Test that lambdas with static variables are not deduplicated.""" + # Two identical lambdas with static variables + lambda1 = cg.LambdaExpression( + parts=["static int counter = 0; return counter++;"], + parameters=[], + capture="", + return_type=cg.RawExpression("int"), + ) + + lambda2 = cg.LambdaExpression( + parts=["static int counter = 0; return counter++;"], + parameters=[], + capture="", + return_type=cg.RawExpression("int"), + ) + + # Should return None (not deduplicated) + func_name1 = cg._get_shared_lambda_name(lambda1) + func_name2 = cg._get_shared_lambda_name(lambda2) + + assert func_name1 is None + assert func_name2 is None + + +def test_lambdas_without_static_still_deduplicated() -> None: + """Test that lambdas without static variables are still deduplicated.""" + # Two identical lambdas WITHOUT static variables + lambda1 = cg.LambdaExpression( + parts=["int counter = 0; return counter++;"], # No static + parameters=[], + capture="", + return_type=cg.RawExpression("int"), + ) + + lambda2 = cg.LambdaExpression( + parts=["int counter = 0; return counter++;"], # No static + parameters=[], + capture="", + return_type=cg.RawExpression("int"), + ) + + # Should be deduplicated (same function name) + func_name1 = cg._get_shared_lambda_name(lambda1) + func_name2 = cg._get_shared_lambda_name(lambda2) + + assert func_name1 is not None + assert func_name2 is not None + assert func_name1 == func_name2 From ae343a94ca68cd4ff25114a5279dc3995ff7e15d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 15 Nov 2025 12:11:50 -0600 Subject: [PATCH 006/415] disable around old code --- esphome/core/automation.h | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/esphome/core/automation.h b/esphome/core/automation.h index 97c9e9c3cc..dacadd35e8 100644 --- a/esphome/core/automation.h +++ b/esphome/core/automation.h @@ -15,23 +15,22 @@ namespace esphome { // Legacy seq<>/gens<> pattern deprecated but kept for backwards compatibility // https://stackoverflow.com/questions/7858817/unpacking-a-tuple-to-call-a-matching-function-pointer/7858971#7858971 // Remove before 2026.6.0 +// NOLINTBEGIN(readability-identifier-naming) #if defined(__GNUC__) || defined(__clang__) #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wdeprecated-declarations" #endif -// NOLINTNEXTLINE(readability-identifier-naming) template struct ESPDEPRECATED("Use std::index_sequence instead. Removed in 2026.6.0", "2025.12.0") seq {}; -// NOLINTNEXTLINE(readability-identifier-naming) template struct ESPDEPRECATED("Use std::make_index_sequence instead. Removed in 2026.6.0", "2025.12.0") gens : gens {}; -// NOLINTNEXTLINE(readability-identifier-naming) template struct gens<0, S...> { using type = seq; }; #if defined(__GNUC__) || defined(__clang__) #pragma GCC diagnostic pop #endif +// NOLINTEND(readability-identifier-naming) #define TEMPLATABLE_VALUE_(type, name) \ protected: \ From f6378990cd3295d4e7bd3dbf01f18754846c4402 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 15 Nov 2025 12:19:06 -0600 Subject: [PATCH 007/415] add tests for crazy edge cases --- tests/unit_tests/test_lambda_dedup.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/tests/unit_tests/test_lambda_dedup.py b/tests/unit_tests/test_lambda_dedup.py index a0691e29dc..c112631f2d 100644 --- a/tests/unit_tests/test_lambda_dedup.py +++ b/tests/unit_tests/test_lambda_dedup.py @@ -201,11 +201,21 @@ def test_static_variable_detection() -> None: assert cg._has_static_variables("static bool flag = false; return flag;") assert cg._has_static_variables(" static float value = 1.0; ") - # Should NOT detect static_cast, static_assert, etc. + # Should NOT detect static_cast, static_assert, etc. (with underscores) assert not cg._has_static_variables("return static_cast(value);") assert not cg._has_static_variables("static_assert(sizeof(int) == 4);") assert not cg._has_static_variables("auto ptr = static_pointer_cast(bar);") + # Edge case: 'cast', 'assert', 'pointer_cast' are NOT C++ keywords + # Someone could use them as type names, but we should NOT flag them + # because they're not actually static variables with state + # NOTE: These are valid C++ but extremely unlikely in ESPHome lambdas + assert not cg._has_static_variables("static cast obj;") # 'cast' as type name + assert not cg._has_static_variables("static assert value;") # 'assert' as type name + assert not cg._has_static_variables( + "static pointer_cast ptr;" + ) # 'pointer_cast' as type + # Should NOT detect in comments assert not cg._has_static_variables("// static int x = 0;\nreturn 42;") assert not cg._has_static_variables("/* static int y = 0; */ return 42;") From 894ba341ba23f64b7e7e87b0982824c0651e398d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 15 Nov 2025 12:56:25 -0600 Subject: [PATCH 008/415] [sensor] Replace timeout filter scheduler with loop-based implementation --- esphome/components/sensor/__init__.py | 8 +++++ esphome/components/sensor/filter.cpp | 43 ++++++++++++++++++++------ esphome/components/sensor/filter.h | 44 ++++++++++++++++++++++----- 3 files changed, 77 insertions(+), 18 deletions(-) diff --git a/esphome/components/sensor/__init__.py b/esphome/components/sensor/__init__.py index e8fec222a1..f7d7513b7b 100644 --- a/esphome/components/sensor/__init__.py +++ b/esphome/components/sensor/__init__.py @@ -271,6 +271,9 @@ ThrottleWithPriorityFilter = sensor_ns.class_( "ThrottleWithPriorityFilter", ValueListFilter ) TimeoutFilter = sensor_ns.class_("TimeoutFilter", Filter, cg.Component) +TimeoutFilterConfigured = sensor_ns.class_( + "TimeoutFilterConfigured", Filter, cg.Component +) DebounceFilter = sensor_ns.class_("DebounceFilter", Filter, cg.Component) HeartbeatFilter = sensor_ns.class_("HeartbeatFilter", Filter, cg.Component) DeltaFilter = sensor_ns.class_("DeltaFilter", Filter) @@ -684,8 +687,13 @@ TIMEOUT_SCHEMA = cv.maybe_simple_value( @FILTER_REGISTRY.register("timeout", TimeoutFilter, TIMEOUT_SCHEMA) async def timeout_filter_to_code(config, filter_id): if config[CONF_VALUE] == "last": + # Use TimeoutFilter for "last" mode (smaller, more common - LD2450, LD2412, etc.) var = cg.new_Pvariable(filter_id, config[CONF_TIMEOUT]) else: + # Use TimeoutFilterConfigured for configured value mode + # Change the type to TimeoutFilterConfigured (similar to stateless lambda pattern) + filter_id = filter_id.copy() + filter_id.type = TimeoutFilterConfigured template_ = await cg.templatable(config[CONF_VALUE], [], float) var = cg.new_Pvariable(filter_id, config[CONF_TIMEOUT], template_) await cg.register_component(var, {}) diff --git a/esphome/components/sensor/filter.cpp b/esphome/components/sensor/filter.cpp index 65d8dea31c..6825dfebd0 100644 --- a/esphome/components/sensor/filter.cpp +++ b/esphome/components/sensor/filter.cpp @@ -339,20 +339,43 @@ void OrFilter::initialize(Sensor *parent, Filter *next) { this->phi_.initialize(parent, nullptr); } -// TimeoutFilter -optional TimeoutFilter::new_value(float value) { - if (this->value_.has_value()) { - this->set_timeout("timeout", this->time_period_, [this]() { this->output(this->value_.value().value()); }); - } else { - this->set_timeout("timeout", this->time_period_, [this, value]() { this->output(value); }); +// TimeoutFilterBase - shared loop logic +void TimeoutFilterBase::loop() { + // Check if timeout period has elapsed + // Use cached loop start time to avoid repeated millis() calls + const uint32_t now = App.get_loop_component_start_time(); + if (now - this->timeout_start_time_ >= this->time_period_) { + // Timeout fired - get output value from derived class and output it + this->output(this->get_output_value()); + + // Disable loop until next value arrives + this->disable_loop(); } +} + +float TimeoutFilterBase::get_setup_priority() const { return setup_priority::HARDWARE; } + +// TimeoutFilter - "last" mode implementation +optional TimeoutFilter::new_value(float value) { + // Store the value to output when timeout fires + this->pending_value_ = value; + + // Record when timeout started and enable loop + this->timeout_start_time_ = millis(); + this->enable_loop(); + return value; } -TimeoutFilter::TimeoutFilter(uint32_t time_period) : time_period_(time_period) {} -TimeoutFilter::TimeoutFilter(uint32_t time_period, const TemplatableValue &new_value) - : time_period_(time_period), value_(new_value) {} -float TimeoutFilter::get_setup_priority() const { return setup_priority::HARDWARE; } +// TimeoutFilterConfigured - configured value mode implementation +optional TimeoutFilterConfigured::new_value(float value) { + // Record when timeout started and enable loop + // Note: we don't store the incoming value since we have a configured value + this->timeout_start_time_ = millis(); + this->enable_loop(); + + return value; +} // DebounceFilter optional DebounceFilter::new_value(float value) { diff --git a/esphome/components/sensor/filter.h b/esphome/components/sensor/filter.h index 75e28a1efe..102ae2fea6 100644 --- a/esphome/components/sensor/filter.h +++ b/esphome/components/sensor/filter.h @@ -380,18 +380,46 @@ class ThrottleWithPriorityFilter : public ValueListFilter { uint32_t min_time_between_inputs_; }; -class TimeoutFilter : public Filter, public Component { +// Base class for timeout filters - contains common loop logic +class TimeoutFilterBase : public Filter, public Component { public: - explicit TimeoutFilter(uint32_t time_period); - explicit TimeoutFilter(uint32_t time_period, const TemplatableValue &new_value); - - optional new_value(float value) override; - + void loop() override; float get_setup_priority() const override; protected: - uint32_t time_period_; - optional> value_; + explicit TimeoutFilterBase(uint32_t time_period) : time_period_(time_period) { this->disable_loop(); } + virtual float get_output_value() = 0; + + uint32_t time_period_; // 4 bytes (timeout duration in ms) + uint32_t timeout_start_time_{0}; // 4 bytes (when the timeout was started) + // Total base: 8 bytes +}; + +// Timeout filter for "last" mode - outputs the last received value after timeout +class TimeoutFilter : public TimeoutFilterBase { + public: + explicit TimeoutFilter(uint32_t time_period) : TimeoutFilterBase(time_period) {} + + optional new_value(float value) override; + + protected: + float get_output_value() override { return this->pending_value_; } + float pending_value_{0}; // 4 bytes (value to output when timeout fires) + // Total: 8 (base) + 4 = 12 bytes + vtable ptr + Component overhead +}; + +// Timeout filter with configured value - evaluates TemplatableValue after timeout +class TimeoutFilterConfigured : public TimeoutFilterBase { + public: + explicit TimeoutFilterConfigured(uint32_t time_period, const TemplatableValue &new_value) + : TimeoutFilterBase(time_period), value_(new_value) {} + + optional new_value(float value) override; + + protected: + float get_output_value() override { return this->value_.value(); } + TemplatableValue value_; // 16 bytes (configured output value, can be lambda) + // Total: 8 (base) + 16 = 24 bytes + vtable ptr + Component overhead }; class DebounceFilter : public Filter, public Component { From 6cca3617d83402d3a59499c6a92fbaba201eeb41 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 15 Nov 2025 13:06:06 -0600 Subject: [PATCH 009/415] cover --- .../fixtures/sensor_timeout_filter.yaml | 150 +++++++++++++ .../integration/test_sensor_timeout_filter.py | 200 ++++++++++++++++++ 2 files changed, 350 insertions(+) create mode 100644 tests/integration/fixtures/sensor_timeout_filter.yaml create mode 100644 tests/integration/test_sensor_timeout_filter.py diff --git a/tests/integration/fixtures/sensor_timeout_filter.yaml b/tests/integration/fixtures/sensor_timeout_filter.yaml new file mode 100644 index 0000000000..0d127140f9 --- /dev/null +++ b/tests/integration/fixtures/sensor_timeout_filter.yaml @@ -0,0 +1,150 @@ +esphome: + name: test-timeout-filters + +host: +api: + batch_delay: 0ms # Disable batching to receive all state updates +logger: + level: DEBUG + +# Template sensors that we'll use to publish values +sensor: + - platform: template + name: "Source Timeout Last" + id: source_timeout_last + accuracy_decimals: 1 + + - platform: template + name: "Source Timeout Reset" + id: source_timeout_reset + accuracy_decimals: 1 + + - platform: template + name: "Source Timeout Static" + id: source_timeout_static + accuracy_decimals: 1 + + - platform: template + name: "Source Timeout Lambda" + id: source_timeout_lambda + accuracy_decimals: 1 + + # Test 1: TimeoutFilter - "last" mode (outputs last received value) + - platform: copy + source_id: source_timeout_last + name: "Timeout Last Sensor" + id: timeout_last_sensor + filters: + - timeout: + timeout: 200ms + value: last # Explicitly specify "last" mode to use TimeoutFilter class + + # Test 2: TimeoutFilter - reset behavior (same filter, different source) + - platform: copy + source_id: source_timeout_reset + name: "Timeout Reset Sensor" + id: timeout_reset_sensor + filters: + - timeout: + timeout: 200ms + value: last # Explicitly specify "last" mode + + # Test 3: TimeoutFilterConfigured - static value mode + - platform: copy + source_id: source_timeout_static + name: "Timeout Static Sensor" + id: timeout_static_sensor + filters: + - timeout: + timeout: 200ms + value: 99.9 + + # Test 4: TimeoutFilterConfigured - lambda mode + - platform: copy + source_id: source_timeout_lambda + name: "Timeout Lambda Sensor" + id: timeout_lambda_sensor + filters: + - timeout: + timeout: 200ms + value: !lambda "return -1.0;" + +# Scripts to publish values with controlled timing +script: + # Test 1: Single value followed by timeout + - id: test_timeout_last_script + then: + # Publish initial value + - sensor.template.publish: + id: source_timeout_last + state: 42.0 + # Wait for timeout to fire (200ms + margin) + - delay: 300ms + + # Test 2: Multiple values before timeout (should reset timer) + - id: test_timeout_reset_script + then: + # Publish first value + - sensor.template.publish: + id: source_timeout_reset + state: 10.0 + # Wait 100ms (halfway to timeout) + - delay: 100ms + # Publish second value (resets timeout) + - sensor.template.publish: + id: source_timeout_reset + state: 20.0 + # Wait 100ms (halfway to timeout again) + - delay: 100ms + # Publish third value (resets timeout) + - sensor.template.publish: + id: source_timeout_reset + state: 30.0 + # Wait for timeout to fire (200ms + margin) + - delay: 300ms + + # Test 3: Static value timeout + - id: test_timeout_static_script + then: + # Publish initial value + - sensor.template.publish: + id: source_timeout_static + state: 55.5 + # Wait for timeout to fire + - delay: 300ms + + # Test 4: Lambda value timeout + - id: test_timeout_lambda_script + then: + # Publish initial value + - sensor.template.publish: + id: source_timeout_lambda + state: 77.7 + # Wait for timeout to fire + - delay: 300ms + +# Buttons to trigger each test scenario +button: + - platform: template + name: "Test Timeout Last Button" + id: test_timeout_last_button + on_press: + - script.execute: test_timeout_last_script + + - platform: template + name: "Test Timeout Reset Button" + id: test_timeout_reset_button + on_press: + - script.execute: test_timeout_reset_script + + - platform: template + name: "Test Timeout Static Button" + id: test_timeout_static_button + on_press: + - script.execute: test_timeout_static_script + + - platform: template + name: "Test Timeout Lambda Button" + id: test_timeout_lambda_button + on_press: + - script.execute: test_timeout_lambda_script diff --git a/tests/integration/test_sensor_timeout_filter.py b/tests/integration/test_sensor_timeout_filter.py new file mode 100644 index 0000000000..6a2fef0c57 --- /dev/null +++ b/tests/integration/test_sensor_timeout_filter.py @@ -0,0 +1,200 @@ +"""Test sensor timeout filter functionality.""" + +from __future__ import annotations + +import asyncio + +from aioesphomeapi import EntityState, SensorState +import pytest + +from .state_utils import InitialStateHelper, build_key_to_entity_mapping +from .types import APIClientConnectedFactory, RunCompiledFunction + + +@pytest.mark.asyncio +async def test_sensor_timeout_filter( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """Test TimeoutFilter and TimeoutFilterConfigured with all modes.""" + loop = asyncio.get_running_loop() + + # Track state changes for all sensors + timeout_last_states: list[float] = [] + timeout_reset_states: list[float] = [] + timeout_static_states: list[float] = [] + timeout_lambda_states: list[float] = [] + + # Futures for each test scenario + test1_complete = loop.create_future() # TimeoutFilter - last mode + test2_complete = loop.create_future() # TimeoutFilter - reset behavior + test3_complete = loop.create_future() # TimeoutFilterConfigured - static value + test4_complete = loop.create_future() # TimeoutFilterConfigured - lambda + + def on_state(state: EntityState) -> None: + """Track sensor state updates.""" + if not isinstance(state, SensorState): + return + + if state.missing_state: + return + + sensor_name = key_to_sensor.get(state.key) + + # Test 1: TimeoutFilter - last mode + if sensor_name == "timeout_last_sensor": + timeout_last_states.append(state.state) + # Expect 2 values: initial 42.0 + timeout fires with 42.0 + if len(timeout_last_states) >= 2 and not test1_complete.done(): + test1_complete.set_result(True) + + # Test 2: TimeoutFilter - reset behavior + elif sensor_name == "timeout_reset_sensor": + timeout_reset_states.append(state.state) + # Expect 4 values: 10.0, 20.0, 30.0, then timeout fires with 30.0 + if len(timeout_reset_states) >= 4 and not test2_complete.done(): + test2_complete.set_result(True) + + # Test 3: TimeoutFilterConfigured - static value + elif sensor_name == "timeout_static_sensor": + timeout_static_states.append(state.state) + # Expect 2 values: initial 55.5 + timeout fires with 99.9 + if len(timeout_static_states) >= 2 and not test3_complete.done(): + test3_complete.set_result(True) + + # Test 4: TimeoutFilterConfigured - lambda + elif sensor_name == "timeout_lambda_sensor": + timeout_lambda_states.append(state.state) + # Expect 2 values: initial 77.7 + timeout fires with -1.0 + if len(timeout_lambda_states) >= 2 and not test4_complete.done(): + test4_complete.set_result(True) + + async with ( + run_compiled(yaml_config), + api_client_connected() as client, + ): + entities, services = await client.list_entities_services() + + key_to_sensor = build_key_to_entity_mapping( + entities, + [ + "timeout_last_sensor", + "timeout_reset_sensor", + "timeout_static_sensor", + "timeout_lambda_sensor", + ], + ) + + initial_state_helper = InitialStateHelper(entities) + client.subscribe_states(initial_state_helper.on_state_wrapper(on_state)) + + try: + await initial_state_helper.wait_for_initial_states() + except TimeoutError: + pytest.fail("Timeout waiting for initial states") + + # Find all test buttons + test1_button = next( + (e for e in entities if "test_timeout_last_button" in e.object_id.lower()), + None, + ) + test2_button = next( + (e for e in entities if "test_timeout_reset_button" in e.object_id.lower()), + None, + ) + test3_button = next( + ( + e + for e in entities + if "test_timeout_static_button" in e.object_id.lower() + ), + None, + ) + test4_button = next( + ( + e + for e in entities + if "test_timeout_lambda_button" in e.object_id.lower() + ), + None, + ) + + assert test1_button is not None, "Test Timeout Last Button not found" + assert test2_button is not None, "Test Timeout Reset Button not found" + assert test3_button is not None, "Test Timeout Static Button not found" + assert test4_button is not None, "Test Timeout Lambda Button not found" + + # === Test 1: TimeoutFilter - last mode === + client.button_command(test1_button.key) + try: + await asyncio.wait_for(test1_complete, timeout=2.0) + except TimeoutError: + pytest.fail(f"Test 1 timeout. Received states: {timeout_last_states}") + + assert len(timeout_last_states) == 2, ( + f"Test 1: Should have 2 states, got {len(timeout_last_states)}: {timeout_last_states}" + ) + assert timeout_last_states[0] == pytest.approx(42.0), ( + f"Test 1: First state should be 42.0, got {timeout_last_states[0]}" + ) + assert timeout_last_states[1] == pytest.approx(42.0), ( + f"Test 1: Timeout should output last value (42.0), got {timeout_last_states[1]}" + ) + + # === Test 2: TimeoutFilter - reset behavior === + client.button_command(test2_button.key) + try: + await asyncio.wait_for(test2_complete, timeout=2.0) + except TimeoutError: + pytest.fail(f"Test 2 timeout. Received states: {timeout_reset_states}") + + assert len(timeout_reset_states) == 4, ( + f"Test 2: Should have 4 states, got {len(timeout_reset_states)}: {timeout_reset_states}" + ) + assert timeout_reset_states[0] == pytest.approx(10.0), ( + f"Test 2: First state should be 10.0, got {timeout_reset_states[0]}" + ) + assert timeout_reset_states[1] == pytest.approx(20.0), ( + f"Test 2: Second state should be 20.0, got {timeout_reset_states[1]}" + ) + assert timeout_reset_states[2] == pytest.approx(30.0), ( + f"Test 2: Third state should be 30.0, got {timeout_reset_states[2]}" + ) + assert timeout_reset_states[3] == pytest.approx(30.0), ( + f"Test 2: Timeout should output last value (30.0), got {timeout_reset_states[3]}" + ) + + # === Test 3: TimeoutFilterConfigured - static value === + client.button_command(test3_button.key) + try: + await asyncio.wait_for(test3_complete, timeout=2.0) + except TimeoutError: + pytest.fail(f"Test 3 timeout. Received states: {timeout_static_states}") + + assert len(timeout_static_states) == 2, ( + f"Test 3: Should have 2 states, got {len(timeout_static_states)}: {timeout_static_states}" + ) + assert timeout_static_states[0] == pytest.approx(55.5), ( + f"Test 3: First state should be 55.5, got {timeout_static_states[0]}" + ) + assert timeout_static_states[1] == pytest.approx(99.9), ( + f"Test 3: Timeout should output configured value (99.9), got {timeout_static_states[1]}" + ) + + # === Test 4: TimeoutFilterConfigured - lambda === + client.button_command(test4_button.key) + try: + await asyncio.wait_for(test4_complete, timeout=2.0) + except TimeoutError: + pytest.fail(f"Test 4 timeout. Received states: {timeout_lambda_states}") + + assert len(timeout_lambda_states) == 2, ( + f"Test 4: Should have 2 states, got {len(timeout_lambda_states)}: {timeout_lambda_states}" + ) + assert timeout_lambda_states[0] == pytest.approx(77.7), ( + f"Test 4: First state should be 77.7, got {timeout_lambda_states[0]}" + ) + assert timeout_lambda_states[1] == pytest.approx(-1.0), ( + f"Test 4: Timeout should evaluate lambda (-1.0), got {timeout_lambda_states[1]}" + ) From 6f5f45f1e901d977019a0c099156eed0064ecb25 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 15 Nov 2025 13:06:54 -0600 Subject: [PATCH 010/415] cover --- .../fixtures/sensor_timeout_filter.yaml | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/tests/integration/fixtures/sensor_timeout_filter.yaml b/tests/integration/fixtures/sensor_timeout_filter.yaml index 0d127140f9..dbd4db3242 100644 --- a/tests/integration/fixtures/sensor_timeout_filter.yaml +++ b/tests/integration/fixtures/sensor_timeout_filter.yaml @@ -36,7 +36,7 @@ sensor: id: timeout_last_sensor filters: - timeout: - timeout: 200ms + timeout: 100ms value: last # Explicitly specify "last" mode to use TimeoutFilter class # Test 2: TimeoutFilter - reset behavior (same filter, different source) @@ -46,7 +46,7 @@ sensor: id: timeout_reset_sensor filters: - timeout: - timeout: 200ms + timeout: 100ms value: last # Explicitly specify "last" mode # Test 3: TimeoutFilterConfigured - static value mode @@ -56,7 +56,7 @@ sensor: id: timeout_static_sensor filters: - timeout: - timeout: 200ms + timeout: 100ms value: 99.9 # Test 4: TimeoutFilterConfigured - lambda mode @@ -66,7 +66,7 @@ sensor: id: timeout_lambda_sensor filters: - timeout: - timeout: 200ms + timeout: 100ms value: !lambda "return -1.0;" # Scripts to publish values with controlled timing @@ -78,8 +78,8 @@ script: - sensor.template.publish: id: source_timeout_last state: 42.0 - # Wait for timeout to fire (200ms + margin) - - delay: 300ms + # Wait for timeout to fire (100ms + margin) + - delay: 150ms # Test 2: Multiple values before timeout (should reset timer) - id: test_timeout_reset_script @@ -88,20 +88,20 @@ script: - sensor.template.publish: id: source_timeout_reset state: 10.0 - # Wait 100ms (halfway to timeout) - - delay: 100ms + # Wait 50ms (halfway to timeout) + - delay: 50ms # Publish second value (resets timeout) - sensor.template.publish: id: source_timeout_reset state: 20.0 - # Wait 100ms (halfway to timeout again) - - delay: 100ms + # Wait 50ms (halfway to timeout again) + - delay: 50ms # Publish third value (resets timeout) - sensor.template.publish: id: source_timeout_reset state: 30.0 - # Wait for timeout to fire (200ms + margin) - - delay: 300ms + # Wait for timeout to fire (100ms + margin) + - delay: 150ms # Test 3: Static value timeout - id: test_timeout_static_script @@ -111,7 +111,7 @@ script: id: source_timeout_static state: 55.5 # Wait for timeout to fire - - delay: 300ms + - delay: 150ms # Test 4: Lambda value timeout - id: test_timeout_lambda_script @@ -121,7 +121,7 @@ script: id: source_timeout_lambda state: 77.7 # Wait for timeout to fire - - delay: 300ms + - delay: 150ms # Buttons to trigger each test scenario button: From aca74e34b832f72deeb15d2cf0b4810e18cc9b6a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 15 Nov 2025 13:07:56 -0600 Subject: [PATCH 011/415] Add tests for sensor timeout filters ahead of optimization effort in https://github.com/esphome/esphome/pull/11922 --- .../fixtures/sensor_timeout_filter.yaml | 150 +++++++++++++ .../integration/test_sensor_timeout_filter.py | 200 ++++++++++++++++++ 2 files changed, 350 insertions(+) create mode 100644 tests/integration/fixtures/sensor_timeout_filter.yaml create mode 100644 tests/integration/test_sensor_timeout_filter.py diff --git a/tests/integration/fixtures/sensor_timeout_filter.yaml b/tests/integration/fixtures/sensor_timeout_filter.yaml new file mode 100644 index 0000000000..dbd4db3242 --- /dev/null +++ b/tests/integration/fixtures/sensor_timeout_filter.yaml @@ -0,0 +1,150 @@ +esphome: + name: test-timeout-filters + +host: +api: + batch_delay: 0ms # Disable batching to receive all state updates +logger: + level: DEBUG + +# Template sensors that we'll use to publish values +sensor: + - platform: template + name: "Source Timeout Last" + id: source_timeout_last + accuracy_decimals: 1 + + - platform: template + name: "Source Timeout Reset" + id: source_timeout_reset + accuracy_decimals: 1 + + - platform: template + name: "Source Timeout Static" + id: source_timeout_static + accuracy_decimals: 1 + + - platform: template + name: "Source Timeout Lambda" + id: source_timeout_lambda + accuracy_decimals: 1 + + # Test 1: TimeoutFilter - "last" mode (outputs last received value) + - platform: copy + source_id: source_timeout_last + name: "Timeout Last Sensor" + id: timeout_last_sensor + filters: + - timeout: + timeout: 100ms + value: last # Explicitly specify "last" mode to use TimeoutFilter class + + # Test 2: TimeoutFilter - reset behavior (same filter, different source) + - platform: copy + source_id: source_timeout_reset + name: "Timeout Reset Sensor" + id: timeout_reset_sensor + filters: + - timeout: + timeout: 100ms + value: last # Explicitly specify "last" mode + + # Test 3: TimeoutFilterConfigured - static value mode + - platform: copy + source_id: source_timeout_static + name: "Timeout Static Sensor" + id: timeout_static_sensor + filters: + - timeout: + timeout: 100ms + value: 99.9 + + # Test 4: TimeoutFilterConfigured - lambda mode + - platform: copy + source_id: source_timeout_lambda + name: "Timeout Lambda Sensor" + id: timeout_lambda_sensor + filters: + - timeout: + timeout: 100ms + value: !lambda "return -1.0;" + +# Scripts to publish values with controlled timing +script: + # Test 1: Single value followed by timeout + - id: test_timeout_last_script + then: + # Publish initial value + - sensor.template.publish: + id: source_timeout_last + state: 42.0 + # Wait for timeout to fire (100ms + margin) + - delay: 150ms + + # Test 2: Multiple values before timeout (should reset timer) + - id: test_timeout_reset_script + then: + # Publish first value + - sensor.template.publish: + id: source_timeout_reset + state: 10.0 + # Wait 50ms (halfway to timeout) + - delay: 50ms + # Publish second value (resets timeout) + - sensor.template.publish: + id: source_timeout_reset + state: 20.0 + # Wait 50ms (halfway to timeout again) + - delay: 50ms + # Publish third value (resets timeout) + - sensor.template.publish: + id: source_timeout_reset + state: 30.0 + # Wait for timeout to fire (100ms + margin) + - delay: 150ms + + # Test 3: Static value timeout + - id: test_timeout_static_script + then: + # Publish initial value + - sensor.template.publish: + id: source_timeout_static + state: 55.5 + # Wait for timeout to fire + - delay: 150ms + + # Test 4: Lambda value timeout + - id: test_timeout_lambda_script + then: + # Publish initial value + - sensor.template.publish: + id: source_timeout_lambda + state: 77.7 + # Wait for timeout to fire + - delay: 150ms + +# Buttons to trigger each test scenario +button: + - platform: template + name: "Test Timeout Last Button" + id: test_timeout_last_button + on_press: + - script.execute: test_timeout_last_script + + - platform: template + name: "Test Timeout Reset Button" + id: test_timeout_reset_button + on_press: + - script.execute: test_timeout_reset_script + + - platform: template + name: "Test Timeout Static Button" + id: test_timeout_static_button + on_press: + - script.execute: test_timeout_static_script + + - platform: template + name: "Test Timeout Lambda Button" + id: test_timeout_lambda_button + on_press: + - script.execute: test_timeout_lambda_script diff --git a/tests/integration/test_sensor_timeout_filter.py b/tests/integration/test_sensor_timeout_filter.py new file mode 100644 index 0000000000..6a2fef0c57 --- /dev/null +++ b/tests/integration/test_sensor_timeout_filter.py @@ -0,0 +1,200 @@ +"""Test sensor timeout filter functionality.""" + +from __future__ import annotations + +import asyncio + +from aioesphomeapi import EntityState, SensorState +import pytest + +from .state_utils import InitialStateHelper, build_key_to_entity_mapping +from .types import APIClientConnectedFactory, RunCompiledFunction + + +@pytest.mark.asyncio +async def test_sensor_timeout_filter( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """Test TimeoutFilter and TimeoutFilterConfigured with all modes.""" + loop = asyncio.get_running_loop() + + # Track state changes for all sensors + timeout_last_states: list[float] = [] + timeout_reset_states: list[float] = [] + timeout_static_states: list[float] = [] + timeout_lambda_states: list[float] = [] + + # Futures for each test scenario + test1_complete = loop.create_future() # TimeoutFilter - last mode + test2_complete = loop.create_future() # TimeoutFilter - reset behavior + test3_complete = loop.create_future() # TimeoutFilterConfigured - static value + test4_complete = loop.create_future() # TimeoutFilterConfigured - lambda + + def on_state(state: EntityState) -> None: + """Track sensor state updates.""" + if not isinstance(state, SensorState): + return + + if state.missing_state: + return + + sensor_name = key_to_sensor.get(state.key) + + # Test 1: TimeoutFilter - last mode + if sensor_name == "timeout_last_sensor": + timeout_last_states.append(state.state) + # Expect 2 values: initial 42.0 + timeout fires with 42.0 + if len(timeout_last_states) >= 2 and not test1_complete.done(): + test1_complete.set_result(True) + + # Test 2: TimeoutFilter - reset behavior + elif sensor_name == "timeout_reset_sensor": + timeout_reset_states.append(state.state) + # Expect 4 values: 10.0, 20.0, 30.0, then timeout fires with 30.0 + if len(timeout_reset_states) >= 4 and not test2_complete.done(): + test2_complete.set_result(True) + + # Test 3: TimeoutFilterConfigured - static value + elif sensor_name == "timeout_static_sensor": + timeout_static_states.append(state.state) + # Expect 2 values: initial 55.5 + timeout fires with 99.9 + if len(timeout_static_states) >= 2 and not test3_complete.done(): + test3_complete.set_result(True) + + # Test 4: TimeoutFilterConfigured - lambda + elif sensor_name == "timeout_lambda_sensor": + timeout_lambda_states.append(state.state) + # Expect 2 values: initial 77.7 + timeout fires with -1.0 + if len(timeout_lambda_states) >= 2 and not test4_complete.done(): + test4_complete.set_result(True) + + async with ( + run_compiled(yaml_config), + api_client_connected() as client, + ): + entities, services = await client.list_entities_services() + + key_to_sensor = build_key_to_entity_mapping( + entities, + [ + "timeout_last_sensor", + "timeout_reset_sensor", + "timeout_static_sensor", + "timeout_lambda_sensor", + ], + ) + + initial_state_helper = InitialStateHelper(entities) + client.subscribe_states(initial_state_helper.on_state_wrapper(on_state)) + + try: + await initial_state_helper.wait_for_initial_states() + except TimeoutError: + pytest.fail("Timeout waiting for initial states") + + # Find all test buttons + test1_button = next( + (e for e in entities if "test_timeout_last_button" in e.object_id.lower()), + None, + ) + test2_button = next( + (e for e in entities if "test_timeout_reset_button" in e.object_id.lower()), + None, + ) + test3_button = next( + ( + e + for e in entities + if "test_timeout_static_button" in e.object_id.lower() + ), + None, + ) + test4_button = next( + ( + e + for e in entities + if "test_timeout_lambda_button" in e.object_id.lower() + ), + None, + ) + + assert test1_button is not None, "Test Timeout Last Button not found" + assert test2_button is not None, "Test Timeout Reset Button not found" + assert test3_button is not None, "Test Timeout Static Button not found" + assert test4_button is not None, "Test Timeout Lambda Button not found" + + # === Test 1: TimeoutFilter - last mode === + client.button_command(test1_button.key) + try: + await asyncio.wait_for(test1_complete, timeout=2.0) + except TimeoutError: + pytest.fail(f"Test 1 timeout. Received states: {timeout_last_states}") + + assert len(timeout_last_states) == 2, ( + f"Test 1: Should have 2 states, got {len(timeout_last_states)}: {timeout_last_states}" + ) + assert timeout_last_states[0] == pytest.approx(42.0), ( + f"Test 1: First state should be 42.0, got {timeout_last_states[0]}" + ) + assert timeout_last_states[1] == pytest.approx(42.0), ( + f"Test 1: Timeout should output last value (42.0), got {timeout_last_states[1]}" + ) + + # === Test 2: TimeoutFilter - reset behavior === + client.button_command(test2_button.key) + try: + await asyncio.wait_for(test2_complete, timeout=2.0) + except TimeoutError: + pytest.fail(f"Test 2 timeout. Received states: {timeout_reset_states}") + + assert len(timeout_reset_states) == 4, ( + f"Test 2: Should have 4 states, got {len(timeout_reset_states)}: {timeout_reset_states}" + ) + assert timeout_reset_states[0] == pytest.approx(10.0), ( + f"Test 2: First state should be 10.0, got {timeout_reset_states[0]}" + ) + assert timeout_reset_states[1] == pytest.approx(20.0), ( + f"Test 2: Second state should be 20.0, got {timeout_reset_states[1]}" + ) + assert timeout_reset_states[2] == pytest.approx(30.0), ( + f"Test 2: Third state should be 30.0, got {timeout_reset_states[2]}" + ) + assert timeout_reset_states[3] == pytest.approx(30.0), ( + f"Test 2: Timeout should output last value (30.0), got {timeout_reset_states[3]}" + ) + + # === Test 3: TimeoutFilterConfigured - static value === + client.button_command(test3_button.key) + try: + await asyncio.wait_for(test3_complete, timeout=2.0) + except TimeoutError: + pytest.fail(f"Test 3 timeout. Received states: {timeout_static_states}") + + assert len(timeout_static_states) == 2, ( + f"Test 3: Should have 2 states, got {len(timeout_static_states)}: {timeout_static_states}" + ) + assert timeout_static_states[0] == pytest.approx(55.5), ( + f"Test 3: First state should be 55.5, got {timeout_static_states[0]}" + ) + assert timeout_static_states[1] == pytest.approx(99.9), ( + f"Test 3: Timeout should output configured value (99.9), got {timeout_static_states[1]}" + ) + + # === Test 4: TimeoutFilterConfigured - lambda === + client.button_command(test4_button.key) + try: + await asyncio.wait_for(test4_complete, timeout=2.0) + except TimeoutError: + pytest.fail(f"Test 4 timeout. Received states: {timeout_lambda_states}") + + assert len(timeout_lambda_states) == 2, ( + f"Test 4: Should have 2 states, got {len(timeout_lambda_states)}: {timeout_lambda_states}" + ) + assert timeout_lambda_states[0] == pytest.approx(77.7), ( + f"Test 4: First state should be 77.7, got {timeout_lambda_states[0]}" + ) + assert timeout_lambda_states[1] == pytest.approx(-1.0), ( + f"Test 4: Timeout should evaluate lambda (-1.0), got {timeout_lambda_states[1]}" + ) From af77dfeacc8510d27247b3d996096e500099e5c6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 15 Nov 2025 13:11:04 -0600 Subject: [PATCH 012/415] helper --- .../integration/test_sensor_timeout_filter.py | 51 +++++++------------ 1 file changed, 18 insertions(+), 33 deletions(-) diff --git a/tests/integration/test_sensor_timeout_filter.py b/tests/integration/test_sensor_timeout_filter.py index 6a2fef0c57..9b4704bb7b 100644 --- a/tests/integration/test_sensor_timeout_filter.py +++ b/tests/integration/test_sensor_timeout_filter.py @@ -94,39 +94,24 @@ async def test_sensor_timeout_filter( except TimeoutError: pytest.fail("Timeout waiting for initial states") - # Find all test buttons - test1_button = next( - (e for e in entities if "test_timeout_last_button" in e.object_id.lower()), - None, - ) - test2_button = next( - (e for e in entities if "test_timeout_reset_button" in e.object_id.lower()), - None, - ) - test3_button = next( - ( - e - for e in entities - if "test_timeout_static_button" in e.object_id.lower() - ), - None, - ) - test4_button = next( - ( - e - for e in entities - if "test_timeout_lambda_button" in e.object_id.lower() - ), - None, - ) + # Helper to find buttons by object_id substring + def find_button(object_id_substring: str) -> int: + """Find a button by object_id substring and return its key.""" + button = next( + (e for e in entities if object_id_substring in e.object_id.lower()), + None, + ) + assert button is not None, f"Button '{object_id_substring}' not found" + return button.key - assert test1_button is not None, "Test Timeout Last Button not found" - assert test2_button is not None, "Test Timeout Reset Button not found" - assert test3_button is not None, "Test Timeout Static Button not found" - assert test4_button is not None, "Test Timeout Lambda Button not found" + # Find all test buttons + test1_button_key = find_button("test_timeout_last_button") + test2_button_key = find_button("test_timeout_reset_button") + test3_button_key = find_button("test_timeout_static_button") + test4_button_key = find_button("test_timeout_lambda_button") # === Test 1: TimeoutFilter - last mode === - client.button_command(test1_button.key) + client.button_command(test1_button_key) try: await asyncio.wait_for(test1_complete, timeout=2.0) except TimeoutError: @@ -143,7 +128,7 @@ async def test_sensor_timeout_filter( ) # === Test 2: TimeoutFilter - reset behavior === - client.button_command(test2_button.key) + client.button_command(test2_button_key) try: await asyncio.wait_for(test2_complete, timeout=2.0) except TimeoutError: @@ -166,7 +151,7 @@ async def test_sensor_timeout_filter( ) # === Test 3: TimeoutFilterConfigured - static value === - client.button_command(test3_button.key) + client.button_command(test3_button_key) try: await asyncio.wait_for(test3_complete, timeout=2.0) except TimeoutError: @@ -183,7 +168,7 @@ async def test_sensor_timeout_filter( ) # === Test 4: TimeoutFilterConfigured - lambda === - client.button_command(test4_button.key) + client.button_command(test4_button_key) try: await asyncio.wait_for(test4_complete, timeout=2.0) except TimeoutError: From c61411c620ec632a35e1120ab135a32ae516fe50 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 15 Nov 2025 14:42:06 -0600 Subject: [PATCH 013/415] [scheduler] Fix timing breakage after 49 days of uptime on ESP8266/RP2040 --- esphome/core/scheduler.cpp | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/esphome/core/scheduler.cpp b/esphome/core/scheduler.cpp index d285af2d0e..d2e0f0dab4 100644 --- a/esphome/core/scheduler.cpp +++ b/esphome/core/scheduler.cpp @@ -609,13 +609,12 @@ uint64_t Scheduler::millis_64_(uint32_t now) { if (now < last && (last - now) > HALF_MAX_UINT32) { this->millis_major_++; major++; + this->last_millis_ = now; #ifdef ESPHOME_DEBUG_SCHEDULER ESP_LOGD(TAG, "Detected true 32-bit rollover at %" PRIu32 "ms (was %" PRIu32 ")", now, last); #endif /* ESPHOME_DEBUG_SCHEDULER */ - } - - // Only update if time moved forward - if (now > last) { + } else if (now > last) { + // Only update if time moved forward this->last_millis_ = now; } From 61eddfdcda506038927aace8d29f43f842ec76c6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 15 Nov 2025 16:47:35 -0600 Subject: [PATCH 014/415] [logger] Reduce ESP32 UART mutex overhead by 50% --- esphome/components/logger/__init__.py | 2 ++ esphome/components/logger/logger.cpp | 1 + esphome/components/logger/logger.h | 35 ++++++++++++++++++++-- esphome/components/logger/logger_esp32.cpp | 24 +++++---------- 4 files changed, 43 insertions(+), 19 deletions(-) diff --git a/esphome/components/logger/__init__.py b/esphome/components/logger/__init__.py index cf78e6ae63..39877030e9 100644 --- a/esphome/components/logger/__init__.py +++ b/esphome/components/logger/__init__.py @@ -365,8 +365,10 @@ async def to_code(config): if CORE.is_esp32: if config[CONF_HARDWARE_UART] == USB_CDC: add_idf_sdkconfig_option("CONFIG_ESP_CONSOLE_USB_CDC", True) + cg.add_define("USE_LOGGER_UART_SELECTION_USB_CDC") elif config[CONF_HARDWARE_UART] == USB_SERIAL_JTAG: add_idf_sdkconfig_option("CONFIG_ESP_CONSOLE_USB_SERIAL_JTAG", True) + cg.add_define("USE_LOGGER_UART_SELECTION_USB_SERIAL_JTAG") try: uart_selection(USB_SERIAL_JTAG) cg.add_define("USE_LOGGER_USB_SERIAL_JTAG") diff --git a/esphome/components/logger/logger.cpp b/esphome/components/logger/logger.cpp index 9a9bf89fe3..810b65db2b 100644 --- a/esphome/components/logger/logger.cpp +++ b/esphome/components/logger/logger.cpp @@ -210,6 +210,7 @@ void Logger::process_messages_() { // Note: Messages may appear slightly out of order due to async processing, but // this is preferred over corrupted/interleaved console output if (this->baud_rate_ > 0) { + this->add_newline_to_buffer_if_needed_(); this->write_msg_(this->tx_buffer_); } } diff --git a/esphome/components/logger/logger.h b/esphome/components/logger/logger.h index dc8e06e0c9..f4f101031f 100644 --- a/esphome/components/logger/logger.h +++ b/esphome/components/logger/logger.h @@ -71,6 +71,16 @@ static constexpr uint16_t MAX_HEADER_SIZE = 128; // "0x" + 2 hex digits per byte + '\0' static constexpr size_t MAX_POINTER_REPRESENTATION = 2 + sizeof(void *) * 2 + 1; +// Platform-specific: does write_msg_ add its own newline? +// ESP32: add newline to buffer (write it in one call) +// Zephyr: let printk/uart_poll_out add newline (unchanged behavior) +// Other platforms: println()/puts() adds newline, so skip +#if defined(USE_ESP32) +static constexpr bool WRITE_MSG_ADDS_NEWLINE = false; +#else +static constexpr bool WRITE_MSG_ADDS_NEWLINE = true; +#endif + #if defined(USE_ESP32) || defined(USE_ESP8266) || defined(USE_RP2040) || defined(USE_LIBRETINY) || defined(USE_ZEPHYR) /** Enum for logging UART selection * @@ -200,6 +210,21 @@ class Logger : public Component { } } + // Helper to add newline to buffer for platforms that need it + inline void HOT add_newline_to_buffer_if_needed_() { + if constexpr (!WRITE_MSG_ADDS_NEWLINE) { + // Add newline - don't need to maintain null termination + // write_msg_ uses tx_buffer_at_ as length, not strlen() + if (this->tx_buffer_at_ < this->tx_buffer_size_) { + this->tx_buffer_[this->tx_buffer_at_++] = '\n'; + } else if (this->tx_buffer_size_ > 0) { + // Buffer was full - replace last char with newline + this->tx_buffer_[this->tx_buffer_size_ - 1] = '\n'; + this->tx_buffer_at_ = this->tx_buffer_size_; + } + } + } + // Helper to format and send a log message to both console and callbacks inline void HOT log_message_to_buffer_and_send_(uint8_t level, const char *tag, int line, const char *format, va_list args) { @@ -208,10 +233,14 @@ class Logger : public Component { this->format_log_to_buffer_with_terminator_(level, tag, line, format, args, this->tx_buffer_, &this->tx_buffer_at_, this->tx_buffer_size_); - if (this->baud_rate_ > 0) { - this->write_msg_(this->tx_buffer_); // If logging is enabled, write to console - } + // Callbacks get message WITHOUT newline (for API/MQTT/syslog) this->log_callback_.call(level, tag, this->tx_buffer_, this->tx_buffer_at_); + + // Console gets message WITH newline (if platform needs it) + if (this->baud_rate_ > 0) { + this->add_newline_to_buffer_if_needed_(); + this->write_msg_(this->tx_buffer_); + } } // Write the body of the log message to the buffer diff --git a/esphome/components/logger/logger_esp32.cpp b/esphome/components/logger/logger_esp32.cpp index 7fc79e6f54..7435daebce 100644 --- a/esphome/components/logger/logger_esp32.cpp +++ b/esphome/components/logger/logger_esp32.cpp @@ -122,24 +122,16 @@ void Logger::pre_setup() { } void HOT Logger::write_msg_(const char *msg) { - if ( -#if defined(USE_LOGGER_USB_CDC) && !defined(USE_LOGGER_USB_SERIAL_JTAG) - this->uart_ == UART_SELECTION_USB_CDC -#elif defined(USE_LOGGER_USB_SERIAL_JTAG) && !defined(USE_LOGGER_USB_CDC) - this->uart_ == UART_SELECTION_USB_SERIAL_JTAG -#elif defined(USE_LOGGER_USB_CDC) && defined(USE_LOGGER_USB_SERIAL_JTAG) - this->uart_ == UART_SELECTION_USB_CDC || this->uart_ == UART_SELECTION_USB_SERIAL_JTAG + // Use tx_buffer_at_ if msg points to tx_buffer_, otherwise fall back to strlen + size_t len = (msg == this->tx_buffer_) ? this->tx_buffer_at_ : strlen(msg); + +#if defined(USE_LOGGER_UART_SELECTION_USB_CDC) || defined(USE_LOGGER_UART_SELECTION_USB_SERIAL_JTAG) + // USB CDC/JTAG - single write including newline (already in buffer) + esp_usb_console_write_buf(msg, len); #else - /* DISABLES CODE */ (false) // NOLINT + // Regular UART - single write including newline (already in buffer) + uart_write_bytes(this->uart_num_, msg, len); #endif - ) { - puts(msg); - } else { - // Use tx_buffer_at_ if msg points to tx_buffer_, otherwise fall back to strlen - size_t len = (msg == this->tx_buffer_) ? this->tx_buffer_at_ : strlen(msg); - uart_write_bytes(this->uart_num_, msg, len); - uart_write_bytes(this->uart_num_, "\n", 1); - } } const LogString *Logger::get_uart_selection_() { From 950dff1a38671141e66c4a6764e8e9f7110ce4da Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 15 Nov 2025 16:54:18 -0600 Subject: [PATCH 015/415] [logger] Reduce ESP32 UART mutex overhead by 50% --- esphome/components/logger/logger_esp32.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/esphome/components/logger/logger_esp32.cpp b/esphome/components/logger/logger_esp32.cpp index 7435daebce..e9f7d02403 100644 --- a/esphome/components/logger/logger_esp32.cpp +++ b/esphome/components/logger/logger_esp32.cpp @@ -127,7 +127,8 @@ void HOT Logger::write_msg_(const char *msg) { #if defined(USE_LOGGER_UART_SELECTION_USB_CDC) || defined(USE_LOGGER_UART_SELECTION_USB_SERIAL_JTAG) // USB CDC/JTAG - single write including newline (already in buffer) - esp_usb_console_write_buf(msg, len); + // Use fwrite to stdout which goes through VFS to USB console + fwrite(msg, 1, len, stdout); #else // Regular UART - single write including newline (already in buffer) uart_write_bytes(this->uart_num_, msg, len); From 88a23acc4b63123a3e51a00353aadcda1e560499 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 15 Nov 2025 17:19:37 -0600 Subject: [PATCH 016/415] tweak --- esphome/components/logger/logger.h | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/esphome/components/logger/logger.h b/esphome/components/logger/logger.h index f4f101031f..79142dc329 100644 --- a/esphome/components/logger/logger.h +++ b/esphome/components/logger/logger.h @@ -72,9 +72,10 @@ static constexpr uint16_t MAX_HEADER_SIZE = 128; static constexpr size_t MAX_POINTER_REPRESENTATION = 2 + sizeof(void *) * 2 + 1; // Platform-specific: does write_msg_ add its own newline? -// ESP32: add newline to buffer (write it in one call) -// Zephyr: let printk/uart_poll_out add newline (unchanged behavior) -// Other platforms: println()/puts() adds newline, so skip +// false: Caller must add newline to buffer before calling write_msg_ (ESP32) +// Allows single write call with newline included for efficiency +// true: write_msg_ adds newline itself via puts()/println() (other platforms) +// Newline should NOT be added to buffer #if defined(USE_ESP32) static constexpr bool WRITE_MSG_ADDS_NEWLINE = false; #else From d096f1192dff05dca00a2fb736ce53151fc525db Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 15 Nov 2025 17:35:32 -0600 Subject: [PATCH 017/415] make bot happy --- esphome/components/logger/logger.cpp | 13 +++++++------ esphome/components/logger/logger.h | 9 ++++++--- esphome/components/logger/logger_esp32.cpp | 11 ++++++++--- esphome/components/logger/logger_esp8266.cpp | 2 +- esphome/components/logger/logger_host.cpp | 2 +- esphome/components/logger/logger_libretiny.cpp | 2 +- esphome/components/logger/logger_rp2040.cpp | 2 +- esphome/components/logger/logger_zephyr.cpp | 2 +- 8 files changed, 26 insertions(+), 17 deletions(-) diff --git a/esphome/components/logger/logger.cpp b/esphome/components/logger/logger.cpp index 810b65db2b..914559a669 100644 --- a/esphome/components/logger/logger.cpp +++ b/esphome/components/logger/logger.cpp @@ -65,7 +65,7 @@ void HOT Logger::log_vprintf_(uint8_t level, const char *tag, int line, const ch uint16_t buffer_at = 0; // Initialize buffer position this->format_log_to_buffer_with_terminator_(level, tag, line, format, args, console_buffer, &buffer_at, MAX_CONSOLE_LOG_MSG_SIZE); - this->write_msg_(console_buffer); + this->write_msg_(console_buffer, buffer_at); } // Reset the recursion guard for this task @@ -135,12 +135,13 @@ void Logger::log_vprintf_(uint8_t level, const char *tag, int line, const __Flas this->format_log_to_buffer_with_terminator_(level, tag, line, this->tx_buffer_, args, this->tx_buffer_, &this->tx_buffer_at_, this->tx_buffer_size_); - // Write to console and send callback starting at the msg_start - if (this->baud_rate_ > 0) { - this->write_msg_(this->tx_buffer_ + msg_start); - } size_t msg_length = this->tx_buffer_at_ - msg_start; // Don't subtract 1 - tx_buffer_at_ is already at the null terminator position + + // Write to console and send callback starting at the msg_start + if (this->baud_rate_ > 0) { + this->write_msg_(this->tx_buffer_ + msg_start, msg_length); + } this->log_callback_.call(level, tag, this->tx_buffer_ + msg_start, msg_length); global_recursion_guard_ = false; @@ -211,7 +212,7 @@ void Logger::process_messages_() { // this is preferred over corrupted/interleaved console output if (this->baud_rate_ > 0) { this->add_newline_to_buffer_if_needed_(); - this->write_msg_(this->tx_buffer_); + this->write_msg_(this->tx_buffer_, this->tx_buffer_at_); } } } else { diff --git a/esphome/components/logger/logger.h b/esphome/components/logger/logger.h index 79142dc329..3031d8a36d 100644 --- a/esphome/components/logger/logger.h +++ b/esphome/components/logger/logger.h @@ -215,11 +215,14 @@ class Logger : public Component { inline void HOT add_newline_to_buffer_if_needed_() { if constexpr (!WRITE_MSG_ADDS_NEWLINE) { // Add newline - don't need to maintain null termination - // write_msg_ uses tx_buffer_at_ as length, not strlen() + // write_msg_ now always receives explicit length, so we can safely overwrite the null terminator + // This is safe because: + // 1. Callbacks already received the message (before we add newline) + // 2. write_msg_ receives the length explicitly (doesn't need null terminator) if (this->tx_buffer_at_ < this->tx_buffer_size_) { this->tx_buffer_[this->tx_buffer_at_++] = '\n'; } else if (this->tx_buffer_size_ > 0) { - // Buffer was full - replace last char with newline + // Buffer was full - replace last char with newline to ensure it's visible this->tx_buffer_[this->tx_buffer_size_ - 1] = '\n'; this->tx_buffer_at_ = this->tx_buffer_size_; } @@ -240,7 +243,7 @@ class Logger : public Component { // Console gets message WITH newline (if platform needs it) if (this->baud_rate_ > 0) { this->add_newline_to_buffer_if_needed_(); - this->write_msg_(this->tx_buffer_); + this->write_msg_(this->tx_buffer_, this->tx_buffer_at_); } } diff --git a/esphome/components/logger/logger_esp32.cpp b/esphome/components/logger/logger_esp32.cpp index e9f7d02403..32ef752462 100644 --- a/esphome/components/logger/logger_esp32.cpp +++ b/esphome/components/logger/logger_esp32.cpp @@ -121,13 +121,18 @@ void Logger::pre_setup() { ESP_LOGI(TAG, "Log initialized"); } -void HOT Logger::write_msg_(const char *msg) { - // Use tx_buffer_at_ if msg points to tx_buffer_, otherwise fall back to strlen - size_t len = (msg == this->tx_buffer_) ? this->tx_buffer_at_ : strlen(msg); +void HOT Logger::write_msg_(const char *msg, size_t len) { + // Length is now always passed explicitly - no strlen() fallback needed #if defined(USE_LOGGER_UART_SELECTION_USB_CDC) || defined(USE_LOGGER_UART_SELECTION_USB_SERIAL_JTAG) // USB CDC/JTAG - single write including newline (already in buffer) // Use fwrite to stdout which goes through VFS to USB console + // + // Note: These defines indicate the user's YAML configuration choice (hardware_uart: USB_CDC/USB_SERIAL_JTAG). + // They are ONLY defined when the user explicitly selects USB as the logger output in their config. + // This is compile-time selection, not runtime detection - if USB is configured, it's always used. + // There is no fallback to regular UART if "USB isn't connected" - that's the user's responsibility + // to configure correctly for their hardware. This approach eliminates runtime overhead. fwrite(msg, 1, len, stdout); #else // Regular UART - single write including newline (already in buffer) diff --git a/esphome/components/logger/logger_esp8266.cpp b/esphome/components/logger/logger_esp8266.cpp index 5063d88b92..8b4c2a3824 100644 --- a/esphome/components/logger/logger_esp8266.cpp +++ b/esphome/components/logger/logger_esp8266.cpp @@ -33,7 +33,7 @@ void Logger::pre_setup() { ESP_LOGI(TAG, "Log initialized"); } -void HOT Logger::write_msg_(const char *msg) { this->hw_serial_->println(msg); } +void HOT Logger::write_msg_(const char *msg, size_t) { this->hw_serial_->println(msg); } const LogString *Logger::get_uart_selection_() { switch (this->uart_) { diff --git a/esphome/components/logger/logger_host.cpp b/esphome/components/logger/logger_host.cpp index 4abe92286a..c5e1e6f865 100644 --- a/esphome/components/logger/logger_host.cpp +++ b/esphome/components/logger/logger_host.cpp @@ -3,7 +3,7 @@ namespace esphome::logger { -void HOT Logger::write_msg_(const char *msg) { +void HOT Logger::write_msg_(const char *msg, size_t) { time_t rawtime; struct tm *timeinfo; char buffer[80]; diff --git a/esphome/components/logger/logger_libretiny.cpp b/esphome/components/logger/logger_libretiny.cpp index 3edfa74480..b8017b841d 100644 --- a/esphome/components/logger/logger_libretiny.cpp +++ b/esphome/components/logger/logger_libretiny.cpp @@ -49,7 +49,7 @@ void Logger::pre_setup() { ESP_LOGI(TAG, "Log initialized"); } -void HOT Logger::write_msg_(const char *msg) { this->hw_serial_->println(msg); } +void HOT Logger::write_msg_(const char *msg, size_t) { this->hw_serial_->println(msg); } const LogString *Logger::get_uart_selection_() { switch (this->uart_) { diff --git a/esphome/components/logger/logger_rp2040.cpp b/esphome/components/logger/logger_rp2040.cpp index 63727c2cda..4a8535c8e4 100644 --- a/esphome/components/logger/logger_rp2040.cpp +++ b/esphome/components/logger/logger_rp2040.cpp @@ -27,7 +27,7 @@ void Logger::pre_setup() { ESP_LOGI(TAG, "Log initialized"); } -void HOT Logger::write_msg_(const char *msg) { this->hw_serial_->println(msg); } +void HOT Logger::write_msg_(const char *msg, size_t) { this->hw_serial_->println(msg); } const LogString *Logger::get_uart_selection_() { switch (this->uart_) { diff --git a/esphome/components/logger/logger_zephyr.cpp b/esphome/components/logger/logger_zephyr.cpp index fb0c7dcca3..ec2ff3013c 100644 --- a/esphome/components/logger/logger_zephyr.cpp +++ b/esphome/components/logger/logger_zephyr.cpp @@ -62,7 +62,7 @@ void Logger::pre_setup() { ESP_LOGI(TAG, "Log initialized"); } -void HOT Logger::write_msg_(const char *msg) { +void HOT Logger::write_msg_(const char *msg, size_t) { #ifdef CONFIG_PRINTK printk("%s\n", msg); #endif From 730a70ee8be343faccced06d79708cb5bc2b7451 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 15 Nov 2025 17:37:37 -0600 Subject: [PATCH 018/415] missed header --- esphome/components/logger/logger.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/logger/logger.h b/esphome/components/logger/logger.h index 3031d8a36d..117e9d54d0 100644 --- a/esphome/components/logger/logger.h +++ b/esphome/components/logger/logger.h @@ -184,7 +184,7 @@ class Logger : public Component { protected: void process_messages_(); - void write_msg_(const char *msg); + void write_msg_(const char *msg, size_t len); // Format a log message with printf-style arguments and write it to a buffer with header, footer, and null terminator // It's the caller's responsibility to initialize buffer_at (typically to 0) From 0d147e5d10cb176bfd27cf7b0d7335c645130580 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 15 Nov 2025 17:42:01 -0600 Subject: [PATCH 019/415] missed one --- esphome/components/logger/logger.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/esphome/components/logger/logger.cpp b/esphome/components/logger/logger.cpp index 914559a669..2e8ed2b37b 100644 --- a/esphome/components/logger/logger.cpp +++ b/esphome/components/logger/logger.cpp @@ -138,11 +138,13 @@ void Logger::log_vprintf_(uint8_t level, const char *tag, int line, const __Flas size_t msg_length = this->tx_buffer_at_ - msg_start; // Don't subtract 1 - tx_buffer_at_ is already at the null terminator position - // Write to console and send callback starting at the msg_start + // Callbacks get message first (before console write) + this->log_callback_.call(level, tag, this->tx_buffer_ + msg_start, msg_length); + + // Write to console starting at the msg_start if (this->baud_rate_ > 0) { this->write_msg_(this->tx_buffer_ + msg_start, msg_length); } - this->log_callback_.call(level, tag, this->tx_buffer_ + msg_start, msg_length); global_recursion_guard_ = false; } From 554cdbd5a4a6357eccc5c0538d31776ba7f55cfd Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 15 Nov 2025 18:02:54 -0600 Subject: [PATCH 020/415] bot is right --- esphome/components/logger/logger.h | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/esphome/components/logger/logger.h b/esphome/components/logger/logger.h index 117e9d54d0..ea68594e23 100644 --- a/esphome/components/logger/logger.h +++ b/esphome/components/logger/logger.h @@ -458,7 +458,9 @@ class Logger : public Component { } // Update buffer_at with the formatted length (handle truncation) - uint16_t formatted_len = (ret >= remaining) ? remaining : ret; + // When vsnprintf truncates (ret >= remaining), it writes (remaining - 1) chars + null terminator + // When it doesn't truncate (ret < remaining), it writes ret chars + null terminator + uint16_t formatted_len = (ret >= remaining) ? (remaining - 1) : ret; *buffer_at += formatted_len; // Remove all trailing newlines right after formatting From d5d61546e7817e2a3e067395d985f4f5aad1b35d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 15 Nov 2025 18:15:10 -0600 Subject: [PATCH 021/415] cleanup --- esphome/components/logger/logger.cpp | 7 +++---- esphome/components/logger/logger.h | 26 ++++++++++++++++---------- 2 files changed, 19 insertions(+), 14 deletions(-) diff --git a/esphome/components/logger/logger.cpp b/esphome/components/logger/logger.cpp index 2e8ed2b37b..5ef0a68a38 100644 --- a/esphome/components/logger/logger.cpp +++ b/esphome/components/logger/logger.cpp @@ -65,6 +65,8 @@ void HOT Logger::log_vprintf_(uint8_t level, const char *tag, int line, const ch uint16_t buffer_at = 0; // Initialize buffer position this->format_log_to_buffer_with_terminator_(level, tag, line, format, args, console_buffer, &buffer_at, MAX_CONSOLE_LOG_MSG_SIZE); + // Add newline if platform needs it (ESP32 doesn't add via write_msg_) + this->add_newline_to_buffer_if_needed_(console_buffer, &buffer_at, MAX_CONSOLE_LOG_MSG_SIZE); this->write_msg_(console_buffer, buffer_at); } @@ -212,10 +214,7 @@ void Logger::process_messages_() { // This ensures all log messages appear on the console in a clean, serialized manner // Note: Messages may appear slightly out of order due to async processing, but // this is preferred over corrupted/interleaved console output - if (this->baud_rate_ > 0) { - this->add_newline_to_buffer_if_needed_(); - this->write_msg_(this->tx_buffer_, this->tx_buffer_at_); - } + this->write_tx_buffer_to_console_(); } } else { // No messages to process, disable loop if appropriate diff --git a/esphome/components/logger/logger.h b/esphome/components/logger/logger.h index ea68594e23..a6a6900f70 100644 --- a/esphome/components/logger/logger.h +++ b/esphome/components/logger/logger.h @@ -212,23 +212,32 @@ class Logger : public Component { } // Helper to add newline to buffer for platforms that need it - inline void HOT add_newline_to_buffer_if_needed_() { + // Modifies buffer_at to include the newline + inline void HOT add_newline_to_buffer_if_needed_(char *buffer, uint16_t *buffer_at, uint16_t buffer_size) { if constexpr (!WRITE_MSG_ADDS_NEWLINE) { // Add newline - don't need to maintain null termination // write_msg_ now always receives explicit length, so we can safely overwrite the null terminator // This is safe because: // 1. Callbacks already received the message (before we add newline) // 2. write_msg_ receives the length explicitly (doesn't need null terminator) - if (this->tx_buffer_at_ < this->tx_buffer_size_) { - this->tx_buffer_[this->tx_buffer_at_++] = '\n'; - } else if (this->tx_buffer_size_ > 0) { + if (*buffer_at < buffer_size) { + buffer[(*buffer_at)++] = '\n'; + } else if (buffer_size > 0) { // Buffer was full - replace last char with newline to ensure it's visible - this->tx_buffer_[this->tx_buffer_size_ - 1] = '\n'; - this->tx_buffer_at_ = this->tx_buffer_size_; + buffer[buffer_size - 1] = '\n'; + *buffer_at = buffer_size; } } } + // Helper to write tx_buffer_ to console if logging is enabled + inline void HOT write_tx_buffer_to_console_() { + if (this->baud_rate_ > 0) { + this->add_newline_to_buffer_if_needed_(this->tx_buffer_, &this->tx_buffer_at_, this->tx_buffer_size_); + this->write_msg_(this->tx_buffer_, this->tx_buffer_at_); + } + } + // Helper to format and send a log message to both console and callbacks inline void HOT log_message_to_buffer_and_send_(uint8_t level, const char *tag, int line, const char *format, va_list args) { @@ -241,10 +250,7 @@ class Logger : public Component { this->log_callback_.call(level, tag, this->tx_buffer_, this->tx_buffer_at_); // Console gets message WITH newline (if platform needs it) - if (this->baud_rate_ > 0) { - this->add_newline_to_buffer_if_needed_(); - this->write_msg_(this->tx_buffer_, this->tx_buffer_at_); - } + this->write_tx_buffer_to_console_(); } // Write the body of the log message to the buffer From d64bcf27b3c80d9d79b9b32b9cfa5fd1e6b61146 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 15 Nov 2025 18:17:50 -0600 Subject: [PATCH 022/415] cleanup --- esphome/components/logger/logger.cpp | 6 ++---- esphome/components/logger/logger.h | 9 ++++++--- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/esphome/components/logger/logger.cpp b/esphome/components/logger/logger.cpp index 5ef0a68a38..2eb2136ca8 100644 --- a/esphome/components/logger/logger.cpp +++ b/esphome/components/logger/logger.cpp @@ -137,16 +137,14 @@ void Logger::log_vprintf_(uint8_t level, const char *tag, int line, const __Flas this->format_log_to_buffer_with_terminator_(level, tag, line, this->tx_buffer_, args, this->tx_buffer_, &this->tx_buffer_at_, this->tx_buffer_size_); - size_t msg_length = + uint16_t msg_length = this->tx_buffer_at_ - msg_start; // Don't subtract 1 - tx_buffer_at_ is already at the null terminator position // Callbacks get message first (before console write) this->log_callback_.call(level, tag, this->tx_buffer_ + msg_start, msg_length); // Write to console starting at the msg_start - if (this->baud_rate_ > 0) { - this->write_msg_(this->tx_buffer_ + msg_start, msg_length); - } + this->write_tx_buffer_to_console_(msg_start, &msg_length); global_recursion_guard_ = false; } diff --git a/esphome/components/logger/logger.h b/esphome/components/logger/logger.h index a6a6900f70..e4a6383cc9 100644 --- a/esphome/components/logger/logger.h +++ b/esphome/components/logger/logger.h @@ -231,10 +231,13 @@ class Logger : public Component { } // Helper to write tx_buffer_ to console if logging is enabled - inline void HOT write_tx_buffer_to_console_() { + // offset: starting position in tx_buffer_ (default 0) + // length: pointer to length of message at offset (updated with newline if added) + inline void HOT write_tx_buffer_to_console_(uint32_t offset = 0, uint16_t *length = nullptr) { if (this->baud_rate_ > 0) { - this->add_newline_to_buffer_if_needed_(this->tx_buffer_, &this->tx_buffer_at_, this->tx_buffer_size_); - this->write_msg_(this->tx_buffer_, this->tx_buffer_at_); + uint16_t *len_ptr = length ? length : &this->tx_buffer_at_; + this->add_newline_to_buffer_if_needed_(this->tx_buffer_ + offset, len_ptr, this->tx_buffer_size_ - offset); + this->write_msg_(this->tx_buffer_ + offset, *len_ptr); } } From 9557c90c2025e83c1a01670da33504438c9905de Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 15 Nov 2025 18:31:07 -0600 Subject: [PATCH 023/415] comment --- esphome/components/logger/logger.h | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/esphome/components/logger/logger.h b/esphome/components/logger/logger.h index e4a6383cc9..6470ca5fc7 100644 --- a/esphome/components/logger/logger.h +++ b/esphome/components/logger/logger.h @@ -231,8 +231,7 @@ class Logger : public Component { } // Helper to write tx_buffer_ to console if logging is enabled - // offset: starting position in tx_buffer_ (default 0) - // length: pointer to length of message at offset (updated with newline if added) + // INTERNAL USE ONLY - offset > 0 requires length parameter to be non-null inline void HOT write_tx_buffer_to_console_(uint32_t offset = 0, uint16_t *length = nullptr) { if (this->baud_rate_ > 0) { uint16_t *len_ptr = length ? length : &this->tx_buffer_at_; From 26b820272aa1c9c2e0528ef3089230a568b397ff Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 15 Nov 2025 18:36:39 -0600 Subject: [PATCH 024/415] optimize esp8266 as well --- esphome/components/logger/logger.h | 4 ++-- esphome/components/logger/logger_esp8266.cpp | 5 ++++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/esphome/components/logger/logger.h b/esphome/components/logger/logger.h index 6470ca5fc7..94ceb5b3db 100644 --- a/esphome/components/logger/logger.h +++ b/esphome/components/logger/logger.h @@ -72,11 +72,11 @@ static constexpr uint16_t MAX_HEADER_SIZE = 128; static constexpr size_t MAX_POINTER_REPRESENTATION = 2 + sizeof(void *) * 2 + 1; // Platform-specific: does write_msg_ add its own newline? -// false: Caller must add newline to buffer before calling write_msg_ (ESP32) +// false: Caller must add newline to buffer before calling write_msg_ (ESP32, ESP8266) // Allows single write call with newline included for efficiency // true: write_msg_ adds newline itself via puts()/println() (other platforms) // Newline should NOT be added to buffer -#if defined(USE_ESP32) +#if defined(USE_ESP32) || defined(USE_ESP8266) static constexpr bool WRITE_MSG_ADDS_NEWLINE = false; #else static constexpr bool WRITE_MSG_ADDS_NEWLINE = true; diff --git a/esphome/components/logger/logger_esp8266.cpp b/esphome/components/logger/logger_esp8266.cpp index 8b4c2a3824..0fc73b747a 100644 --- a/esphome/components/logger/logger_esp8266.cpp +++ b/esphome/components/logger/logger_esp8266.cpp @@ -33,7 +33,10 @@ void Logger::pre_setup() { ESP_LOGI(TAG, "Log initialized"); } -void HOT Logger::write_msg_(const char *msg, size_t) { this->hw_serial_->println(msg); } +void HOT Logger::write_msg_(const char *msg, size_t len) { + // Single write with newline already in buffer (added by caller) + this->hw_serial_->write(msg, len); +} const LogString *Logger::get_uart_selection_() { switch (this->uart_) { From d60c358f48c1662e0faeeb7d248bc8af1cbd2b68 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 15 Nov 2025 18:43:39 -0600 Subject: [PATCH 025/415] preen --- esphome/components/logger/logger.cpp | 2 +- esphome/components/logger/logger.h | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/logger/logger.cpp b/esphome/components/logger/logger.cpp index 2eb2136ca8..9803bf528c 100644 --- a/esphome/components/logger/logger.cpp +++ b/esphome/components/logger/logger.cpp @@ -133,7 +133,7 @@ void Logger::log_vprintf_(uint8_t level, const char *tag, int line, const __Flas // Save the offset before calling format_log_to_buffer_with_terminator_ // since it will increment tx_buffer_at_ to the end of the formatted string - uint32_t msg_start = this->tx_buffer_at_; + uint16_t msg_start = this->tx_buffer_at_; this->format_log_to_buffer_with_terminator_(level, tag, line, this->tx_buffer_, args, this->tx_buffer_, &this->tx_buffer_at_, this->tx_buffer_size_); diff --git a/esphome/components/logger/logger.h b/esphome/components/logger/logger.h index 94ceb5b3db..8ba3dacacb 100644 --- a/esphome/components/logger/logger.h +++ b/esphome/components/logger/logger.h @@ -232,7 +232,7 @@ class Logger : public Component { // Helper to write tx_buffer_ to console if logging is enabled // INTERNAL USE ONLY - offset > 0 requires length parameter to be non-null - inline void HOT write_tx_buffer_to_console_(uint32_t offset = 0, uint16_t *length = nullptr) { + inline void HOT write_tx_buffer_to_console_(uint16_t offset = 0, uint16_t *length = nullptr) { if (this->baud_rate_ > 0) { uint16_t *len_ptr = length ? length : &this->tx_buffer_at_; this->add_newline_to_buffer_if_needed_(this->tx_buffer_ + offset, len_ptr, this->tx_buffer_size_ - offset); From eb759efb3d6503dd76f8e7e414250775b533b19b Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Sun, 16 Nov 2025 11:48:02 +1000 Subject: [PATCH 026/415] [font] Store glyph data in flash only (#11926) --- esphome/components/font/__init__.py | 29 ++++++---------- esphome/components/font/font.cpp | 46 ++++++++++++-------------- esphome/components/font/font.h | 44 ++++++++++++------------ esphome/components/lvgl/font.cpp | 4 +-- esphome/components/lvgl/lvgl_esphome.h | 4 +-- esphome/core/helpers.h | 17 ++++++++++ 6 files changed, 75 insertions(+), 69 deletions(-) diff --git a/esphome/components/font/__init__.py b/esphome/components/font/__init__.py index ddcee14635..32e803f405 100644 --- a/esphome/components/font/__init__.py +++ b/esphome/components/font/__init__.py @@ -36,7 +36,6 @@ from esphome.const import ( CONF_WEIGHT, ) from esphome.core import CORE, HexInt -from esphome.helpers import cpp_string_escape from esphome.types import ConfigType _LOGGER = logging.getLogger(__name__) @@ -50,7 +49,6 @@ font_ns = cg.esphome_ns.namespace("font") Font = font_ns.class_("Font") Glyph = font_ns.class_("Glyph") -GlyphData = font_ns.struct("GlyphData") CONF_BPP = "bpp" CONF_EXTRAS = "extras" @@ -463,7 +461,7 @@ FONT_SCHEMA = cv.Schema( ) ), cv.GenerateID(CONF_RAW_DATA_ID): cv.declare_id(cg.uint8), - cv.GenerateID(CONF_RAW_GLYPH_ID): cv.declare_id(GlyphData), + cv.GenerateID(CONF_RAW_GLYPH_ID): cv.declare_id(Glyph), }, ) @@ -583,22 +581,15 @@ async def to_code(config): # Create the glyph table that points to data in the above array. glyph_initializer = [ - cg.StructInitializer( - GlyphData, - ( - "a_char", - cg.RawExpression(f"(const uint8_t *){cpp_string_escape(x.glyph)}"), - ), - ( - "data", - cg.RawExpression(f"{str(prog_arr)} + {str(y - len(x.bitmap_data))}"), - ), - ("advance", x.advance), - ("offset_x", x.offset_x), - ("offset_y", x.offset_y), - ("width", x.width), - ("height", x.height), - ) + [ + x.glyph, + prog_arr + (y - len(x.bitmap_data)), + x.advance, + x.offset_x, + x.offset_y, + x.width, + x.height, + ] for (x, y) in zip( glyph_args, list(accumulate([len(x.bitmap_data) for x in glyph_args])) ) diff --git a/esphome/components/font/font.cpp b/esphome/components/font/font.cpp index 8b2420ac07..add403fe98 100644 --- a/esphome/components/font/font.cpp +++ b/esphome/components/font/font.cpp @@ -9,20 +9,19 @@ namespace font { static const char *const TAG = "font"; -const uint8_t *Glyph::get_char() const { return this->glyph_data_->a_char; } // Compare the char at the string position with this char. // Return true if this char is less than or equal the other. bool Glyph::compare_to(const uint8_t *str) const { // 1 -> this->char_ // 2 -> str for (uint32_t i = 0;; i++) { - if (this->glyph_data_->a_char[i] == '\0') + if (this->a_char[i] == '\0') return true; if (str[i] == '\0') return false; - if (this->glyph_data_->a_char[i] > str[i]) + if (this->a_char[i] > str[i]) return false; - if (this->glyph_data_->a_char[i] < str[i]) + if (this->a_char[i] < str[i]) return true; } // this should not happen @@ -30,35 +29,32 @@ bool Glyph::compare_to(const uint8_t *str) const { } int Glyph::match_length(const uint8_t *str) const { for (uint32_t i = 0;; i++) { - if (this->glyph_data_->a_char[i] == '\0') + if (this->a_char[i] == '\0') return i; - if (str[i] != this->glyph_data_->a_char[i]) + if (str[i] != this->a_char[i]) return 0; } // this should not happen return 0; } void Glyph::scan_area(int *x1, int *y1, int *width, int *height) const { - *x1 = this->glyph_data_->offset_x; - *y1 = this->glyph_data_->offset_y; - *width = this->glyph_data_->width; - *height = this->glyph_data_->height; + *x1 = this->offset_x; + *y1 = this->offset_y; + *width = this->width; + *height = this->height; } -Font::Font(const GlyphData *data, int data_nr, int baseline, int height, int descender, int xheight, int capheight, +Font::Font(const Glyph *data, int data_nr, int baseline, int height, int descender, int xheight, int capheight, uint8_t bpp) - : baseline_(baseline), + : glyphs_(ConstVector(data, data_nr)), + baseline_(baseline), height_(height), descender_(descender), linegap_(height - baseline - descender), xheight_(xheight), capheight_(capheight), - bpp_(bpp) { - glyphs_.reserve(data_nr); - for (int i = 0; i < data_nr; ++i) - glyphs_.emplace_back(&data[i]); -} -int Font::match_next_glyph(const uint8_t *str, int *match_length) { + bpp_(bpp) {} +int Font::match_next_glyph(const uint8_t *str, int *match_length) const { int lo = 0; int hi = this->glyphs_.size() - 1; while (lo != hi) { @@ -88,18 +84,18 @@ void Font::measure(const char *str, int *width, int *x_offset, int *baseline, in if (glyph_n < 0) { // Unknown char, skip if (!this->get_glyphs().empty()) - x += this->get_glyphs()[0].glyph_data_->advance; + x += this->get_glyphs()[0].advance; i++; continue; } const Glyph &glyph = this->glyphs_[glyph_n]; if (!has_char) { - min_x = glyph.glyph_data_->offset_x; + min_x = glyph.offset_x; } else { - min_x = std::min(min_x, x + glyph.glyph_data_->offset_x); + min_x = std::min(min_x, x + glyph.offset_x); } - x += glyph.glyph_data_->advance; + x += glyph.advance; i += match_length; has_char = true; @@ -118,7 +114,7 @@ void Font::print(int x_start, int y_start, display::Display *display, Color colo // Unknown char, skip ESP_LOGW(TAG, "Encountered character without representation in font: '%c'", text[i]); if (!this->get_glyphs().empty()) { - uint8_t glyph_width = this->get_glyphs()[0].glyph_data_->advance; + uint8_t glyph_width = this->get_glyphs()[0].advance; display->filled_rectangle(x_at, y_start, glyph_width, this->height_, color); x_at += glyph_width; } @@ -130,7 +126,7 @@ void Font::print(int x_start, int y_start, display::Display *display, Color colo const Glyph &glyph = this->get_glyphs()[glyph_n]; glyph.scan_area(&scan_x1, &scan_y1, &scan_width, &scan_height); - const uint8_t *data = glyph.glyph_data_->data; + const uint8_t *data = glyph.data; const int max_x = x_at + scan_x1 + scan_width; const int max_y = y_start + scan_y1 + scan_height; @@ -168,7 +164,7 @@ void Font::print(int x_start, int y_start, display::Display *display, Color colo } } } - x_at += glyph.glyph_data_->advance; + x_at += glyph.advance; i += match_length; } diff --git a/esphome/components/font/font.h b/esphome/components/font/font.h index 28832d647d..cb6cc89137 100644 --- a/esphome/components/font/font.h +++ b/esphome/components/font/font.h @@ -12,21 +12,19 @@ namespace font { class Font; -struct GlyphData { - const uint8_t *a_char; - const uint8_t *data; - int advance; - int offset_x; - int offset_y; - int width; - int height; -}; - class Glyph { public: - Glyph(const GlyphData *data) : glyph_data_(data) {} + constexpr Glyph(const char *a_char, const uint8_t *data, int advance, int offset_x, int offset_y, int width, + int height) + : a_char(a_char), + data(data), + advance(advance), + offset_x(offset_x), + offset_y(offset_y), + width(width), + height(height) {} - const uint8_t *get_char() const; + const uint8_t *get_char() const { return reinterpret_cast(this->a_char); } bool compare_to(const uint8_t *str) const; @@ -34,12 +32,16 @@ class Glyph { void scan_area(int *x1, int *y1, int *width, int *height) const; - const GlyphData *get_glyph_data() const { return this->glyph_data_; } + const char *a_char; + const uint8_t *data; + int advance; + int offset_x; + int offset_y; + int width; + int height; protected: friend Font; - - const GlyphData *glyph_data_; }; class Font @@ -50,8 +52,8 @@ class Font public: /** Construct the font with the given glyphs. * - * @param data A vector of glyphs, must be sorted lexicographically. - * @param data_nr The number of glyphs in data. + * @param data A list of glyphs, must be sorted lexicographically. + * @param data_nr The number of glyphs * @param baseline The y-offset from the top of the text to the baseline. * @param height The y-offset from the top of the text to the bottom. * @param descender The y-offset from the baseline to the lowest stroke in the font (e.g. from letters like g or p). @@ -59,10 +61,10 @@ class Font * @param capheight The height of capital letters, usually measured at the "X" glyph. * @param bpp The bits per pixel used for this font. Used to read data out of the glyph bitmaps. */ - Font(const GlyphData *data, int data_nr, int baseline, int height, int descender, int xheight, int capheight, + Font(const Glyph *data, int data_nr, int baseline, int height, int descender, int xheight, int capheight, uint8_t bpp = 1); - int match_next_glyph(const uint8_t *str, int *match_length); + int match_next_glyph(const uint8_t *str, int *match_length) const; #ifdef USE_DISPLAY void print(int x_start, int y_start, display::Display *display, Color color, const char *text, @@ -78,10 +80,10 @@ class Font inline int get_capheight() { return this->capheight_; } inline int get_bpp() { return this->bpp_; } - const std::vector> &get_glyphs() const { return glyphs_; } + const ConstVector &get_glyphs() const { return glyphs_; } protected: - std::vector> glyphs_; + ConstVector glyphs_; int baseline_; int height_; int descender_; diff --git a/esphome/components/lvgl/font.cpp b/esphome/components/lvgl/font.cpp index a0d5127570..1976fb9608 100644 --- a/esphome/components/lvgl/font.cpp +++ b/esphome/components/lvgl/font.cpp @@ -43,7 +43,7 @@ FontEngine::FontEngine(font::Font *esp_font) : font_(esp_font) { const lv_font_t *FontEngine::get_lv_font() { return &this->lv_font_; } -const font::GlyphData *FontEngine::get_glyph_data(uint32_t unicode_letter) { +const font::Glyph *FontEngine::get_glyph_data(uint32_t unicode_letter) { if (unicode_letter == last_letter_) return this->last_data_; uint8_t unicode[5]; @@ -67,7 +67,7 @@ const font::GlyphData *FontEngine::get_glyph_data(uint32_t unicode_letter) { int glyph_n = this->font_->match_next_glyph(unicode, &match_length); if (glyph_n < 0) return nullptr; - this->last_data_ = this->font_->get_glyphs()[glyph_n].get_glyph_data(); + this->last_data_ = &this->font_->get_glyphs()[glyph_n]; this->last_letter_ = unicode_letter; return this->last_data_; } diff --git a/esphome/components/lvgl/lvgl_esphome.h b/esphome/components/lvgl/lvgl_esphome.h index 1ae05f933f..196a0d1cb4 100644 --- a/esphome/components/lvgl/lvgl_esphome.h +++ b/esphome/components/lvgl/lvgl_esphome.h @@ -140,7 +140,7 @@ class FontEngine { FontEngine(font::Font *esp_font); const lv_font_t *get_lv_font(); - const font::GlyphData *get_glyph_data(uint32_t unicode_letter); + const font::Glyph *get_glyph_data(uint32_t unicode_letter); uint16_t baseline{}; uint16_t height{}; uint8_t bpp{}; @@ -148,7 +148,7 @@ class FontEngine { protected: font::Font *font_{}; uint32_t last_letter_{}; - const font::GlyphData *last_data_{}; + const font::Glyph *last_data_{}; lv_font_t lv_font_{}; }; #endif // USE_LVGL_FONT diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index 52a0746057..16eab8b8f6 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -111,6 +111,23 @@ template<> constexpr int64_t byteswap(int64_t n) { return __builtin_bswap64(n); /// @name Container utilities ///@{ +/// Lightweight read-only view over a const array stored in RODATA (will typically be in flash memory) +/// Avoids copying data from flash to RAM by keeping a pointer to the flash data. +/// Similar to std::span but with minimal overhead for embedded systems. + +template class ConstVector { + public: + constexpr ConstVector(const T *data, size_t size) : data_(data), size_(size) {} + + const constexpr T &operator[](size_t i) const { return data_[i]; } + constexpr size_t size() const { return size_; } + constexpr bool empty() const { return size_ == 0; } + + protected: + const T *data_; + size_t size_; +}; + /// Minimal static vector - saves memory by avoiding std::vector overhead template class StaticVector { public: From 6d03afecd05f448c542ecae0a66b146cefcdb89f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 15 Nov 2025 21:32:46 -0600 Subject: [PATCH 027/415] [binary_sensor] Modernize to C++17 nested namespaces and remove redundant qualifications --- .../components/binary_sensor/automation.cpp | 18 ++++++++---------- esphome/components/binary_sensor/automation.h | 6 ++---- .../components/binary_sensor/binary_sensor.cpp | 8 ++------ .../components/binary_sensor/binary_sensor.h | 7 ++----- esphome/components/binary_sensor/filter.cpp | 8 ++------ esphome/components/binary_sensor/filter.h | 8 ++------ 6 files changed, 18 insertions(+), 37 deletions(-) diff --git a/esphome/components/binary_sensor/automation.cpp b/esphome/components/binary_sensor/automation.cpp index 64a0d3db8d..66d8d6e90f 100644 --- a/esphome/components/binary_sensor/automation.cpp +++ b/esphome/components/binary_sensor/automation.cpp @@ -1,12 +1,11 @@ #include "automation.h" #include "esphome/core/log.h" -namespace esphome { -namespace binary_sensor { +namespace esphome::binary_sensor { static const char *const TAG = "binary_sensor.automation"; -void binary_sensor::MultiClickTrigger::on_state_(bool state) { +void MultiClickTrigger::on_state_(bool state) { // Handle duplicate events if (state == this->last_state_) { return; @@ -67,7 +66,7 @@ void binary_sensor::MultiClickTrigger::on_state_(bool state) { *this->at_index_ = *this->at_index_ + 1; } -void binary_sensor::MultiClickTrigger::schedule_cooldown_() { +void MultiClickTrigger::schedule_cooldown_() { ESP_LOGV(TAG, "Multi Click: Invalid length of press, starting cooldown of %" PRIu32 " ms", this->invalid_cooldown_); this->is_in_cooldown_ = true; this->set_timeout("cooldown", this->invalid_cooldown_, [this]() { @@ -79,7 +78,7 @@ void binary_sensor::MultiClickTrigger::schedule_cooldown_() { this->cancel_timeout("is_valid"); this->cancel_timeout("is_not_valid"); } -void binary_sensor::MultiClickTrigger::schedule_is_valid_(uint32_t min_length) { +void MultiClickTrigger::schedule_is_valid_(uint32_t min_length) { if (min_length == 0) { this->is_valid_ = true; return; @@ -90,19 +89,19 @@ void binary_sensor::MultiClickTrigger::schedule_is_valid_(uint32_t min_length) { this->is_valid_ = true; }); } -void binary_sensor::MultiClickTrigger::schedule_is_not_valid_(uint32_t max_length) { +void MultiClickTrigger::schedule_is_not_valid_(uint32_t max_length) { this->set_timeout("is_not_valid", max_length, [this]() { ESP_LOGV(TAG, "Multi Click: You waited too long to %s.", this->parent_->state ? "RELEASE" : "PRESS"); this->is_valid_ = false; this->schedule_cooldown_(); }); } -void binary_sensor::MultiClickTrigger::cancel() { +void MultiClickTrigger::cancel() { ESP_LOGV(TAG, "Multi Click: Sequence explicitly cancelled."); this->is_valid_ = false; this->schedule_cooldown_(); } -void binary_sensor::MultiClickTrigger::trigger_() { +void MultiClickTrigger::trigger_() { ESP_LOGV(TAG, "Multi Click: Hooray, multi click is valid. Triggering!"); this->at_index_.reset(); this->cancel_timeout("trigger"); @@ -118,5 +117,4 @@ bool match_interval(uint32_t min_length, uint32_t max_length, uint32_t length) { return length >= min_length && length <= max_length; } } -} // namespace binary_sensor -} // namespace esphome +} // namespace esphome::binary_sensor diff --git a/esphome/components/binary_sensor/automation.h b/esphome/components/binary_sensor/automation.h index f6971a2fc4..f8b130e08a 100644 --- a/esphome/components/binary_sensor/automation.h +++ b/esphome/components/binary_sensor/automation.h @@ -9,8 +9,7 @@ #include "esphome/core/helpers.h" #include "esphome/components/binary_sensor/binary_sensor.h" -namespace esphome { -namespace binary_sensor { +namespace esphome::binary_sensor { struct MultiClickTriggerEvent { bool state; @@ -172,5 +171,4 @@ template class BinarySensorInvalidateAction : public Action filters) { } bool BinarySensor::is_status_binary_sensor() const { return false; } -} // namespace binary_sensor - -} // namespace esphome +} // namespace esphome::binary_sensor diff --git a/esphome/components/binary_sensor/binary_sensor.h b/esphome/components/binary_sensor/binary_sensor.h index c1661d710f..0dca3e1520 100644 --- a/esphome/components/binary_sensor/binary_sensor.h +++ b/esphome/components/binary_sensor/binary_sensor.h @@ -6,9 +6,7 @@ #include -namespace esphome { - -namespace binary_sensor { +namespace esphome::binary_sensor { class BinarySensor; void log_binary_sensor(const char *tag, const char *prefix, const char *type, BinarySensor *obj); @@ -70,5 +68,4 @@ class BinarySensorInitiallyOff : public BinarySensor { bool has_state() const override { return true; } }; -} // namespace binary_sensor -} // namespace esphome +} // namespace esphome::binary_sensor diff --git a/esphome/components/binary_sensor/filter.cpp b/esphome/components/binary_sensor/filter.cpp index 8f31cf6fc2..9c7238f6d7 100644 --- a/esphome/components/binary_sensor/filter.cpp +++ b/esphome/components/binary_sensor/filter.cpp @@ -2,9 +2,7 @@ #include "binary_sensor.h" -namespace esphome { - -namespace binary_sensor { +namespace esphome::binary_sensor { static const char *const TAG = "sensor.filter"; @@ -132,6 +130,4 @@ optional SettleFilter::new_value(bool value) { float SettleFilter::get_setup_priority() const { return setup_priority::HARDWARE; } -} // namespace binary_sensor - -} // namespace esphome +} // namespace esphome::binary_sensor diff --git a/esphome/components/binary_sensor/filter.h b/esphome/components/binary_sensor/filter.h index 2d473c3b64..59bc43eeba 100644 --- a/esphome/components/binary_sensor/filter.h +++ b/esphome/components/binary_sensor/filter.h @@ -4,9 +4,7 @@ #include "esphome/core/component.h" #include "esphome/core/helpers.h" -namespace esphome { - -namespace binary_sensor { +namespace esphome::binary_sensor { class BinarySensor; @@ -139,6 +137,4 @@ class SettleFilter : public Filter, public Component { bool steady_{true}; }; -} // namespace binary_sensor - -} // namespace esphome +} // namespace esphome::binary_sensor From 5710cab972485a60af1488a62b71dca775002fb9 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 15 Nov 2025 22:03:43 -0600 Subject: [PATCH 028/415] [ld2412] Fix stuck targets by adding timeout filter (#11919) --- esphome/components/ld2412/sensor.py | 76 ++++++++++++++++++++++++++--- 1 file changed, 68 insertions(+), 8 deletions(-) diff --git a/esphome/components/ld2412/sensor.py b/esphome/components/ld2412/sensor.py index abb823faad..0bfbd9bf1d 100644 --- a/esphome/components/ld2412/sensor.py +++ b/esphome/components/ld2412/sensor.py @@ -31,36 +31,84 @@ CONFIG_SCHEMA = cv.Schema( cv.GenerateID(CONF_LD2412_ID): cv.use_id(LD2412Component), cv.Optional(CONF_DETECTION_DISTANCE): sensor.sensor_schema( device_class=DEVICE_CLASS_DISTANCE, - filters=[{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}], + filters=[ + { + "timeout": { + "timeout": cv.TimePeriod(milliseconds=1000), + "value": "last", + } + }, + {"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}, + ], icon=ICON_SIGNAL, unit_of_measurement=UNIT_CENTIMETER, ), cv.Optional(CONF_LIGHT): sensor.sensor_schema( device_class=DEVICE_CLASS_ILLUMINANCE, entity_category=ENTITY_CATEGORY_DIAGNOSTIC, - filters=[{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}], + filters=[ + { + "timeout": { + "timeout": cv.TimePeriod(milliseconds=1000), + "value": "last", + } + }, + {"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}, + ], icon=ICON_LIGHTBULB, unit_of_measurement=UNIT_EMPTY, # No standard unit for this light sensor ), cv.Optional(CONF_MOVING_DISTANCE): sensor.sensor_schema( device_class=DEVICE_CLASS_DISTANCE, - filters=[{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}], + filters=[ + { + "timeout": { + "timeout": cv.TimePeriod(milliseconds=1000), + "value": "last", + } + }, + {"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}, + ], icon=ICON_SIGNAL, unit_of_measurement=UNIT_CENTIMETER, ), cv.Optional(CONF_MOVING_ENERGY): sensor.sensor_schema( - filters=[{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}], + filters=[ + { + "timeout": { + "timeout": cv.TimePeriod(milliseconds=1000), + "value": "last", + } + }, + {"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}, + ], icon=ICON_MOTION_SENSOR, unit_of_measurement=UNIT_PERCENT, ), cv.Optional(CONF_STILL_DISTANCE): sensor.sensor_schema( device_class=DEVICE_CLASS_DISTANCE, - filters=[{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}], + filters=[ + { + "timeout": { + "timeout": cv.TimePeriod(milliseconds=1000), + "value": "last", + } + }, + {"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}, + ], icon=ICON_SIGNAL, unit_of_measurement=UNIT_CENTIMETER, ), cv.Optional(CONF_STILL_ENERGY): sensor.sensor_schema( - filters=[{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}], + filters=[ + { + "timeout": { + "timeout": cv.TimePeriod(milliseconds=1000), + "value": "last", + } + }, + {"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}, + ], icon=ICON_FLASH, unit_of_measurement=UNIT_PERCENT, ), @@ -74,7 +122,13 @@ CONFIG_SCHEMA = CONFIG_SCHEMA.extend( cv.Optional(CONF_MOVE_ENERGY): sensor.sensor_schema( entity_category=ENTITY_CATEGORY_DIAGNOSTIC, filters=[ - {"throttle_with_priority": cv.TimePeriod(milliseconds=1000)} + { + "timeout": { + "timeout": cv.TimePeriod(milliseconds=1000), + "value": "last", + } + }, + {"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}, ], icon=ICON_MOTION_SENSOR, unit_of_measurement=UNIT_PERCENT, @@ -82,7 +136,13 @@ CONFIG_SCHEMA = CONFIG_SCHEMA.extend( cv.Optional(CONF_STILL_ENERGY): sensor.sensor_schema( entity_category=ENTITY_CATEGORY_DIAGNOSTIC, filters=[ - {"throttle_with_priority": cv.TimePeriod(milliseconds=1000)} + { + "timeout": { + "timeout": cv.TimePeriod(milliseconds=1000), + "value": "last", + } + }, + {"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}, ], icon=ICON_FLASH, unit_of_measurement=UNIT_PERCENT, From 6b158e760d379033b811cd7bf73be0f95b33a09c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 15 Nov 2025 22:04:25 -0600 Subject: [PATCH 029/415] [ld2410] Add timeout filter to prevent stuck targets (#11920) --- esphome/components/ld2410/sensor.py | 76 ++++++++++++++++++++++++++--- 1 file changed, 68 insertions(+), 8 deletions(-) diff --git a/esphome/components/ld2410/sensor.py b/esphome/components/ld2410/sensor.py index fca2b2ceca..3bd34963bc 100644 --- a/esphome/components/ld2410/sensor.py +++ b/esphome/components/ld2410/sensor.py @@ -31,35 +31,83 @@ CONFIG_SCHEMA = cv.Schema( cv.GenerateID(CONF_LD2410_ID): cv.use_id(LD2410Component), cv.Optional(CONF_MOVING_DISTANCE): sensor.sensor_schema( device_class=DEVICE_CLASS_DISTANCE, - filters=[{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}], + filters=[ + { + "timeout": { + "timeout": cv.TimePeriod(milliseconds=1000), + "value": "last", + } + }, + {"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}, + ], icon=ICON_SIGNAL, unit_of_measurement=UNIT_CENTIMETER, ), cv.Optional(CONF_STILL_DISTANCE): sensor.sensor_schema( device_class=DEVICE_CLASS_DISTANCE, - filters=[{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}], + filters=[ + { + "timeout": { + "timeout": cv.TimePeriod(milliseconds=1000), + "value": "last", + } + }, + {"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}, + ], icon=ICON_SIGNAL, unit_of_measurement=UNIT_CENTIMETER, ), cv.Optional(CONF_MOVING_ENERGY): sensor.sensor_schema( - filters=[{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}], + filters=[ + { + "timeout": { + "timeout": cv.TimePeriod(milliseconds=1000), + "value": "last", + } + }, + {"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}, + ], icon=ICON_MOTION_SENSOR, unit_of_measurement=UNIT_PERCENT, ), cv.Optional(CONF_STILL_ENERGY): sensor.sensor_schema( - filters=[{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}], + filters=[ + { + "timeout": { + "timeout": cv.TimePeriod(milliseconds=1000), + "value": "last", + } + }, + {"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}, + ], icon=ICON_FLASH, unit_of_measurement=UNIT_PERCENT, ), cv.Optional(CONF_LIGHT): sensor.sensor_schema( device_class=DEVICE_CLASS_ILLUMINANCE, entity_category=ENTITY_CATEGORY_DIAGNOSTIC, - filters=[{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}], + filters=[ + { + "timeout": { + "timeout": cv.TimePeriod(milliseconds=1000), + "value": "last", + } + }, + {"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}, + ], icon=ICON_LIGHTBULB, ), cv.Optional(CONF_DETECTION_DISTANCE): sensor.sensor_schema( device_class=DEVICE_CLASS_DISTANCE, - filters=[{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}], + filters=[ + { + "timeout": { + "timeout": cv.TimePeriod(milliseconds=1000), + "value": "last", + } + }, + {"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}, + ], icon=ICON_SIGNAL, unit_of_measurement=UNIT_CENTIMETER, ), @@ -73,7 +121,13 @@ CONFIG_SCHEMA = CONFIG_SCHEMA.extend( cv.Optional(CONF_MOVE_ENERGY): sensor.sensor_schema( entity_category=ENTITY_CATEGORY_DIAGNOSTIC, filters=[ - {"throttle_with_priority": cv.TimePeriod(milliseconds=1000)} + { + "timeout": { + "timeout": cv.TimePeriod(milliseconds=1000), + "value": "last", + } + }, + {"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}, ], icon=ICON_MOTION_SENSOR, unit_of_measurement=UNIT_PERCENT, @@ -81,7 +135,13 @@ CONFIG_SCHEMA = CONFIG_SCHEMA.extend( cv.Optional(CONF_STILL_ENERGY): sensor.sensor_schema( entity_category=ENTITY_CATEGORY_DIAGNOSTIC, filters=[ - {"throttle_with_priority": cv.TimePeriod(milliseconds=1000)} + { + "timeout": { + "timeout": cv.TimePeriod(milliseconds=1000), + "value": "last", + } + }, + {"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}, ], icon=ICON_FLASH, unit_of_measurement=UNIT_PERCENT, From fc546ca3f6b63aaa43e208fe472c12eaa9c194b7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 15 Nov 2025 22:20:57 -0600 Subject: [PATCH 030/415] [scheduler] Fix timing breakage after 49 days of uptime on ESP8266/RP2040 (#11924) --- esphome/core/scheduler.cpp | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/esphome/core/scheduler.cpp b/esphome/core/scheduler.cpp index d285af2d0e..d2e0f0dab4 100644 --- a/esphome/core/scheduler.cpp +++ b/esphome/core/scheduler.cpp @@ -609,13 +609,12 @@ uint64_t Scheduler::millis_64_(uint32_t now) { if (now < last && (last - now) > HALF_MAX_UINT32) { this->millis_major_++; major++; + this->last_millis_ = now; #ifdef ESPHOME_DEBUG_SCHEDULER ESP_LOGD(TAG, "Detected true 32-bit rollover at %" PRIu32 "ms (was %" PRIu32 ")", now, last); #endif /* ESPHOME_DEBUG_SCHEDULER */ - } - - // Only update if time moved forward - if (now > last) { + } else if (now > last) { + // Only update if time moved forward this->last_millis_ = now; } From ea2b4c3e2500a21d01d28a255a755c34391e237a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 15 Nov 2025 22:21:06 -0600 Subject: [PATCH 031/415] [binary_sensor] Modernize to C++17 nested namespaces and remove redundant qualifications (#11929) --- .../components/binary_sensor/automation.cpp | 18 ++++++++---------- esphome/components/binary_sensor/automation.h | 6 ++---- .../components/binary_sensor/binary_sensor.cpp | 8 ++------ .../components/binary_sensor/binary_sensor.h | 7 ++----- esphome/components/binary_sensor/filter.cpp | 8 ++------ esphome/components/binary_sensor/filter.h | 8 ++------ 6 files changed, 18 insertions(+), 37 deletions(-) diff --git a/esphome/components/binary_sensor/automation.cpp b/esphome/components/binary_sensor/automation.cpp index 64a0d3db8d..66d8d6e90f 100644 --- a/esphome/components/binary_sensor/automation.cpp +++ b/esphome/components/binary_sensor/automation.cpp @@ -1,12 +1,11 @@ #include "automation.h" #include "esphome/core/log.h" -namespace esphome { -namespace binary_sensor { +namespace esphome::binary_sensor { static const char *const TAG = "binary_sensor.automation"; -void binary_sensor::MultiClickTrigger::on_state_(bool state) { +void MultiClickTrigger::on_state_(bool state) { // Handle duplicate events if (state == this->last_state_) { return; @@ -67,7 +66,7 @@ void binary_sensor::MultiClickTrigger::on_state_(bool state) { *this->at_index_ = *this->at_index_ + 1; } -void binary_sensor::MultiClickTrigger::schedule_cooldown_() { +void MultiClickTrigger::schedule_cooldown_() { ESP_LOGV(TAG, "Multi Click: Invalid length of press, starting cooldown of %" PRIu32 " ms", this->invalid_cooldown_); this->is_in_cooldown_ = true; this->set_timeout("cooldown", this->invalid_cooldown_, [this]() { @@ -79,7 +78,7 @@ void binary_sensor::MultiClickTrigger::schedule_cooldown_() { this->cancel_timeout("is_valid"); this->cancel_timeout("is_not_valid"); } -void binary_sensor::MultiClickTrigger::schedule_is_valid_(uint32_t min_length) { +void MultiClickTrigger::schedule_is_valid_(uint32_t min_length) { if (min_length == 0) { this->is_valid_ = true; return; @@ -90,19 +89,19 @@ void binary_sensor::MultiClickTrigger::schedule_is_valid_(uint32_t min_length) { this->is_valid_ = true; }); } -void binary_sensor::MultiClickTrigger::schedule_is_not_valid_(uint32_t max_length) { +void MultiClickTrigger::schedule_is_not_valid_(uint32_t max_length) { this->set_timeout("is_not_valid", max_length, [this]() { ESP_LOGV(TAG, "Multi Click: You waited too long to %s.", this->parent_->state ? "RELEASE" : "PRESS"); this->is_valid_ = false; this->schedule_cooldown_(); }); } -void binary_sensor::MultiClickTrigger::cancel() { +void MultiClickTrigger::cancel() { ESP_LOGV(TAG, "Multi Click: Sequence explicitly cancelled."); this->is_valid_ = false; this->schedule_cooldown_(); } -void binary_sensor::MultiClickTrigger::trigger_() { +void MultiClickTrigger::trigger_() { ESP_LOGV(TAG, "Multi Click: Hooray, multi click is valid. Triggering!"); this->at_index_.reset(); this->cancel_timeout("trigger"); @@ -118,5 +117,4 @@ bool match_interval(uint32_t min_length, uint32_t max_length, uint32_t length) { return length >= min_length && length <= max_length; } } -} // namespace binary_sensor -} // namespace esphome +} // namespace esphome::binary_sensor diff --git a/esphome/components/binary_sensor/automation.h b/esphome/components/binary_sensor/automation.h index f6971a2fc4..f8b130e08a 100644 --- a/esphome/components/binary_sensor/automation.h +++ b/esphome/components/binary_sensor/automation.h @@ -9,8 +9,7 @@ #include "esphome/core/helpers.h" #include "esphome/components/binary_sensor/binary_sensor.h" -namespace esphome { -namespace binary_sensor { +namespace esphome::binary_sensor { struct MultiClickTriggerEvent { bool state; @@ -172,5 +171,4 @@ template class BinarySensorInvalidateAction : public Action filters) { } bool BinarySensor::is_status_binary_sensor() const { return false; } -} // namespace binary_sensor - -} // namespace esphome +} // namespace esphome::binary_sensor diff --git a/esphome/components/binary_sensor/binary_sensor.h b/esphome/components/binary_sensor/binary_sensor.h index c1661d710f..0dca3e1520 100644 --- a/esphome/components/binary_sensor/binary_sensor.h +++ b/esphome/components/binary_sensor/binary_sensor.h @@ -6,9 +6,7 @@ #include -namespace esphome { - -namespace binary_sensor { +namespace esphome::binary_sensor { class BinarySensor; void log_binary_sensor(const char *tag, const char *prefix, const char *type, BinarySensor *obj); @@ -70,5 +68,4 @@ class BinarySensorInitiallyOff : public BinarySensor { bool has_state() const override { return true; } }; -} // namespace binary_sensor -} // namespace esphome +} // namespace esphome::binary_sensor diff --git a/esphome/components/binary_sensor/filter.cpp b/esphome/components/binary_sensor/filter.cpp index 8f31cf6fc2..9c7238f6d7 100644 --- a/esphome/components/binary_sensor/filter.cpp +++ b/esphome/components/binary_sensor/filter.cpp @@ -2,9 +2,7 @@ #include "binary_sensor.h" -namespace esphome { - -namespace binary_sensor { +namespace esphome::binary_sensor { static const char *const TAG = "sensor.filter"; @@ -132,6 +130,4 @@ optional SettleFilter::new_value(bool value) { float SettleFilter::get_setup_priority() const { return setup_priority::HARDWARE; } -} // namespace binary_sensor - -} // namespace esphome +} // namespace esphome::binary_sensor diff --git a/esphome/components/binary_sensor/filter.h b/esphome/components/binary_sensor/filter.h index 2d473c3b64..59bc43eeba 100644 --- a/esphome/components/binary_sensor/filter.h +++ b/esphome/components/binary_sensor/filter.h @@ -4,9 +4,7 @@ #include "esphome/core/component.h" #include "esphome/core/helpers.h" -namespace esphome { - -namespace binary_sensor { +namespace esphome::binary_sensor { class BinarySensor; @@ -139,6 +137,4 @@ class SettleFilter : public Filter, public Component { bool steady_{true}; }; -} // namespace binary_sensor - -} // namespace esphome +} // namespace esphome::binary_sensor From 6f4042f401a27e8f6c46fa4b52f0e4f3c406be84 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 15 Nov 2025 22:21:38 -0600 Subject: [PATCH 032/415] Add tests for sensor timeout filters (#11923) --- .../fixtures/sensor_timeout_filter.yaml | 150 ++++++++++++++ .../integration/test_sensor_timeout_filter.py | 185 ++++++++++++++++++ 2 files changed, 335 insertions(+) create mode 100644 tests/integration/fixtures/sensor_timeout_filter.yaml create mode 100644 tests/integration/test_sensor_timeout_filter.py diff --git a/tests/integration/fixtures/sensor_timeout_filter.yaml b/tests/integration/fixtures/sensor_timeout_filter.yaml new file mode 100644 index 0000000000..dbd4db3242 --- /dev/null +++ b/tests/integration/fixtures/sensor_timeout_filter.yaml @@ -0,0 +1,150 @@ +esphome: + name: test-timeout-filters + +host: +api: + batch_delay: 0ms # Disable batching to receive all state updates +logger: + level: DEBUG + +# Template sensors that we'll use to publish values +sensor: + - platform: template + name: "Source Timeout Last" + id: source_timeout_last + accuracy_decimals: 1 + + - platform: template + name: "Source Timeout Reset" + id: source_timeout_reset + accuracy_decimals: 1 + + - platform: template + name: "Source Timeout Static" + id: source_timeout_static + accuracy_decimals: 1 + + - platform: template + name: "Source Timeout Lambda" + id: source_timeout_lambda + accuracy_decimals: 1 + + # Test 1: TimeoutFilter - "last" mode (outputs last received value) + - platform: copy + source_id: source_timeout_last + name: "Timeout Last Sensor" + id: timeout_last_sensor + filters: + - timeout: + timeout: 100ms + value: last # Explicitly specify "last" mode to use TimeoutFilter class + + # Test 2: TimeoutFilter - reset behavior (same filter, different source) + - platform: copy + source_id: source_timeout_reset + name: "Timeout Reset Sensor" + id: timeout_reset_sensor + filters: + - timeout: + timeout: 100ms + value: last # Explicitly specify "last" mode + + # Test 3: TimeoutFilterConfigured - static value mode + - platform: copy + source_id: source_timeout_static + name: "Timeout Static Sensor" + id: timeout_static_sensor + filters: + - timeout: + timeout: 100ms + value: 99.9 + + # Test 4: TimeoutFilterConfigured - lambda mode + - platform: copy + source_id: source_timeout_lambda + name: "Timeout Lambda Sensor" + id: timeout_lambda_sensor + filters: + - timeout: + timeout: 100ms + value: !lambda "return -1.0;" + +# Scripts to publish values with controlled timing +script: + # Test 1: Single value followed by timeout + - id: test_timeout_last_script + then: + # Publish initial value + - sensor.template.publish: + id: source_timeout_last + state: 42.0 + # Wait for timeout to fire (100ms + margin) + - delay: 150ms + + # Test 2: Multiple values before timeout (should reset timer) + - id: test_timeout_reset_script + then: + # Publish first value + - sensor.template.publish: + id: source_timeout_reset + state: 10.0 + # Wait 50ms (halfway to timeout) + - delay: 50ms + # Publish second value (resets timeout) + - sensor.template.publish: + id: source_timeout_reset + state: 20.0 + # Wait 50ms (halfway to timeout again) + - delay: 50ms + # Publish third value (resets timeout) + - sensor.template.publish: + id: source_timeout_reset + state: 30.0 + # Wait for timeout to fire (100ms + margin) + - delay: 150ms + + # Test 3: Static value timeout + - id: test_timeout_static_script + then: + # Publish initial value + - sensor.template.publish: + id: source_timeout_static + state: 55.5 + # Wait for timeout to fire + - delay: 150ms + + # Test 4: Lambda value timeout + - id: test_timeout_lambda_script + then: + # Publish initial value + - sensor.template.publish: + id: source_timeout_lambda + state: 77.7 + # Wait for timeout to fire + - delay: 150ms + +# Buttons to trigger each test scenario +button: + - platform: template + name: "Test Timeout Last Button" + id: test_timeout_last_button + on_press: + - script.execute: test_timeout_last_script + + - platform: template + name: "Test Timeout Reset Button" + id: test_timeout_reset_button + on_press: + - script.execute: test_timeout_reset_script + + - platform: template + name: "Test Timeout Static Button" + id: test_timeout_static_button + on_press: + - script.execute: test_timeout_static_script + + - platform: template + name: "Test Timeout Lambda Button" + id: test_timeout_lambda_button + on_press: + - script.execute: test_timeout_lambda_script diff --git a/tests/integration/test_sensor_timeout_filter.py b/tests/integration/test_sensor_timeout_filter.py new file mode 100644 index 0000000000..9b4704bb7b --- /dev/null +++ b/tests/integration/test_sensor_timeout_filter.py @@ -0,0 +1,185 @@ +"""Test sensor timeout filter functionality.""" + +from __future__ import annotations + +import asyncio + +from aioesphomeapi import EntityState, SensorState +import pytest + +from .state_utils import InitialStateHelper, build_key_to_entity_mapping +from .types import APIClientConnectedFactory, RunCompiledFunction + + +@pytest.mark.asyncio +async def test_sensor_timeout_filter( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """Test TimeoutFilter and TimeoutFilterConfigured with all modes.""" + loop = asyncio.get_running_loop() + + # Track state changes for all sensors + timeout_last_states: list[float] = [] + timeout_reset_states: list[float] = [] + timeout_static_states: list[float] = [] + timeout_lambda_states: list[float] = [] + + # Futures for each test scenario + test1_complete = loop.create_future() # TimeoutFilter - last mode + test2_complete = loop.create_future() # TimeoutFilter - reset behavior + test3_complete = loop.create_future() # TimeoutFilterConfigured - static value + test4_complete = loop.create_future() # TimeoutFilterConfigured - lambda + + def on_state(state: EntityState) -> None: + """Track sensor state updates.""" + if not isinstance(state, SensorState): + return + + if state.missing_state: + return + + sensor_name = key_to_sensor.get(state.key) + + # Test 1: TimeoutFilter - last mode + if sensor_name == "timeout_last_sensor": + timeout_last_states.append(state.state) + # Expect 2 values: initial 42.0 + timeout fires with 42.0 + if len(timeout_last_states) >= 2 and not test1_complete.done(): + test1_complete.set_result(True) + + # Test 2: TimeoutFilter - reset behavior + elif sensor_name == "timeout_reset_sensor": + timeout_reset_states.append(state.state) + # Expect 4 values: 10.0, 20.0, 30.0, then timeout fires with 30.0 + if len(timeout_reset_states) >= 4 and not test2_complete.done(): + test2_complete.set_result(True) + + # Test 3: TimeoutFilterConfigured - static value + elif sensor_name == "timeout_static_sensor": + timeout_static_states.append(state.state) + # Expect 2 values: initial 55.5 + timeout fires with 99.9 + if len(timeout_static_states) >= 2 and not test3_complete.done(): + test3_complete.set_result(True) + + # Test 4: TimeoutFilterConfigured - lambda + elif sensor_name == "timeout_lambda_sensor": + timeout_lambda_states.append(state.state) + # Expect 2 values: initial 77.7 + timeout fires with -1.0 + if len(timeout_lambda_states) >= 2 and not test4_complete.done(): + test4_complete.set_result(True) + + async with ( + run_compiled(yaml_config), + api_client_connected() as client, + ): + entities, services = await client.list_entities_services() + + key_to_sensor = build_key_to_entity_mapping( + entities, + [ + "timeout_last_sensor", + "timeout_reset_sensor", + "timeout_static_sensor", + "timeout_lambda_sensor", + ], + ) + + initial_state_helper = InitialStateHelper(entities) + client.subscribe_states(initial_state_helper.on_state_wrapper(on_state)) + + try: + await initial_state_helper.wait_for_initial_states() + except TimeoutError: + pytest.fail("Timeout waiting for initial states") + + # Helper to find buttons by object_id substring + def find_button(object_id_substring: str) -> int: + """Find a button by object_id substring and return its key.""" + button = next( + (e for e in entities if object_id_substring in e.object_id.lower()), + None, + ) + assert button is not None, f"Button '{object_id_substring}' not found" + return button.key + + # Find all test buttons + test1_button_key = find_button("test_timeout_last_button") + test2_button_key = find_button("test_timeout_reset_button") + test3_button_key = find_button("test_timeout_static_button") + test4_button_key = find_button("test_timeout_lambda_button") + + # === Test 1: TimeoutFilter - last mode === + client.button_command(test1_button_key) + try: + await asyncio.wait_for(test1_complete, timeout=2.0) + except TimeoutError: + pytest.fail(f"Test 1 timeout. Received states: {timeout_last_states}") + + assert len(timeout_last_states) == 2, ( + f"Test 1: Should have 2 states, got {len(timeout_last_states)}: {timeout_last_states}" + ) + assert timeout_last_states[0] == pytest.approx(42.0), ( + f"Test 1: First state should be 42.0, got {timeout_last_states[0]}" + ) + assert timeout_last_states[1] == pytest.approx(42.0), ( + f"Test 1: Timeout should output last value (42.0), got {timeout_last_states[1]}" + ) + + # === Test 2: TimeoutFilter - reset behavior === + client.button_command(test2_button_key) + try: + await asyncio.wait_for(test2_complete, timeout=2.0) + except TimeoutError: + pytest.fail(f"Test 2 timeout. Received states: {timeout_reset_states}") + + assert len(timeout_reset_states) == 4, ( + f"Test 2: Should have 4 states, got {len(timeout_reset_states)}: {timeout_reset_states}" + ) + assert timeout_reset_states[0] == pytest.approx(10.0), ( + f"Test 2: First state should be 10.0, got {timeout_reset_states[0]}" + ) + assert timeout_reset_states[1] == pytest.approx(20.0), ( + f"Test 2: Second state should be 20.0, got {timeout_reset_states[1]}" + ) + assert timeout_reset_states[2] == pytest.approx(30.0), ( + f"Test 2: Third state should be 30.0, got {timeout_reset_states[2]}" + ) + assert timeout_reset_states[3] == pytest.approx(30.0), ( + f"Test 2: Timeout should output last value (30.0), got {timeout_reset_states[3]}" + ) + + # === Test 3: TimeoutFilterConfigured - static value === + client.button_command(test3_button_key) + try: + await asyncio.wait_for(test3_complete, timeout=2.0) + except TimeoutError: + pytest.fail(f"Test 3 timeout. Received states: {timeout_static_states}") + + assert len(timeout_static_states) == 2, ( + f"Test 3: Should have 2 states, got {len(timeout_static_states)}: {timeout_static_states}" + ) + assert timeout_static_states[0] == pytest.approx(55.5), ( + f"Test 3: First state should be 55.5, got {timeout_static_states[0]}" + ) + assert timeout_static_states[1] == pytest.approx(99.9), ( + f"Test 3: Timeout should output configured value (99.9), got {timeout_static_states[1]}" + ) + + # === Test 4: TimeoutFilterConfigured - lambda === + client.button_command(test4_button_key) + try: + await asyncio.wait_for(test4_complete, timeout=2.0) + except TimeoutError: + pytest.fail(f"Test 4 timeout. Received states: {timeout_lambda_states}") + + assert len(timeout_lambda_states) == 2, ( + f"Test 4: Should have 2 states, got {len(timeout_lambda_states)}: {timeout_lambda_states}" + ) + assert timeout_lambda_states[0] == pytest.approx(77.7), ( + f"Test 4: First state should be 77.7, got {timeout_lambda_states[0]}" + ) + assert timeout_lambda_states[1] == pytest.approx(-1.0), ( + f"Test 4: Timeout should evaluate lambda (-1.0), got {timeout_lambda_states[1]}" + ) From 4fc4da6ed2d69d9cdb47d777abc24e0e5e0c29c4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 16 Nov 2025 07:35:31 -0600 Subject: [PATCH 033/415] [analyze-memory] Show all core symbols > 100 B instead of top 15 (#11909) --- esphome/analyze_memory/cli.py | 26 ++++++++++++++++++++------ 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/esphome/analyze_memory/cli.py b/esphome/analyze_memory/cli.py index 718f42330d..44ade221f8 100644 --- a/esphome/analyze_memory/cli.py +++ b/esphome/analyze_memory/cli.py @@ -15,6 +15,11 @@ from . import ( class MemoryAnalyzerCLI(MemoryAnalyzer): """Memory analyzer with CLI-specific report generation.""" + # Symbol size threshold for detailed analysis + SYMBOL_SIZE_THRESHOLD: int = ( + 100 # Show symbols larger than this in detailed analysis + ) + # Column width constants COL_COMPONENT: int = 29 COL_FLASH_TEXT: int = 14 @@ -191,14 +196,21 @@ class MemoryAnalyzerCLI(MemoryAnalyzer): f"{len(symbols):>{self.COL_CORE_COUNT}} | {percentage:>{self.COL_CORE_PERCENT - 1}.1f}%" ) - # Top 15 largest core symbols + # All core symbols above threshold lines.append("") - lines.append(f"Top 15 Largest {_COMPONENT_CORE} Symbols:") sorted_core_symbols = sorted( self._esphome_core_symbols, key=lambda x: x[2], reverse=True ) + large_core_symbols = [ + (symbol, demangled, size) + for symbol, demangled, size in sorted_core_symbols + if size > self.SYMBOL_SIZE_THRESHOLD + ] - for i, (symbol, demangled, size) in enumerate(sorted_core_symbols[:15]): + lines.append( + f"{_COMPONENT_CORE} Symbols > {self.SYMBOL_SIZE_THRESHOLD} B ({len(large_core_symbols)} symbols):" + ) + for i, (symbol, demangled, size) in enumerate(large_core_symbols): lines.append(f"{i + 1}. {demangled} ({size:,} B)") lines.append("=" * self.TABLE_WIDTH) @@ -268,13 +280,15 @@ class MemoryAnalyzerCLI(MemoryAnalyzer): lines.append(f"Total size: {comp_mem.flash_total:,} B") lines.append("") - # Show all symbols > 100 bytes for better visibility + # Show all symbols above threshold for better visibility large_symbols = [ - (sym, dem, size) for sym, dem, size in sorted_symbols if size > 100 + (sym, dem, size) + for sym, dem, size in sorted_symbols + if size > self.SYMBOL_SIZE_THRESHOLD ] lines.append( - f"{comp_name} Symbols > 100 B ({len(large_symbols)} symbols):" + f"{comp_name} Symbols > {self.SYMBOL_SIZE_THRESHOLD} B ({len(large_symbols)} symbols):" ) for i, (symbol, demangled, size) in enumerate(large_symbols): lines.append(f"{i + 1}. {demangled} ({size:,} B)") From a913e7df3329efaec15a08d62115449cf9e05e64 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 16 Nov 2025 08:16:41 -0600 Subject: [PATCH 034/415] handles newlines, add test to prove it --- tests/unit_tests/test_lambda_dedup.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tests/unit_tests/test_lambda_dedup.py b/tests/unit_tests/test_lambda_dedup.py index c112631f2d..bbf5f02e6d 100644 --- a/tests/unit_tests/test_lambda_dedup.py +++ b/tests/unit_tests/test_lambda_dedup.py @@ -227,6 +227,13 @@ def test_static_variable_detection() -> None: assert not cg._has_static_variables("int counter = 0; return counter++;") assert not cg._has_static_variables("return 42;") + # Should handle newlines between static and type/variable + assert cg._has_static_variables("static int\nfoo = 0;") + assert cg._has_static_variables("static\nint\nbar = 0;") + assert cg._has_static_variables( + "static int \n foo = 0;" + ) # Mixed spaces/newlines + def test_lambdas_with_static_not_deduplicated() -> None: """Test that lambdas with static variables are not deduplicated.""" From 02c5f18b5d698d80625db97bd2cef8c2b09fae75 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 16 Nov 2025 09:18:07 -0600 Subject: [PATCH 035/415] [web_server_idf] Fix lwIP assertion crash by shutting down sockets on connection close --- .../components/web_server_idf/web_server_idf.cpp | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/esphome/components/web_server_idf/web_server_idf.cpp b/esphome/components/web_server_idf/web_server_idf.cpp index 0dab5e7e8c..ce91569de2 100644 --- a/esphome/components/web_server_idf/web_server_idf.cpp +++ b/esphome/components/web_server_idf/web_server_idf.cpp @@ -489,10 +489,18 @@ AsyncEventSourceResponse::AsyncEventSourceResponse(const AsyncWebServerRequest * void AsyncEventSourceResponse::destroy(void *ptr) { auto *rsp = static_cast(ptr); - ESP_LOGD(TAG, "Event source connection closed (fd: %d)", rsp->fd_.load()); - // Mark as dead by setting fd to 0 - will be cleaned up in the main loop - rsp->fd_.store(0); - // Note: We don't delete or remove from set here to avoid race conditions + int fd = rsp->fd_.exchange(0); // Atomically get and clear fd + + if (fd > 0) { + ESP_LOGD(TAG, "Event source connection closed (fd: %d)", fd); + // Immediately shut down the socket to prevent lwIP from delivering more data + // This prevents "recv_tcp: recv for wrong pcb!" assertions when the TCP stack + // tries to deliver queued data after the session is marked as dead + // See: https://github.com/esphome/esphome/issues/11936 + shutdown(fd, SHUT_RDWR); + // Note: We don't close() the socket - httpd owns it and will close it + } + // Session will be cleaned up in the main loop to avoid race conditions } // helper for allowing only unique entries in the queue From 4c9d90377367d5542ab10b6bc048955ca2f5cf43 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 16 Nov 2025 11:56:52 -0600 Subject: [PATCH 036/415] [logger] Eliminate strlen overhead on LibreTiny --- esphome/components/logger/logger.h | 4 ++-- esphome/components/logger/logger_libretiny.cpp | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/esphome/components/logger/logger.h b/esphome/components/logger/logger.h index 8ba3dacacb..6a8b640331 100644 --- a/esphome/components/logger/logger.h +++ b/esphome/components/logger/logger.h @@ -72,11 +72,11 @@ static constexpr uint16_t MAX_HEADER_SIZE = 128; static constexpr size_t MAX_POINTER_REPRESENTATION = 2 + sizeof(void *) * 2 + 1; // Platform-specific: does write_msg_ add its own newline? -// false: Caller must add newline to buffer before calling write_msg_ (ESP32, ESP8266) +// false: Caller must add newline to buffer before calling write_msg_ (ESP32, ESP8266, LibreTiny) // Allows single write call with newline included for efficiency // true: write_msg_ adds newline itself via puts()/println() (other platforms) // Newline should NOT be added to buffer -#if defined(USE_ESP32) || defined(USE_ESP8266) +#if defined(USE_ESP32) || defined(USE_ESP8266) || defined(USE_LIBRETINY) static constexpr bool WRITE_MSG_ADDS_NEWLINE = false; #else static constexpr bool WRITE_MSG_ADDS_NEWLINE = true; diff --git a/esphome/components/logger/logger_libretiny.cpp b/esphome/components/logger/logger_libretiny.cpp index b8017b841d..cdf55e710c 100644 --- a/esphome/components/logger/logger_libretiny.cpp +++ b/esphome/components/logger/logger_libretiny.cpp @@ -49,7 +49,7 @@ void Logger::pre_setup() { ESP_LOGI(TAG, "Log initialized"); } -void HOT Logger::write_msg_(const char *msg, size_t) { this->hw_serial_->println(msg); } +void HOT Logger::write_msg_(const char *msg, size_t len) { this->hw_serial_->write(msg, len); } const LogString *Logger::get_uart_selection_() { switch (this->uart_) { From 8997fb3443a4fc64b5c083f0459477d04fbe5fc2 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 16 Nov 2025 13:02:59 -0600 Subject: [PATCH 037/415] [core] Reduce flash size by combining set_name() and set_object_id() calls --- esphome/core/entity_base.cpp | 6 ++++++ esphome/core/entity_base.h | 3 +++ esphome/core/entity_helpers.py | 6 ++---- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/esphome/core/entity_base.cpp b/esphome/core/entity_base.cpp index 4883c72cf1..046f99d8cc 100644 --- a/esphome/core/entity_base.cpp +++ b/esphome/core/entity_base.cpp @@ -74,6 +74,12 @@ void EntityBase::set_object_id(const char *object_id) { this->calc_object_id_(); } +void EntityBase::set_name_and_object_id(const char *name, const char *object_id) { + this->set_name(name); + this->object_id_c_str_ = object_id; + this->calc_object_id_(); +} + // Calculate Object ID Hash from Entity Name void EntityBase::calc_object_id_() { this->object_id_hash_ = diff --git a/esphome/core/entity_base.h b/esphome/core/entity_base.h index 2b52d66f76..aa9b92877a 100644 --- a/esphome/core/entity_base.h +++ b/esphome/core/entity_base.h @@ -41,6 +41,9 @@ class EntityBase { std::string get_object_id() const; void set_object_id(const char *object_id); + // Set both name and object_id in one call (reduces generated code size) + void set_name_and_object_id(const char *name, const char *object_id); + // Get the unique Object ID of this Entity uint32_t get_object_id_hash(); diff --git a/esphome/core/entity_helpers.py b/esphome/core/entity_helpers.py index 9b4786f835..f360b4d809 100644 --- a/esphome/core/entity_helpers.py +++ b/esphome/core/entity_helpers.py @@ -84,8 +84,6 @@ async def setup_entity(var: MockObj, config: ConfigType, platform: str) -> None: # Get device name for object ID calculation device_name = device_id_obj.id - add(var.set_name(config[CONF_NAME])) - # Calculate base object_id using the same logic as C++ # This must match the C++ behavior in esphome/core/entity_base.cpp base_object_id = get_base_entity_object_id( @@ -97,8 +95,8 @@ async def setup_entity(var: MockObj, config: ConfigType, platform: str) -> None: "Entity has empty name, using '%s' as object_id base", base_object_id ) - # Set the object ID - add(var.set_object_id(base_object_id)) + # Set both name and object_id in one call to reduce generated code size + add(var.set_name_and_object_id(config[CONF_NAME], base_object_id)) _LOGGER.debug( "Setting object_id '%s' for entity '%s' on platform '%s'", base_object_id, From 320120883cab070fa21ba187e13f0f3c6447c3b8 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Mon, 17 Nov 2025 07:47:54 +1000 Subject: [PATCH 038/415] [lvgl] Migrate lv_font creation into Font class and optimise (#11915) --- esphome/components/font/__init__.py | 2 + esphome/components/font/font.cpp | 263 ++++++++++++++++------- esphome/components/font/font.h | 33 +-- esphome/components/lvgl/__init__.py | 14 +- esphome/components/lvgl/font.cpp | 76 ------- esphome/components/lvgl/lv_validation.py | 5 +- esphome/components/lvgl/lvgl_esphome.h | 26 +-- esphome/components/lvgl/types.py | 1 - tests/components/lvgl/lvgl-package.yaml | 4 +- tests/components/lvgl/test.host.yaml | 6 + 10 files changed, 232 insertions(+), 198 deletions(-) delete mode 100644 esphome/components/lvgl/font.cpp diff --git a/esphome/components/font/__init__.py b/esphome/components/font/__init__.py index 32e803f405..2667dbdbdf 100644 --- a/esphome/components/font/__init__.py +++ b/esphome/components/font/__init__.py @@ -486,6 +486,8 @@ class GlyphInfo: def glyph_to_glyphinfo(glyph, font, size, bpp): + # Convert to 32 bit unicode codepoint + glyph = ord(glyph) scale = 256 // (1 << bpp) if not font.is_scalable: sizes = [pt_to_px(x.size) for x in font.available_sizes] diff --git a/esphome/components/font/font.cpp b/esphome/components/font/font.cpp index add403fe98..5e3bf1dd20 100644 --- a/esphome/components/font/font.cpp +++ b/esphome/components/font/font.cpp @@ -6,42 +6,147 @@ namespace esphome { namespace font { - static const char *const TAG = "font"; -// Compare the char at the string position with this char. -// Return true if this char is less than or equal the other. -bool Glyph::compare_to(const uint8_t *str) const { - // 1 -> this->char_ - // 2 -> str - for (uint32_t i = 0;; i++) { - if (this->a_char[i] == '\0') - return true; - if (str[i] == '\0') - return false; - if (this->a_char[i] > str[i]) - return false; - if (this->a_char[i] < str[i]) - return true; +#ifdef USE_LVGL_FONT +const uint8_t *Font::get_glyph_bitmap(const lv_font_t *font, uint32_t unicode_letter) { + auto *fe = (Font *) font->dsc; + const auto *gd = fe->get_glyph_data_(unicode_letter); + if (gd == nullptr) { + return nullptr; } - // this should not happen - return false; + return gd->data; } -int Glyph::match_length(const uint8_t *str) const { - for (uint32_t i = 0;; i++) { - if (this->a_char[i] == '\0') - return i; - if (str[i] != this->a_char[i]) + +bool Font::get_glyph_dsc_cb(const lv_font_t *font, lv_font_glyph_dsc_t *dsc, uint32_t unicode_letter, uint32_t next) { + auto *fe = (Font *) font->dsc; + const auto *gd = fe->get_glyph_data_(unicode_letter); + if (gd == nullptr) { + return false; + } + dsc->adv_w = gd->advance; + dsc->ofs_x = gd->offset_x; + dsc->ofs_y = fe->height_ - gd->height - gd->offset_y - fe->lv_font_.base_line; + dsc->box_w = gd->width; + dsc->box_h = gd->height; + dsc->is_placeholder = 0; + dsc->bpp = fe->get_bpp(); + return true; +} + +const Glyph *Font::get_glyph_data_(uint32_t unicode_letter) { + if (unicode_letter == this->last_letter_ && this->last_letter_ != 0) + return this->last_data_; + auto *glyph = this->find_glyph(unicode_letter); + if (glyph == nullptr) { + return nullptr; + } + this->last_data_ = glyph; + this->last_letter_ = unicode_letter; + return glyph; +} +#endif + +/** + * Attempt to extract a 32 bit Unicode codepoint from a UTF-8 string. + * If successful, return the codepoint and set the length to the number of bytes read. + * If the end of the string has been reached and a valid codepoint has not been found, return 0 and set the length to + * 0. + * + * @param utf8_str The input string + * @param length Pointer to length storage + * @return The extracted code point + */ +static uint32_t extract_unicode_codepoint(const char *utf8_str, size_t *length) { + // Safely cast to uint8_t* for correct bitwise operations on bytes + const uint8_t *current = reinterpret_cast(utf8_str); + uint32_t code_point = 0; + uint8_t c1 = *current++; + + // check for end of string + if (c1 == 0) { + *length = 0; + return 0; + } + + // --- 1-Byte Sequence: 0xxxxxxx (ASCII) --- + if (c1 < 0x80) { + // Valid ASCII byte. + code_point = c1; + // Optimization: No need to check for continuation bytes. + } + // --- 2-Byte Sequence: 110xxxxx 10xxxxxx --- + else if ((c1 & 0xE0) == 0xC0) { + uint8_t c2 = *current++; + + // Error Check 1: Check if c2 is a valid continuation byte (10xxxxxx) + if ((c2 & 0xC0) != 0x80) { + *length = 0; return 0; + } + + code_point = (c1 & 0x1F) << 6; + code_point |= (c2 & 0x3F); + + // Error Check 2: Overlong check (2-byte must be > 0x7F) + if (code_point <= 0x7F) { + *length = 0; + return 0; + } } - // this should not happen - return 0; -} -void Glyph::scan_area(int *x1, int *y1, int *width, int *height) const { - *x1 = this->offset_x; - *y1 = this->offset_y; - *width = this->width; - *height = this->height; + // --- 3-Byte Sequence: 1110xxxx 10xxxxxx 10xxxxxx --- + else if ((c1 & 0xF0) == 0xE0) { + uint8_t c2 = *current++; + uint8_t c3 = *current++; + + // Error Check 1: Check continuation bytes + if (((c2 & 0xC0) != 0x80) || ((c3 & 0xC0) != 0x80)) { + *length = 0; + return 0; + } + + code_point = (c1 & 0x0F) << 12; + code_point |= (c2 & 0x3F) << 6; + code_point |= (c3 & 0x3F); + + // Error Check 2: Overlong check (3-byte must be > 0x7FF) + // Also check for surrogates (0xD800-0xDFFF) + if (code_point <= 0x7FF || (code_point >= 0xD800 && code_point <= 0xDFFF)) { + *length = 0; + return 0; + } + } + // --- 4-Byte Sequence: 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx --- + else if ((c1 & 0xF8) == 0xF0) { + uint8_t c2 = *current++; + uint8_t c3 = *current++; + uint8_t c4 = *current++; + + // Error Check 1: Check continuation bytes + if (((c2 & 0xC0) != 0x80) || ((c3 & 0xC0) != 0x80) || ((c4 & 0xC0) != 0x80)) { + *length = 0; + return 0; + } + + code_point = (c1 & 0x07) << 18; + code_point |= (c2 & 0x3F) << 12; + code_point |= (c3 & 0x3F) << 6; + code_point |= (c4 & 0x3F); + + // Error Check 2: Overlong check (4-byte must be > 0xFFFF) + // Also check for valid Unicode range (must be <= 0x10FFFF) + if (code_point <= 0xFFFF || code_point > 0x10FFFF) { + *length = 0; + return 0; + } + } + // --- Invalid leading byte (e.g., 10xxxxxx or 11111xxx) --- + else { + *length = 0; + return 0; + } + *length = current - reinterpret_cast(utf8_str); + return code_point; } Font::Font(const Glyph *data, int data_nr, int baseline, int height, int descender, int xheight, int capheight, @@ -53,82 +158,93 @@ Font::Font(const Glyph *data, int data_nr, int baseline, int height, int descend linegap_(height - baseline - descender), xheight_(xheight), capheight_(capheight), - bpp_(bpp) {} -int Font::match_next_glyph(const uint8_t *str, int *match_length) const { + bpp_(bpp) { +#ifdef USE_LVGL_FONT + this->lv_font_.dsc = this; + this->lv_font_.line_height = this->get_height(); + this->lv_font_.base_line = this->lv_font_.line_height - this->get_baseline(); + this->lv_font_.get_glyph_dsc = get_glyph_dsc_cb; + this->lv_font_.get_glyph_bitmap = get_glyph_bitmap; + this->lv_font_.subpx = LV_FONT_SUBPX_NONE; + this->lv_font_.underline_position = -1; + this->lv_font_.underline_thickness = 1; +#endif +} + +const Glyph *Font::find_glyph(uint32_t codepoint) const { int lo = 0; int hi = this->glyphs_.size() - 1; while (lo != hi) { int mid = (lo + hi + 1) / 2; - if (this->glyphs_[mid].compare_to(str)) { + if (this->glyphs_[mid].is_less_or_equal(codepoint)) { lo = mid; } else { hi = mid - 1; } } - *match_length = this->glyphs_[lo].match_length(str); - if (*match_length <= 0) - return -1; - return lo; + auto *result = &this->glyphs_[lo]; + if (result->code_point == codepoint) + return result; + return nullptr; } + #ifdef USE_DISPLAY void Font::measure(const char *str, int *width, int *x_offset, int *baseline, int *height) { *baseline = this->baseline_; *height = this->height_; - int i = 0; int min_x = 0; bool has_char = false; int x = 0; - while (str[i] != '\0') { - int match_length; - int glyph_n = this->match_next_glyph((const uint8_t *) str + i, &match_length); - if (glyph_n < 0) { + for (;;) { + size_t length; + auto code_point = extract_unicode_codepoint(str, &length); + if (length == 0) + break; + str += length; + auto *glyph = this->find_glyph(code_point); + if (glyph == nullptr) { // Unknown char, skip - if (!this->get_glyphs().empty()) - x += this->get_glyphs()[0].advance; - i++; + if (!this->glyphs_.empty()) + x += this->glyphs_[0].advance; continue; } - const Glyph &glyph = this->glyphs_[glyph_n]; if (!has_char) { - min_x = glyph.offset_x; + min_x = glyph->offset_x; } else { - min_x = std::min(min_x, x + glyph.offset_x); + min_x = std::min(min_x, x + glyph->offset_x); } - x += glyph.advance; + x += glyph->advance; - i += match_length; has_char = true; } *x_offset = min_x; *width = x - min_x; } + void Font::print(int x_start, int y_start, display::Display *display, Color color, const char *text, Color background) { - int i = 0; int x_at = x_start; - int scan_x1, scan_y1, scan_width, scan_height; - while (text[i] != '\0') { - int match_length; - int glyph_n = this->match_next_glyph((const uint8_t *) text + i, &match_length); - if (glyph_n < 0) { + for (;;) { + size_t length; + auto code_point = extract_unicode_codepoint(text, &length); + if (length == 0) + break; + text += length; + auto *glyph = this->find_glyph(code_point); + if (glyph == nullptr) { // Unknown char, skip - ESP_LOGW(TAG, "Encountered character without representation in font: '%c'", text[i]); - if (!this->get_glyphs().empty()) { - uint8_t glyph_width = this->get_glyphs()[0].advance; - display->filled_rectangle(x_at, y_start, glyph_width, this->height_, color); + ESP_LOGW(TAG, "Codepoint 0x%08" PRIx32 " not found in font", code_point); + if (!this->glyphs_.empty()) { + uint8_t glyph_width = this->glyphs_[0].advance; + display->rectangle(x_at, y_start, glyph_width, this->height_, color); x_at += glyph_width; } - - i++; continue; } - const Glyph &glyph = this->get_glyphs()[glyph_n]; - glyph.scan_area(&scan_x1, &scan_y1, &scan_width, &scan_height); - - const uint8_t *data = glyph.data; - const int max_x = x_at + scan_x1 + scan_width; - const int max_y = y_start + scan_y1 + scan_height; + const uint8_t *data = glyph->data; + const int max_x = x_at + glyph->offset_x + glyph->width; + const int max_y = y_start + glyph->offset_y + glyph->height; uint8_t bitmask = 0; uint8_t pixel_data = 0; @@ -141,10 +257,10 @@ void Font::print(int x_start, int y_start, display::Display *display, Color colo auto b_g = (float) background.g; auto b_b = (float) background.b; auto b_w = (float) background.w; - for (int glyph_y = y_start + scan_y1; glyph_y != max_y; glyph_y++) { - for (int glyph_x = x_at + scan_x1; glyph_x != max_x; glyph_x++) { + for (int glyph_y = y_start + glyph->offset_y; glyph_y != max_y; glyph_y++) { + for (int glyph_x = x_at + glyph->offset_x; glyph_x != max_x; glyph_x++) { uint8_t pixel = 0; - for (int bit_num = 0; bit_num != this->bpp_; bit_num++) { + for (uint8_t bit_num = 0; bit_num != this->bpp_; bit_num++) { if (bitmask == 0) { pixel_data = progmem_read_byte(data++); bitmask = 0x80; @@ -164,12 +280,9 @@ void Font::print(int x_start, int y_start, display::Display *display, Color colo } } } - x_at += glyph.advance; - - i += match_length; + x_at += glyph->advance; } } #endif - } // namespace font } // namespace esphome diff --git a/esphome/components/font/font.h b/esphome/components/font/font.h index cb6cc89137..262ded3be4 100644 --- a/esphome/components/font/font.h +++ b/esphome/components/font/font.h @@ -6,6 +6,9 @@ #ifdef USE_DISPLAY #include "esphome/components/display/display.h" #endif +#ifdef USE_LVGL_FONT +#include +#endif namespace esphome { namespace font { @@ -14,9 +17,9 @@ class Font; class Glyph { public: - constexpr Glyph(const char *a_char, const uint8_t *data, int advance, int offset_x, int offset_y, int width, + constexpr Glyph(uint32_t code_point, const uint8_t *data, int advance, int offset_x, int offset_y, int width, int height) - : a_char(a_char), + : code_point(code_point), data(data), advance(advance), offset_x(offset_x), @@ -24,24 +27,15 @@ class Glyph { width(width), height(height) {} - const uint8_t *get_char() const { return reinterpret_cast(this->a_char); } + bool is_less_or_equal(uint32_t other) const { return this->code_point <= other; } - bool compare_to(const uint8_t *str) const; - - int match_length(const uint8_t *str) const; - - void scan_area(int *x1, int *y1, int *width, int *height) const; - - const char *a_char; + const uint32_t code_point; const uint8_t *data; int advance; int offset_x; int offset_y; int width; int height; - - protected: - friend Font; }; class Font @@ -64,7 +58,7 @@ class Font Font(const Glyph *data, int data_nr, int baseline, int height, int descender, int xheight, int capheight, uint8_t bpp = 1); - int match_next_glyph(const uint8_t *str, int *match_length) const; + const Glyph *find_glyph(uint32_t codepoint) const; #ifdef USE_DISPLAY void print(int x_start, int y_start, display::Display *display, Color color, const char *text, @@ -79,6 +73,9 @@ class Font inline int get_xheight() { return this->xheight_; } inline int get_capheight() { return this->capheight_; } inline int get_bpp() { return this->bpp_; } +#ifdef USE_LVGL_FONT + const lv_font_t *get_lv_font() const { return &this->lv_font_; } +#endif const ConstVector &get_glyphs() const { return glyphs_; } @@ -91,6 +88,14 @@ class Font int xheight_; int capheight_; uint8_t bpp_; // bits per pixel +#ifdef USE_LVGL_FONT + lv_font_t lv_font_{}; + static const uint8_t *get_glyph_bitmap(const lv_font_t *font, uint32_t unicode_letter); + static bool get_glyph_dsc_cb(const lv_font_t *font, lv_font_glyph_dsc_t *dsc, uint32_t unicode_letter, uint32_t next); + const Glyph *get_glyph_data_(uint32_t unicode_letter); + uint32_t last_letter_{}; + const Glyph *last_data_{}; +#endif }; } // namespace font diff --git a/esphome/components/lvgl/__init__.py b/esphome/components/lvgl/__init__.py index 2a24f343c3..eaa37b54dd 100644 --- a/esphome/components/lvgl/__init__.py +++ b/esphome/components/lvgl/__init__.py @@ -52,15 +52,7 @@ from .schemas import ( from .styles import add_top_layer, styles_to_code, theme_to_code from .touchscreens import touchscreen_schema, touchscreens_to_code from .trigger import add_on_boot_triggers, generate_triggers -from .types import ( - FontEngine, - IdleTrigger, - PlainTrigger, - lv_font_t, - lv_group_t, - lv_style_t, - lvgl_ns, -) +from .types import IdleTrigger, PlainTrigger, lv_font_t, lv_group_t, lv_style_t, lvgl_ns from .widgets import ( LvScrActType, Widget, @@ -244,7 +236,6 @@ async def to_code(configs): cg.add_global(lvgl_ns.using) for font in helpers.esphome_fonts_used: await cg.get_variable(font) - cg.new_Pvariable(ID(f"{font}_engine", True, type=FontEngine), MockObj(font)) default_font = config_0[df.CONF_DEFAULT_FONT] if not lvalid.is_lv_font(default_font): add_define( @@ -256,7 +247,8 @@ async def to_code(configs): type=lv_font_t.operator("ptr").operator("const"), ) cg.new_variable( - globfont_id, MockObj(await lvalid.lv_font.process(default_font)) + globfont_id, + MockObj(await lvalid.lv_font.process(default_font), "->").get_lv_font(), ) add_define("LV_FONT_DEFAULT", df.DEFAULT_ESPHOME_FONT) else: diff --git a/esphome/components/lvgl/font.cpp b/esphome/components/lvgl/font.cpp deleted file mode 100644 index 1976fb9608..0000000000 --- a/esphome/components/lvgl/font.cpp +++ /dev/null @@ -1,76 +0,0 @@ -#include "lvgl_esphome.h" - -#ifdef USE_LVGL_FONT -namespace esphome { -namespace lvgl { - -static const uint8_t *get_glyph_bitmap(const lv_font_t *font, uint32_t unicode_letter) { - auto *fe = (FontEngine *) font->dsc; - const auto *gd = fe->get_glyph_data(unicode_letter); - if (gd == nullptr) - return nullptr; - // esph_log_d(TAG, "Returning bitmap @ %X", (uint32_t)gd->data); - - return gd->data; -} - -static bool get_glyph_dsc_cb(const lv_font_t *font, lv_font_glyph_dsc_t *dsc, uint32_t unicode_letter, uint32_t next) { - auto *fe = (FontEngine *) font->dsc; - const auto *gd = fe->get_glyph_data(unicode_letter); - if (gd == nullptr) - return false; - dsc->adv_w = gd->advance; - dsc->ofs_x = gd->offset_x; - dsc->ofs_y = fe->height - gd->height - gd->offset_y - fe->baseline; - dsc->box_w = gd->width; - dsc->box_h = gd->height; - dsc->is_placeholder = 0; - dsc->bpp = fe->bpp; - return true; -} - -FontEngine::FontEngine(font::Font *esp_font) : font_(esp_font) { - this->bpp = esp_font->get_bpp(); - this->lv_font_.dsc = this; - this->lv_font_.line_height = this->height = esp_font->get_height(); - this->lv_font_.base_line = this->baseline = this->lv_font_.line_height - esp_font->get_baseline(); - this->lv_font_.get_glyph_dsc = get_glyph_dsc_cb; - this->lv_font_.get_glyph_bitmap = get_glyph_bitmap; - this->lv_font_.subpx = LV_FONT_SUBPX_NONE; - this->lv_font_.underline_position = -1; - this->lv_font_.underline_thickness = 1; -} - -const lv_font_t *FontEngine::get_lv_font() { return &this->lv_font_; } - -const font::Glyph *FontEngine::get_glyph_data(uint32_t unicode_letter) { - if (unicode_letter == last_letter_) - return this->last_data_; - uint8_t unicode[5]; - memset(unicode, 0, sizeof unicode); - if (unicode_letter > 0xFFFF) { - unicode[0] = 0xF0 + ((unicode_letter >> 18) & 0x7); - unicode[1] = 0x80 + ((unicode_letter >> 12) & 0x3F); - unicode[2] = 0x80 + ((unicode_letter >> 6) & 0x3F); - unicode[3] = 0x80 + (unicode_letter & 0x3F); - } else if (unicode_letter > 0x7FF) { - unicode[0] = 0xE0 + ((unicode_letter >> 12) & 0xF); - unicode[1] = 0x80 + ((unicode_letter >> 6) & 0x3F); - unicode[2] = 0x80 + (unicode_letter & 0x3F); - } else if (unicode_letter > 0x7F) { - unicode[0] = 0xC0 + ((unicode_letter >> 6) & 0x1F); - unicode[1] = 0x80 + (unicode_letter & 0x3F); - } else { - unicode[0] = unicode_letter; - } - int match_length; - int glyph_n = this->font_->match_next_glyph(unicode, &match_length); - if (glyph_n < 0) - return nullptr; - this->last_data_ = &this->font_->get_glyphs()[glyph_n]; - this->last_letter_ = unicode_letter; - return this->last_data_; -} -} // namespace lvgl -} // namespace esphome -#endif // USES_LVGL_FONT diff --git a/esphome/components/lvgl/lv_validation.py b/esphome/components/lvgl/lv_validation.py index 045258555c..23c322c31f 100644 --- a/esphome/components/lvgl/lv_validation.py +++ b/esphome/components/lvgl/lv_validation.py @@ -493,6 +493,7 @@ class LvFont(LValidator): return LV_FONTS if is_lv_font(value): return lv_builtin_font(value) + add_lv_use("font") fontval = cv.use_id(Font)(value) esphome_fonts_used.add(fontval) return requires_component("font")(fontval) @@ -502,7 +503,9 @@ class LvFont(LValidator): async def process(self, value, args=()): if is_lv_font(value): return literal(f"&lv_font_{value}") - return literal(f"{value}_engine->get_lv_font()") + if isinstance(value, str): + return literal(f"{value}") + return await super().process(value, args) lv_font = LvFont() diff --git a/esphome/components/lvgl/lvgl_esphome.h b/esphome/components/lvgl/lvgl_esphome.h index 196a0d1cb4..bd6f1fdb61 100644 --- a/esphome/components/lvgl/lvgl_esphome.h +++ b/esphome/components/lvgl/lvgl_esphome.h @@ -50,6 +50,14 @@ static const display::ColorBitness LV_BITNESS = display::ColorBitness::COLOR_BIT static const display::ColorBitness LV_BITNESS = display::ColorBitness::COLOR_BITNESS_332; #endif // LV_COLOR_DEPTH +#ifdef USE_LVGL_FONT +inline void lv_obj_set_style_text_font(lv_obj_t *obj, const font::Font *font, lv_style_selector_t part) { + lv_obj_set_style_text_font(obj, font->get_lv_font(), part); +} +inline void lv_style_set_text_font(lv_style_t *style, const font::Font *font) { + lv_style_set_text_font(style, font->get_lv_font()); +} +#endif #ifdef USE_LVGL_IMAGE // Shortcut / overload, so that the source of an image can easily be updated // from within a lambda. @@ -134,24 +142,6 @@ template class ObjUpdateAction : public Action { protected: std::function lamb_; }; -#ifdef USE_LVGL_FONT -class FontEngine { - public: - FontEngine(font::Font *esp_font); - const lv_font_t *get_lv_font(); - - const font::Glyph *get_glyph_data(uint32_t unicode_letter); - uint16_t baseline{}; - uint16_t height{}; - uint8_t bpp{}; - - protected: - font::Font *font_{}; - uint32_t last_letter_{}; - const font::Glyph *last_data_{}; - lv_font_t lv_font_{}; -}; -#endif // USE_LVGL_FONT #ifdef USE_LVGL_ANIMIMG void lv_animimg_stop(lv_obj_t *obj); #endif // USE_LVGL_ANIMIMG diff --git a/esphome/components/lvgl/types.py b/esphome/components/lvgl/types.py index 035320b6ac..b99c0ad5a3 100644 --- a/esphome/components/lvgl/types.py +++ b/esphome/components/lvgl/types.py @@ -45,7 +45,6 @@ lv_coord_t = cg.global_ns.namespace("lv_coord_t") lv_event_code_t = cg.global_ns.enum("lv_event_code_t") lv_indev_type_t = cg.global_ns.enum("lv_indev_type_t") lv_key_t = cg.global_ns.enum("lv_key_t") -FontEngine = lvgl_ns.class_("FontEngine") PlainTrigger = esphome_ns.class_("Trigger<>", automation.Trigger.template()) DrawEndTrigger = esphome_ns.class_( "Trigger", automation.Trigger.template(cg.uint32, cg.uint32) diff --git a/tests/components/lvgl/lvgl-package.yaml b/tests/components/lvgl/lvgl-package.yaml index d7c342b16e..e42a813b40 100644 --- a/tests/components/lvgl/lvgl-package.yaml +++ b/tests/components/lvgl/lvgl-package.yaml @@ -76,7 +76,7 @@ lvgl: line_width: 8 line_rounded: true - id: date_style - text_font: roboto10 + text_font: !lambda return id(roboto10); align: center text_color: !lambda return color_id2; bg_opa: cover @@ -267,7 +267,7 @@ lvgl: snprintf(buf, sizeof(buf), "Setup: %d", 42); return std::string(buf); align: top_mid - text_font: space16 + text_font: !lambda return id(space16); - label: id: chip_info_label # Test complex setup lambda (real-world pattern) diff --git a/tests/components/lvgl/test.host.yaml b/tests/components/lvgl/test.host.yaml index 39d9a0ebf3..00a8cd8c01 100644 --- a/tests/components/lvgl/test.host.yaml +++ b/tests/components/lvgl/test.host.yaml @@ -18,6 +18,7 @@ touchscreen: lvgl: - id: lvgl_0 + default_font: space16 displays: sdl0 - id: lvgl_1 displays: sdl1 @@ -39,3 +40,8 @@ lvgl: text: Click ME on_click: logger.log: Clicked + +font: + - file: "gfonts://Roboto" + id: space16 + bpp: 4 From 986d3c8f137aad0a6097606fae49b98a22949258 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 16 Nov 2025 18:38:38 -0600 Subject: [PATCH 039/415] [sntp] Merge multiple instances to fix crash and undefined behavior (#11904) --- esphome/components/sntp/time.py | 68 +++++ tests/component_tests/sntp/__init__.py | 1 + .../sntp/config/sntp_test.yaml | 22 ++ tests/component_tests/sntp/test_init.py | 238 ++++++++++++++++++ 4 files changed, 329 insertions(+) create mode 100644 tests/component_tests/sntp/__init__.py create mode 100644 tests/component_tests/sntp/config/sntp_test.yaml create mode 100644 tests/component_tests/sntp/test_init.py diff --git a/esphome/components/sntp/time.py b/esphome/components/sntp/time.py index d27fc9991d..69a2436d3d 100644 --- a/esphome/components/sntp/time.py +++ b/esphome/components/sntp/time.py @@ -1,9 +1,14 @@ +import logging + import esphome.codegen as cg from esphome.components import time as time_ +from esphome.config_helpers import merge_config import esphome.config_validation as cv from esphome.const import ( CONF_ID, + CONF_PLATFORM, CONF_SERVERS, + CONF_TIME, PLATFORM_BK72XX, PLATFORM_ESP32, PLATFORM_ESP8266, @@ -12,13 +17,74 @@ from esphome.const import ( PLATFORM_RTL87XX, ) from esphome.core import CORE +import esphome.final_validate as fv +from esphome.types import ConfigType + +_LOGGER = logging.getLogger(__name__) DEPENDENCIES = ["network"] + +CONF_SNTP = "sntp" + sntp_ns = cg.esphome_ns.namespace("sntp") SNTPComponent = sntp_ns.class_("SNTPComponent", time_.RealTimeClock) DEFAULT_SERVERS = ["0.pool.ntp.org", "1.pool.ntp.org", "2.pool.ntp.org"] + +def _sntp_final_validate(config: ConfigType) -> None: + """Merge multiple SNTP instances into one, similar to OTA merging behavior.""" + full_conf = fv.full_config.get() + time_confs = full_conf.get(CONF_TIME, []) + + sntp_configs: list[ConfigType] = [] + other_time_configs: list[ConfigType] = [] + + for time_conf in time_confs: + if time_conf.get(CONF_PLATFORM) == CONF_SNTP: + sntp_configs.append(time_conf) + else: + other_time_configs.append(time_conf) + + if len(sntp_configs) <= 1: + return + + # Merge all SNTP configs into the first one + merged = sntp_configs[0] + for sntp_conf in sntp_configs[1:]: + # Validate that IDs are consistent if manually specified + if merged[CONF_ID].is_manual and sntp_conf[CONF_ID].is_manual: + raise cv.Invalid( + f"Found multiple SNTP configurations but {CONF_ID} is inconsistent" + ) + merged = merge_config(merged, sntp_conf) + + # Deduplicate servers while preserving order + servers = merged[CONF_SERVERS] + unique_servers = list(dict.fromkeys(servers)) + + # Warn if we're dropping servers due to 3-server limit + if len(unique_servers) > 3: + dropped = unique_servers[3:] + unique_servers = unique_servers[:3] + _LOGGER.warning( + "SNTP supports maximum 3 servers. Dropped excess server(s): %s", + dropped, + ) + + merged[CONF_SERVERS] = unique_servers + + _LOGGER.warning( + "Found and merged %d SNTP time configurations into one instance", + len(sntp_configs), + ) + + # Replace time configs with merged SNTP + other time platforms + other_time_configs.append(merged) + full_conf[CONF_TIME] = other_time_configs + fv.full_config.set(full_conf) + + CONFIG_SCHEMA = cv.All( time_.TIME_SCHEMA.extend( { @@ -40,6 +106,8 @@ CONFIG_SCHEMA = cv.All( ), ) +FINAL_VALIDATE_SCHEMA = _sntp_final_validate + async def to_code(config): servers = config[CONF_SERVERS] diff --git a/tests/component_tests/sntp/__init__.py b/tests/component_tests/sntp/__init__.py new file mode 100644 index 0000000000..7d323a4980 --- /dev/null +++ b/tests/component_tests/sntp/__init__.py @@ -0,0 +1 @@ +"""Tests for SNTP component.""" diff --git a/tests/component_tests/sntp/config/sntp_test.yaml b/tests/component_tests/sntp/config/sntp_test.yaml new file mode 100644 index 0000000000..3942c9606b --- /dev/null +++ b/tests/component_tests/sntp/config/sntp_test.yaml @@ -0,0 +1,22 @@ +esphome: + name: sntp-test + +esp32: + board: esp32dev + framework: + type: esp-idf + +wifi: + ssid: "testssid" + password: "testpassword" + +# Test multiple SNTP instances that should be merged +time: + - platform: sntp + servers: + - 192.168.1.1 + - pool.ntp.org + - platform: sntp + servers: + - pool.ntp.org + - 192.168.1.2 diff --git a/tests/component_tests/sntp/test_init.py b/tests/component_tests/sntp/test_init.py new file mode 100644 index 0000000000..9197ff55d0 --- /dev/null +++ b/tests/component_tests/sntp/test_init.py @@ -0,0 +1,238 @@ +"""Tests for SNTP time configuration validation.""" + +from __future__ import annotations + +import logging +from typing import Any + +import pytest + +from esphome import config_validation as cv +from esphome.components.sntp.time import CONF_SNTP, _sntp_final_validate +from esphome.const import CONF_ID, CONF_PLATFORM, CONF_SERVERS, CONF_TIME +from esphome.core import ID +import esphome.final_validate as fv + + +@pytest.mark.parametrize( + ("time_configs", "expected_count", "expected_servers", "warning_messages"), + [ + pytest.param( + [ + { + CONF_PLATFORM: CONF_SNTP, + CONF_ID: ID("sntp_time", is_manual=False), + CONF_SERVERS: ["192.168.1.1", "pool.ntp.org"], + } + ], + 1, + ["192.168.1.1", "pool.ntp.org"], + [], + id="single_instance_no_merge", + ), + pytest.param( + [ + { + CONF_PLATFORM: CONF_SNTP, + CONF_ID: ID("sntp_time_1", is_manual=False), + CONF_SERVERS: ["192.168.1.1", "pool.ntp.org"], + }, + { + CONF_PLATFORM: CONF_SNTP, + CONF_ID: ID("sntp_time_2", is_manual=False), + CONF_SERVERS: ["192.168.1.2"], + }, + ], + 1, + ["192.168.1.1", "pool.ntp.org", "192.168.1.2"], + ["Found and merged 2 SNTP time configurations into one instance"], + id="two_instances_merged", + ), + pytest.param( + [ + { + CONF_PLATFORM: CONF_SNTP, + CONF_ID: ID("sntp_time_1", is_manual=False), + CONF_SERVERS: ["192.168.1.1", "pool.ntp.org"], + }, + { + CONF_PLATFORM: CONF_SNTP, + CONF_ID: ID("sntp_time_2", is_manual=False), + CONF_SERVERS: ["pool.ntp.org", "192.168.1.2"], + }, + ], + 1, + ["192.168.1.1", "pool.ntp.org", "192.168.1.2"], + ["Found and merged 2 SNTP time configurations into one instance"], + id="deduplication_preserves_order", + ), + pytest.param( + [ + { + CONF_PLATFORM: CONF_SNTP, + CONF_ID: ID("sntp_time_1", is_manual=False), + CONF_SERVERS: ["192.168.1.1", "pool.ntp.org"], + }, + { + CONF_PLATFORM: CONF_SNTP, + CONF_ID: ID("sntp_time_2", is_manual=False), + CONF_SERVERS: ["192.168.1.2", "pool2.ntp.org"], + }, + { + CONF_PLATFORM: CONF_SNTP, + CONF_ID: ID("sntp_time_3", is_manual=False), + CONF_SERVERS: ["pool3.ntp.org"], + }, + ], + 1, + ["192.168.1.1", "pool.ntp.org", "192.168.1.2"], + [ + "SNTP supports maximum 3 servers. Dropped excess server(s): ['pool2.ntp.org', 'pool3.ntp.org']", + "Found and merged 3 SNTP time configurations into one instance", + ], + id="three_instances_drops_excess_servers", + ), + pytest.param( + [ + { + CONF_PLATFORM: CONF_SNTP, + CONF_ID: ID("sntp_time_1", is_manual=False), + CONF_SERVERS: [ + "192.168.1.1", + "pool.ntp.org", + "pool.ntp.org", + "192.168.1.1", + ], + }, + { + CONF_PLATFORM: CONF_SNTP, + CONF_ID: ID("sntp_time_2", is_manual=False), + CONF_SERVERS: ["pool.ntp.org", "192.168.1.2"], + }, + ], + 1, + ["192.168.1.1", "pool.ntp.org", "192.168.1.2"], + ["Found and merged 2 SNTP time configurations into one instance"], + id="deduplication_multiple_duplicates", + ), + ], +) +def test_sntp_instance_merging( + time_configs: list[dict[str, Any]], + expected_count: int, + expected_servers: list[str], + warning_messages: list[str], + caplog: pytest.LogCaptureFixture, +) -> None: + """Test SNTP instance merging behavior.""" + # Create a mock full config with time configs + full_conf = {CONF_TIME: time_configs.copy()} + + # Set the context var + token = fv.full_config.set(full_conf) + try: + with caplog.at_level(logging.WARNING): + _sntp_final_validate({}) + + # Get the updated config + updated_conf = fv.full_config.get() + + # Check if merging occurred + if len(time_configs) > 1: + # Verify only one SNTP instance remains + sntp_instances = [ + tc + for tc in updated_conf[CONF_TIME] + if tc.get(CONF_PLATFORM) == CONF_SNTP + ] + assert len(sntp_instances) == expected_count + + # Verify server list + assert sntp_instances[0][CONF_SERVERS] == expected_servers + + # Verify warnings + for expected_msg in warning_messages: + assert any( + expected_msg in record.message for record in caplog.records + ), f"Expected warning message '{expected_msg}' not found in log" + else: + # Single instance should not trigger merging or warnings + assert len(caplog.records) == 0 + # Config should be unchanged + assert updated_conf[CONF_TIME] == time_configs + finally: + fv.full_config.reset(token) + + +def test_sntp_inconsistent_manual_ids() -> None: + """Test that inconsistent manual IDs raise an error.""" + # Create configs with manual IDs that are inconsistent + time_configs = [ + { + CONF_PLATFORM: CONF_SNTP, + CONF_ID: ID("sntp_time_1", is_manual=True), + CONF_SERVERS: ["192.168.1.1"], + }, + { + CONF_PLATFORM: CONF_SNTP, + CONF_ID: ID("sntp_time_2", is_manual=True), + CONF_SERVERS: ["192.168.1.2"], + }, + ] + + full_conf = {CONF_TIME: time_configs} + + token = fv.full_config.set(full_conf) + try: + with pytest.raises( + cv.Invalid, + match="Found multiple SNTP configurations but id is inconsistent", + ): + _sntp_final_validate({}) + finally: + fv.full_config.reset(token) + + +def test_sntp_with_other_time_platforms(caplog: pytest.LogCaptureFixture) -> None: + """Test that SNTP merging doesn't affect other time platforms.""" + time_configs = [ + { + CONF_PLATFORM: CONF_SNTP, + CONF_ID: ID("sntp_time_1", is_manual=False), + CONF_SERVERS: ["192.168.1.1"], + }, + { + CONF_PLATFORM: "homeassistant", + CONF_ID: ID("homeassistant_time", is_manual=False), + }, + { + CONF_PLATFORM: CONF_SNTP, + CONF_ID: ID("sntp_time_2", is_manual=False), + CONF_SERVERS: ["192.168.1.2"], + }, + ] + + full_conf = {CONF_TIME: time_configs.copy()} + + token = fv.full_config.set(full_conf) + try: + with caplog.at_level(logging.WARNING): + _sntp_final_validate({}) + + updated_conf = fv.full_config.get() + + # Should have 2 time platforms: 1 merged SNTP + 1 homeassistant + assert len(updated_conf[CONF_TIME]) == 2 + + # Find the platforms + platforms = {tc[CONF_PLATFORM] for tc in updated_conf[CONF_TIME]} + assert platforms == {CONF_SNTP, "homeassistant"} + + # Verify SNTP was merged + sntp_instances = [ + tc for tc in updated_conf[CONF_TIME] if tc[CONF_PLATFORM] == CONF_SNTP + ] + assert len(sntp_instances) == 1 + assert sntp_instances[0][CONF_SERVERS] == ["192.168.1.1", "192.168.1.2"] + finally: + fv.full_config.reset(token) From 96ee38759d7f5a6dd0e9ea7ca4331007ef49e936 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 16 Nov 2025 18:38:52 -0600 Subject: [PATCH 040/415] [web_server.ota] Merge multiple instances to prevent undefined behavior (#11905) --- esphome/components/web_server/ota/__init__.py | 58 ++++++- .../ota/test_web_server_ota.py | 153 ++++++++++++++++++ 2 files changed, 210 insertions(+), 1 deletion(-) diff --git a/esphome/components/web_server/ota/__init__.py b/esphome/components/web_server/ota/__init__.py index 4a98db8877..260e6aea6d 100644 --- a/esphome/components/web_server/ota/__init__.py +++ b/esphome/components/web_server/ota/__init__.py @@ -1,10 +1,17 @@ +import logging + import esphome.codegen as cg from esphome.components.esp32 import add_idf_component from esphome.components.ota import BASE_OTA_SCHEMA, OTAComponent, ota_to_code +from esphome.config_helpers import merge_config import esphome.config_validation as cv -from esphome.const import CONF_ID +from esphome.const import CONF_ID, CONF_OTA, CONF_PLATFORM, CONF_WEB_SERVER from esphome.core import CORE, coroutine_with_priority from esphome.coroutine import CoroPriority +import esphome.final_validate as fv +from esphome.types import ConfigType + +_LOGGER = logging.getLogger(__name__) CODEOWNERS = ["@esphome/core"] DEPENDENCIES = ["network", "web_server_base"] @@ -12,6 +19,53 @@ DEPENDENCIES = ["network", "web_server_base"] web_server_ns = cg.esphome_ns.namespace("web_server") WebServerOTAComponent = web_server_ns.class_("WebServerOTAComponent", OTAComponent) + +def _web_server_ota_final_validate(config: ConfigType) -> None: + """Merge multiple web_server OTA instances into one. + + Multiple web_server OTA instances register duplicate HTTP handlers for /update, + causing undefined behavior. Merge them into a single instance. + """ + full_conf = fv.full_config.get() + ota_confs = full_conf.get(CONF_OTA, []) + + web_server_ota_configs: list[ConfigType] = [] + other_ota_configs: list[ConfigType] = [] + + for ota_conf in ota_confs: + if ota_conf.get(CONF_PLATFORM) == CONF_WEB_SERVER: + web_server_ota_configs.append(ota_conf) + else: + other_ota_configs.append(ota_conf) + + if len(web_server_ota_configs) <= 1: + return + + # Merge all web_server OTA configs into the first one + merged = web_server_ota_configs[0] + for ota_conf in web_server_ota_configs[1:]: + # Validate that IDs are consistent if manually specified + if ( + merged[CONF_ID].is_manual + and ota_conf[CONF_ID].is_manual + and merged[CONF_ID] != ota_conf[CONF_ID] + ): + raise cv.Invalid( + f"Found multiple web_server OTA configurations but {CONF_ID} is inconsistent" + ) + merged = merge_config(merged, ota_conf) + + _LOGGER.warning( + "Found and merged %d web_server OTA configurations into one instance", + len(web_server_ota_configs), + ) + + # Replace OTA configs with merged web_server + other OTA platforms + other_ota_configs.append(merged) + full_conf[CONF_OTA] = other_ota_configs + fv.full_config.set(full_conf) + + CONFIG_SCHEMA = ( cv.Schema( { @@ -22,6 +76,8 @@ CONFIG_SCHEMA = ( .extend(cv.COMPONENT_SCHEMA) ) +FINAL_VALIDATE_SCHEMA = _web_server_ota_final_validate + @coroutine_with_priority(CoroPriority.WEB_SERVER_OTA) async def to_code(config): diff --git a/tests/component_tests/ota/test_web_server_ota.py b/tests/component_tests/ota/test_web_server_ota.py index 0d8ff6f134..794eaac9be 100644 --- a/tests/component_tests/ota/test_web_server_ota.py +++ b/tests/component_tests/ota/test_web_server_ota.py @@ -1,6 +1,18 @@ """Tests for the web_server OTA platform.""" +from __future__ import annotations + from collections.abc import Callable +import logging +from typing import Any + +import pytest + +from esphome import config_validation as cv +from esphome.components.web_server.ota import _web_server_ota_final_validate +from esphome.const import CONF_ID, CONF_OTA, CONF_PLATFORM, CONF_WEB_SERVER +from esphome.core import ID +import esphome.final_validate as fv def test_web_server_ota_generated(generate_main: Callable[[str], str]) -> None: @@ -100,3 +112,144 @@ def test_web_server_ota_esp8266(generate_main: Callable[[str], str]) -> None: # Check web server OTA component is present assert "WebServerOTAComponent" in main_cpp assert "web_server::WebServerOTAComponent" in main_cpp + + +@pytest.mark.parametrize( + ("ota_configs", "expected_count", "warning_expected"), + [ + pytest.param( + [ + { + CONF_PLATFORM: CONF_WEB_SERVER, + CONF_ID: ID("ota_web", is_manual=False), + } + ], + 1, + False, + id="single_instance_no_merge", + ), + pytest.param( + [ + { + CONF_PLATFORM: CONF_WEB_SERVER, + CONF_ID: ID("ota_web_1", is_manual=False), + }, + { + CONF_PLATFORM: CONF_WEB_SERVER, + CONF_ID: ID("ota_web_2", is_manual=False), + }, + ], + 1, + True, + id="two_instances_merged", + ), + pytest.param( + [ + { + CONF_PLATFORM: CONF_WEB_SERVER, + CONF_ID: ID("ota_web_1", is_manual=False), + }, + { + CONF_PLATFORM: "esphome", + CONF_ID: ID("ota_esphome", is_manual=False), + }, + { + CONF_PLATFORM: CONF_WEB_SERVER, + CONF_ID: ID("ota_web_2", is_manual=False), + }, + ], + 2, + True, + id="mixed_platforms_web_server_merged", + ), + ], +) +def test_web_server_ota_instance_merging( + ota_configs: list[dict[str, Any]], + expected_count: int, + warning_expected: bool, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test web_server OTA instance merging behavior.""" + full_conf = {CONF_OTA: ota_configs.copy()} + + token = fv.full_config.set(full_conf) + try: + with caplog.at_level(logging.WARNING): + _web_server_ota_final_validate({}) + + updated_conf = fv.full_config.get() + + # Verify total number of OTA platforms + assert len(updated_conf[CONF_OTA]) == expected_count + + # Verify warning + if warning_expected: + assert any( + "Found and merged" in record.message + and "web_server OTA" in record.message + for record in caplog.records + ), "Expected merge warning not found in log" + else: + assert len(caplog.records) == 0, "Unexpected warnings logged" + finally: + fv.full_config.reset(token) + + +def test_web_server_ota_consistent_manual_ids( + caplog: pytest.LogCaptureFixture, +) -> None: + """Test that consistent manual IDs can be merged successfully.""" + ota_configs = [ + { + CONF_PLATFORM: CONF_WEB_SERVER, + CONF_ID: ID("ota_web", is_manual=True), + }, + { + CONF_PLATFORM: CONF_WEB_SERVER, + CONF_ID: ID("ota_web", is_manual=True), + }, + ] + + full_conf = {CONF_OTA: ota_configs} + + token = fv.full_config.set(full_conf) + try: + with caplog.at_level(logging.WARNING): + _web_server_ota_final_validate({}) + + updated_conf = fv.full_config.get() + assert len(updated_conf[CONF_OTA]) == 1 + assert updated_conf[CONF_OTA][0][CONF_ID].id == "ota_web" + assert any( + "Found and merged" in record.message and "web_server OTA" in record.message + for record in caplog.records + ) + finally: + fv.full_config.reset(token) + + +def test_web_server_ota_inconsistent_manual_ids() -> None: + """Test that inconsistent manual IDs raise an error.""" + ota_configs = [ + { + CONF_PLATFORM: CONF_WEB_SERVER, + CONF_ID: ID("ota_web_1", is_manual=True), + }, + { + CONF_PLATFORM: CONF_WEB_SERVER, + CONF_ID: ID("ota_web_2", is_manual=True), + }, + ] + + full_conf = {CONF_OTA: ota_configs} + + token = fv.full_config.set(full_conf) + try: + with pytest.raises( + cv.Invalid, + match="Found multiple web_server OTA configurations but id is inconsistent", + ): + _web_server_ota_final_validate({}) + finally: + fv.full_config.reset(token) From 3b860e784c462ee07c74b79a0393d29f635d876d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 16 Nov 2025 18:39:01 -0600 Subject: [PATCH 041/415] [web_server_idf] Fix lwIP assertion crash by shutting down sockets on connection close (#11937) --- .../components/web_server_idf/web_server_idf.cpp | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/esphome/components/web_server_idf/web_server_idf.cpp b/esphome/components/web_server_idf/web_server_idf.cpp index 0dab5e7e8c..ce91569de2 100644 --- a/esphome/components/web_server_idf/web_server_idf.cpp +++ b/esphome/components/web_server_idf/web_server_idf.cpp @@ -489,10 +489,18 @@ AsyncEventSourceResponse::AsyncEventSourceResponse(const AsyncWebServerRequest * void AsyncEventSourceResponse::destroy(void *ptr) { auto *rsp = static_cast(ptr); - ESP_LOGD(TAG, "Event source connection closed (fd: %d)", rsp->fd_.load()); - // Mark as dead by setting fd to 0 - will be cleaned up in the main loop - rsp->fd_.store(0); - // Note: We don't delete or remove from set here to avoid race conditions + int fd = rsp->fd_.exchange(0); // Atomically get and clear fd + + if (fd > 0) { + ESP_LOGD(TAG, "Event source connection closed (fd: %d)", fd); + // Immediately shut down the socket to prevent lwIP from delivering more data + // This prevents "recv_tcp: recv for wrong pcb!" assertions when the TCP stack + // tries to deliver queued data after the session is marked as dead + // See: https://github.com/esphome/esphome/issues/11936 + shutdown(fd, SHUT_RDWR); + // Note: We don't close() the socket - httpd owns it and will close it + } + // Session will be cleaned up in the main loop to avoid race conditions } // helper for allowing only unique entries in the queue From aa097a2fe6ba533a23b1598d44290c677d03ef04 Mon Sep 17 00:00:00 2001 From: Anton Sergunov Date: Mon, 17 Nov 2025 07:25:00 +0600 Subject: [PATCH 042/415] [uart] Setup uart pins only if flags are set (#11914) Co-authored-by: J. Nick Koston --- .../components/uart/uart_component_esp8266.cpp | 18 +++++++++++++----- .../components/uart/uart_component_esp_idf.cpp | 18 +++++++++++++----- .../uart/uart_component_libretiny.cpp | 2 +- .../components/uart/uart_component_rp2040.cpp | 18 +++++++++++++----- 4 files changed, 40 insertions(+), 16 deletions(-) diff --git a/esphome/components/uart/uart_component_esp8266.cpp b/esphome/components/uart/uart_component_esp8266.cpp index 7a453dbb50..c84a877ef4 100644 --- a/esphome/components/uart/uart_component_esp8266.cpp +++ b/esphome/components/uart/uart_component_esp8266.cpp @@ -56,11 +56,19 @@ uint32_t ESP8266UartComponent::get_config() { } void ESP8266UartComponent::setup() { - if (this->rx_pin_) { - this->rx_pin_->setup(); - } - if (this->tx_pin_ && this->rx_pin_ != this->tx_pin_) { - this->tx_pin_->setup(); + auto setup_pin_if_needed = [](InternalGPIOPin *pin) { + if (!pin) { + return; + } + const auto mask = gpio::Flags::FLAG_OPEN_DRAIN | gpio::Flags::FLAG_PULLUP | gpio::Flags::FLAG_PULLDOWN; + if ((pin->get_flags() & mask) != gpio::Flags::FLAG_NONE) { + pin->setup(); + } + }; + + setup_pin_if_needed(this->rx_pin_); + if (this->rx_pin_ != this->tx_pin_) { + setup_pin_if_needed(this->tx_pin_); } // Use Arduino HardwareSerial UARTs if all used pins match the ones diff --git a/esphome/components/uart/uart_component_esp_idf.cpp b/esphome/components/uart/uart_component_esp_idf.cpp index 70a13c9e37..61ca8c1c0c 100644 --- a/esphome/components/uart/uart_component_esp_idf.cpp +++ b/esphome/components/uart/uart_component_esp_idf.cpp @@ -133,11 +133,19 @@ void IDFUARTComponent::load_settings(bool dump_config) { return; } - if (this->rx_pin_) { - this->rx_pin_->setup(); - } - if (this->tx_pin_ && this->rx_pin_ != this->tx_pin_) { - this->tx_pin_->setup(); + auto setup_pin_if_needed = [](InternalGPIOPin *pin) { + if (!pin) { + return; + } + const auto mask = gpio::Flags::FLAG_OPEN_DRAIN | gpio::Flags::FLAG_PULLUP | gpio::Flags::FLAG_PULLDOWN; + if ((pin->get_flags() & mask) != gpio::Flags::FLAG_NONE) { + pin->setup(); + } + }; + + setup_pin_if_needed(this->rx_pin_); + if (this->rx_pin_ != this->tx_pin_) { + setup_pin_if_needed(this->tx_pin_); } int8_t tx = this->tx_pin_ != nullptr ? this->tx_pin_->get_pin() : -1; diff --git a/esphome/components/uart/uart_component_libretiny.cpp b/esphome/components/uart/uart_component_libretiny.cpp index 8d1d28fce4..1e408b169b 100644 --- a/esphome/components/uart/uart_component_libretiny.cpp +++ b/esphome/components/uart/uart_component_libretiny.cpp @@ -53,7 +53,7 @@ void LibreTinyUARTComponent::setup() { auto shouldFallbackToSoftwareSerial = [&]() -> bool { auto hasFlags = [](InternalGPIOPin *pin, const gpio::Flags mask) -> bool { - return pin && pin->get_flags() & mask != gpio::Flags::FLAG_NONE; + return pin && (pin->get_flags() & mask) != gpio::Flags::FLAG_NONE; }; if (hasFlags(this->tx_pin_, gpio::Flags::FLAG_OPEN_DRAIN | gpio::Flags::FLAG_PULLUP | gpio::Flags::FLAG_PULLDOWN) || hasFlags(this->rx_pin_, gpio::Flags::FLAG_OPEN_DRAIN | gpio::Flags::FLAG_PULLUP | gpio::Flags::FLAG_PULLDOWN)) { diff --git a/esphome/components/uart/uart_component_rp2040.cpp b/esphome/components/uart/uart_component_rp2040.cpp index c78691653d..cd3905b5c1 100644 --- a/esphome/components/uart/uart_component_rp2040.cpp +++ b/esphome/components/uart/uart_component_rp2040.cpp @@ -52,11 +52,19 @@ uint16_t RP2040UartComponent::get_config() { } void RP2040UartComponent::setup() { - if (this->rx_pin_) { - this->rx_pin_->setup(); - } - if (this->tx_pin_ && this->rx_pin_ != this->tx_pin_) { - this->tx_pin_->setup(); + auto setup_pin_if_needed = [](InternalGPIOPin *pin) { + if (!pin) { + return; + } + const auto mask = gpio::Flags::FLAG_OPEN_DRAIN | gpio::Flags::FLAG_PULLUP | gpio::Flags::FLAG_PULLDOWN; + if ((pin->get_flags() & mask) != gpio::Flags::FLAG_NONE) { + pin->setup(); + } + }; + + setup_pin_if_needed(this->rx_pin_); + if (this->rx_pin_ != this->tx_pin_) { + setup_pin_if_needed(this->tx_pin_); } uint16_t config = get_config(); From aed80732f965ec9dc93fb1bde0dc5d9e603f866e Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Fri, 14 Nov 2025 00:55:48 +1000 Subject: [PATCH 043/415] [esp32] Make esp-idf default framework for P4 (#11884) --- esphome/components/esp32/__init__.py | 163 ++++++++++++--------------- 1 file changed, 74 insertions(+), 89 deletions(-) diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index 61511cba0c..9741dc76a1 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -381,8 +381,9 @@ PLATFORM_VERSION_LOOKUP = { } -def _check_versions(value): - value = value.copy() +def _check_versions(config): + config = config.copy() + value = config[CONF_FRAMEWORK] if value[CONF_VERSION] in PLATFORM_VERSION_LOOKUP: if CONF_SOURCE in value or CONF_PLATFORM_VERSION in value: @@ -447,7 +448,7 @@ def _check_versions(value): "If there are connectivity or build issues please remove the manual version." ) - return value + return config def _parse_platform_version(value): @@ -598,89 +599,72 @@ def _validate_idf_component(config: ConfigType) -> ConfigType: FRAMEWORK_ESP_IDF = "esp-idf" FRAMEWORK_ARDUINO = "arduino" -FRAMEWORK_SCHEMA = cv.All( - cv.Schema( - { - cv.Optional(CONF_TYPE, default=FRAMEWORK_ARDUINO): cv.one_of( - FRAMEWORK_ESP_IDF, FRAMEWORK_ARDUINO - ), - cv.Optional(CONF_VERSION, default="recommended"): cv.string_strict, - cv.Optional(CONF_RELEASE): cv.string_strict, - cv.Optional(CONF_SOURCE): cv.string_strict, - cv.Optional(CONF_PLATFORM_VERSION): _parse_platform_version, - cv.Optional(CONF_SDKCONFIG_OPTIONS, default={}): { - cv.string_strict: cv.string_strict - }, - cv.Optional(CONF_LOG_LEVEL, default="ERROR"): cv.one_of( - *LOG_LEVELS_IDF, upper=True - ), - cv.Optional(CONF_ADVANCED, default={}): cv.Schema( - { - cv.Optional(CONF_ASSERTION_LEVEL): cv.one_of( - *ASSERTION_LEVELS, upper=True - ), - cv.Optional(CONF_COMPILER_OPTIMIZATION, default="SIZE"): cv.one_of( - *COMPILER_OPTIMIZATIONS, upper=True - ), - cv.Optional(CONF_ENABLE_IDF_EXPERIMENTAL_FEATURES): cv.boolean, - cv.Optional(CONF_ENABLE_LWIP_ASSERT, default=True): cv.boolean, - cv.Optional( - CONF_IGNORE_EFUSE_CUSTOM_MAC, default=False - ): cv.boolean, - cv.Optional(CONF_IGNORE_EFUSE_MAC_CRC): cv.boolean, - # DHCP server is needed for WiFi AP mode. When WiFi component is used, - # it will handle disabling DHCP server when AP is not configured. - # Default to false (disabled) when WiFi is not used. - cv.OnlyWithout( - CONF_ENABLE_LWIP_DHCP_SERVER, "wifi", default=False - ): cv.boolean, - cv.Optional( - CONF_ENABLE_LWIP_MDNS_QUERIES, default=True - ): cv.boolean, - cv.Optional( - CONF_ENABLE_LWIP_BRIDGE_INTERFACE, default=False - ): cv.boolean, - cv.Optional( - CONF_ENABLE_LWIP_TCPIP_CORE_LOCKING, default=True - ): cv.boolean, - cv.Optional( - CONF_ENABLE_LWIP_CHECK_THREAD_SAFETY, default=True - ): cv.boolean, - cv.Optional( - CONF_DISABLE_LIBC_LOCKS_IN_IRAM, default=True - ): cv.boolean, - cv.Optional( - CONF_DISABLE_VFS_SUPPORT_TERMIOS, default=True - ): cv.boolean, - cv.Optional( - CONF_DISABLE_VFS_SUPPORT_SELECT, default=True - ): cv.boolean, - cv.Optional(CONF_DISABLE_VFS_SUPPORT_DIR, default=True): cv.boolean, - cv.Optional(CONF_EXECUTE_FROM_PSRAM): cv.boolean, - cv.Optional(CONF_LOOP_TASK_STACK_SIZE, default=8192): cv.int_range( - min=8192, max=32768 - ), - } - ), - cv.Optional(CONF_COMPONENTS, default=[]): cv.ensure_list( - cv.All( - cv.Schema( - { - cv.Required(CONF_NAME): cv.string_strict, - cv.Optional(CONF_SOURCE): cv.git_ref, - cv.Optional(CONF_REF): cv.string, - cv.Optional(CONF_PATH): cv.string, - cv.Optional(CONF_REFRESH): cv.All( - cv.string, cv.source_refresh - ), - } - ), - _validate_idf_component, - ) - ), - } - ), - _check_versions, +FRAMEWORK_SCHEMA = cv.Schema( + { + cv.Optional(CONF_TYPE): cv.one_of(FRAMEWORK_ESP_IDF, FRAMEWORK_ARDUINO), + cv.Optional(CONF_VERSION, default="recommended"): cv.string_strict, + cv.Optional(CONF_RELEASE): cv.string_strict, + cv.Optional(CONF_SOURCE): cv.string_strict, + cv.Optional(CONF_PLATFORM_VERSION): _parse_platform_version, + cv.Optional(CONF_SDKCONFIG_OPTIONS, default={}): { + cv.string_strict: cv.string_strict + }, + cv.Optional(CONF_LOG_LEVEL, default="ERROR"): cv.one_of( + *LOG_LEVELS_IDF, upper=True + ), + cv.Optional(CONF_ADVANCED, default={}): cv.Schema( + { + cv.Optional(CONF_ASSERTION_LEVEL): cv.one_of( + *ASSERTION_LEVELS, upper=True + ), + cv.Optional(CONF_COMPILER_OPTIMIZATION, default="SIZE"): cv.one_of( + *COMPILER_OPTIMIZATIONS, upper=True + ), + cv.Optional(CONF_ENABLE_IDF_EXPERIMENTAL_FEATURES): cv.boolean, + cv.Optional(CONF_ENABLE_LWIP_ASSERT, default=True): cv.boolean, + cv.Optional(CONF_IGNORE_EFUSE_CUSTOM_MAC, default=False): cv.boolean, + cv.Optional(CONF_IGNORE_EFUSE_MAC_CRC): cv.boolean, + # DHCP server is needed for WiFi AP mode. When WiFi component is used, + # it will handle disabling DHCP server when AP is not configured. + # Default to false (disabled) when WiFi is not used. + cv.OnlyWithout( + CONF_ENABLE_LWIP_DHCP_SERVER, "wifi", default=False + ): cv.boolean, + cv.Optional(CONF_ENABLE_LWIP_MDNS_QUERIES, default=True): cv.boolean, + cv.Optional( + CONF_ENABLE_LWIP_BRIDGE_INTERFACE, default=False + ): cv.boolean, + cv.Optional( + CONF_ENABLE_LWIP_TCPIP_CORE_LOCKING, default=True + ): cv.boolean, + cv.Optional( + CONF_ENABLE_LWIP_CHECK_THREAD_SAFETY, default=True + ): cv.boolean, + cv.Optional(CONF_DISABLE_LIBC_LOCKS_IN_IRAM, default=True): cv.boolean, + cv.Optional(CONF_DISABLE_VFS_SUPPORT_TERMIOS, default=True): cv.boolean, + cv.Optional(CONF_DISABLE_VFS_SUPPORT_SELECT, default=True): cv.boolean, + cv.Optional(CONF_DISABLE_VFS_SUPPORT_DIR, default=True): cv.boolean, + cv.Optional(CONF_EXECUTE_FROM_PSRAM): cv.boolean, + cv.Optional(CONF_LOOP_TASK_STACK_SIZE, default=8192): cv.int_range( + min=8192, max=32768 + ), + } + ), + cv.Optional(CONF_COMPONENTS, default=[]): cv.ensure_list( + cv.All( + cv.Schema( + { + cv.Required(CONF_NAME): cv.string_strict, + cv.Optional(CONF_SOURCE): cv.git_ref, + cv.Optional(CONF_REF): cv.string, + cv.Optional(CONF_PATH): cv.string, + cv.Optional(CONF_REFRESH): cv.All(cv.string, cv.source_refresh), + } + ), + _validate_idf_component, + ) + ), + } ) @@ -743,11 +727,11 @@ def _show_framework_migration_message(name: str, variant: str) -> None: def _set_default_framework(config): + config = config.copy() if CONF_FRAMEWORK not in config: - config = config.copy() - - variant = config[CONF_VARIANT] config[CONF_FRAMEWORK] = FRAMEWORK_SCHEMA({}) + if CONF_TYPE not in config[CONF_FRAMEWORK]: + variant = config[CONF_VARIANT] if variant in ARDUINO_ALLOWED_VARIANTS: config[CONF_FRAMEWORK][CONF_TYPE] = FRAMEWORK_ARDUINO _show_framework_migration_message( @@ -787,6 +771,7 @@ CONFIG_SCHEMA = cv.All( ), _detect_variant, _set_default_framework, + _check_versions, set_core_data, cv.has_at_least_one_key(CONF_BOARD, CONF_VARIANT), ) From fe00e209fff4f9c2e3e3b4c4664a300503488e94 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Fri, 14 Nov 2025 01:52:08 +1000 Subject: [PATCH 044/415] [esp32] Add sdkconfig flag to make OTA work for 32MB flash (#11883) Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> --- esphome/components/esp32/__init__.py | 81 +++++++++---------- tests/components/esp32/test.esp32-p4-idf.yaml | 27 +++++++ 2 files changed, 67 insertions(+), 41 deletions(-) create mode 100644 tests/components/esp32/test.esp32-p4-idf.yaml diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index 9741dc76a1..0f85e585f7 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -498,6 +498,8 @@ def final_validate(config): from esphome.components.psram import DOMAIN as PSRAM_DOMAIN errs = [] + conf_fw = config[CONF_FRAMEWORK] + advanced = conf_fw[CONF_ADVANCED] full_config = fv.full_config.get() if pio_options := full_config[CONF_ESPHOME].get(CONF_PLATFORMIO_OPTIONS): pio_flash_size_key = "board_upload.flash_size" @@ -514,22 +516,14 @@ def final_validate(config): f"Please specify {CONF_FLASH_SIZE} within esp32 configuration only" ) ) - if ( - config[CONF_VARIANT] != VARIANT_ESP32 - and CONF_ADVANCED in (conf_fw := config[CONF_FRAMEWORK]) - and CONF_IGNORE_EFUSE_MAC_CRC in conf_fw[CONF_ADVANCED] - ): + if config[CONF_VARIANT] != VARIANT_ESP32 and advanced[CONF_IGNORE_EFUSE_MAC_CRC]: errs.append( cv.Invalid( f"'{CONF_IGNORE_EFUSE_MAC_CRC}' is not supported on {config[CONF_VARIANT]}", path=[CONF_FRAMEWORK, CONF_ADVANCED, CONF_IGNORE_EFUSE_MAC_CRC], ) ) - if ( - config.get(CONF_FRAMEWORK, {}) - .get(CONF_ADVANCED, {}) - .get(CONF_EXECUTE_FROM_PSRAM) - ): + if advanced[CONF_EXECUTE_FROM_PSRAM]: if config[CONF_VARIANT] != VARIANT_ESP32S3: errs.append( cv.Invalid( @@ -545,6 +539,17 @@ def final_validate(config): ) ) + if ( + config[CONF_FLASH_SIZE] == "32MB" + and "ota" in full_config + and not advanced[CONF_ENABLE_IDF_EXPERIMENTAL_FEATURES] + ): + errs.append( + cv.Invalid( + f"OTA with 32MB flash requires '{CONF_ENABLE_IDF_EXPERIMENTAL_FEATURES}' to be set in the '{CONF_ADVANCED}' section of the esp32 configuration", + path=[CONF_FLASH_SIZE], + ) + ) if errs: raise cv.MultipleInvalid(errs) @@ -620,10 +625,12 @@ FRAMEWORK_SCHEMA = cv.Schema( cv.Optional(CONF_COMPILER_OPTIMIZATION, default="SIZE"): cv.one_of( *COMPILER_OPTIMIZATIONS, upper=True ), - cv.Optional(CONF_ENABLE_IDF_EXPERIMENTAL_FEATURES): cv.boolean, + cv.Optional( + CONF_ENABLE_IDF_EXPERIMENTAL_FEATURES, default=False + ): cv.boolean, cv.Optional(CONF_ENABLE_LWIP_ASSERT, default=True): cv.boolean, cv.Optional(CONF_IGNORE_EFUSE_CUSTOM_MAC, default=False): cv.boolean, - cv.Optional(CONF_IGNORE_EFUSE_MAC_CRC): cv.boolean, + cv.Optional(CONF_IGNORE_EFUSE_MAC_CRC, default=False): cv.boolean, # DHCP server is needed for WiFi AP mode. When WiFi component is used, # it will handle disabling DHCP server when AP is not configured. # Default to false (disabled) when WiFi is not used. @@ -644,7 +651,7 @@ FRAMEWORK_SCHEMA = cv.Schema( cv.Optional(CONF_DISABLE_VFS_SUPPORT_TERMIOS, default=True): cv.boolean, cv.Optional(CONF_DISABLE_VFS_SUPPORT_SELECT, default=True): cv.boolean, cv.Optional(CONF_DISABLE_VFS_SUPPORT_DIR, default=True): cv.boolean, - cv.Optional(CONF_EXECUTE_FROM_PSRAM): cv.boolean, + cv.Optional(CONF_EXECUTE_FROM_PSRAM, default=False): cv.boolean, cv.Optional(CONF_LOOP_TASK_STACK_SIZE, default=8192): cv.int_range( min=8192, max=32768 ), @@ -790,9 +797,7 @@ def _configure_lwip_max_sockets(conf: dict) -> None: from esphome.components.socket import KEY_SOCKET_CONSUMERS # Check if user manually specified CONFIG_LWIP_MAX_SOCKETS - user_max_sockets = conf.get(CONF_SDKCONFIG_OPTIONS, {}).get( - "CONFIG_LWIP_MAX_SOCKETS" - ) + user_max_sockets = conf[CONF_SDKCONFIG_OPTIONS].get("CONFIG_LWIP_MAX_SOCKETS") socket_consumers: dict[str, int] = CORE.data.get(KEY_SOCKET_CONSUMERS, {}) total_sockets = sum(socket_consumers.values()) @@ -962,23 +967,18 @@ async def to_code(config): # WiFi component handles its own optimization when AP mode is not used # When using Arduino with Ethernet, DHCP server functions must be available # for the Network library to compile, even if not actively used - if ( - CONF_ENABLE_LWIP_DHCP_SERVER in advanced - and not advanced[CONF_ENABLE_LWIP_DHCP_SERVER] - and not ( - conf[CONF_TYPE] == FRAMEWORK_ARDUINO - and "ethernet" in CORE.loaded_integrations - ) + if advanced.get(CONF_ENABLE_LWIP_DHCP_SERVER) is False and not ( + conf[CONF_TYPE] == FRAMEWORK_ARDUINO and "ethernet" in CORE.loaded_integrations ): add_idf_sdkconfig_option("CONFIG_LWIP_DHCPS", False) - if not advanced.get(CONF_ENABLE_LWIP_MDNS_QUERIES, True): + if not advanced[CONF_ENABLE_LWIP_MDNS_QUERIES]: add_idf_sdkconfig_option("CONFIG_LWIP_DNS_SUPPORT_MDNS_QUERIES", False) - if not advanced.get(CONF_ENABLE_LWIP_BRIDGE_INTERFACE, False): + if not advanced[CONF_ENABLE_LWIP_BRIDGE_INTERFACE]: add_idf_sdkconfig_option("CONFIG_LWIP_BRIDGEIF_MAX_PORTS", 0) _configure_lwip_max_sockets(conf) - if advanced.get(CONF_EXECUTE_FROM_PSRAM, False): + if advanced[CONF_EXECUTE_FROM_PSRAM]: add_idf_sdkconfig_option("CONFIG_SPIRAM_FETCH_INSTRUCTIONS", True) add_idf_sdkconfig_option("CONFIG_SPIRAM_RODATA", True) @@ -989,23 +989,22 @@ async def to_code(config): # - select() on 4 sockets: ~190μs (Arduino/core locking) vs ~235μs (ESP-IDF default) # - Up to 200% slower under load when all operations queue through tcpip_thread # Enabling this makes ESP-IDF socket performance match Arduino framework. - if advanced.get(CONF_ENABLE_LWIP_TCPIP_CORE_LOCKING, True): + if advanced[CONF_ENABLE_LWIP_TCPIP_CORE_LOCKING]: add_idf_sdkconfig_option("CONFIG_LWIP_TCPIP_CORE_LOCKING", True) - if advanced.get(CONF_ENABLE_LWIP_CHECK_THREAD_SAFETY, True): + if advanced[CONF_ENABLE_LWIP_CHECK_THREAD_SAFETY]: add_idf_sdkconfig_option("CONFIG_LWIP_CHECK_THREAD_SAFETY", True) # Disable placing libc locks in IRAM to save RAM # This is safe for ESPHome since no IRAM ISRs (interrupts that run while cache is disabled) # use libc lock APIs. Saves approximately 1.3KB (1,356 bytes) of IRAM. - if advanced.get(CONF_DISABLE_LIBC_LOCKS_IN_IRAM, True): + if advanced[CONF_DISABLE_LIBC_LOCKS_IN_IRAM]: add_idf_sdkconfig_option("CONFIG_LIBC_LOCKS_PLACE_IN_IRAM", False) # Disable VFS support for termios (terminal I/O functions) # ESPHome doesn't use termios functions on ESP32 (only used in host UART driver). # Saves approximately 1.8KB of flash when disabled (default). add_idf_sdkconfig_option( - "CONFIG_VFS_SUPPORT_TERMIOS", - not advanced.get(CONF_DISABLE_VFS_SUPPORT_TERMIOS, True), + "CONFIG_VFS_SUPPORT_TERMIOS", not advanced[CONF_DISABLE_VFS_SUPPORT_TERMIOS] ) # Disable VFS support for select() with file descriptors @@ -1019,8 +1018,7 @@ async def to_code(config): else: # No component needs it - allow user to control (default: disabled) add_idf_sdkconfig_option( - "CONFIG_VFS_SUPPORT_SELECT", - not advanced.get(CONF_DISABLE_VFS_SUPPORT_SELECT, True), + "CONFIG_VFS_SUPPORT_SELECT", not advanced[CONF_DISABLE_VFS_SUPPORT_SELECT] ) # Disable VFS support for directory functions (opendir, readdir, mkdir, etc.) @@ -1033,8 +1031,7 @@ async def to_code(config): else: # No component needs it - allow user to control (default: disabled) add_idf_sdkconfig_option( - "CONFIG_VFS_SUPPORT_DIR", - not advanced.get(CONF_DISABLE_VFS_SUPPORT_DIR, True), + "CONFIG_VFS_SUPPORT_DIR", not advanced[CONF_DISABLE_VFS_SUPPORT_DIR] ) cg.add_platformio_option("board_build.partitions", "partitions.csv") @@ -1048,7 +1045,7 @@ async def to_code(config): add_idf_sdkconfig_option(flag, assertion_level == key) add_idf_sdkconfig_option("CONFIG_COMPILER_OPTIMIZATION_DEFAULT", False) - compiler_optimization = advanced.get(CONF_COMPILER_OPTIMIZATION) + compiler_optimization = advanced[CONF_COMPILER_OPTIMIZATION] for key, flag in COMPILER_OPTIMIZATIONS.items(): add_idf_sdkconfig_option(flag, compiler_optimization == key) @@ -1057,18 +1054,20 @@ async def to_code(config): conf[CONF_ADVANCED][CONF_ENABLE_LWIP_ASSERT], ) - if advanced.get(CONF_IGNORE_EFUSE_MAC_CRC): + if advanced[CONF_IGNORE_EFUSE_MAC_CRC]: add_idf_sdkconfig_option("CONFIG_ESP_MAC_IGNORE_MAC_CRC_ERROR", True) add_idf_sdkconfig_option("CONFIG_ESP_PHY_CALIBRATION_AND_DATA_STORAGE", False) - if advanced.get(CONF_ENABLE_IDF_EXPERIMENTAL_FEATURES): + if advanced[CONF_ENABLE_IDF_EXPERIMENTAL_FEATURES]: _LOGGER.warning( "Using experimental features in ESP-IDF may result in unexpected failures." ) add_idf_sdkconfig_option("CONFIG_IDF_EXPERIMENTAL_FEATURES", True) + if config[CONF_FLASH_SIZE] == "32MB": + add_idf_sdkconfig_option( + "CONFIG_BOOTLOADER_CACHE_32BIT_ADDR_QUAD_FLASH", True + ) - cg.add_define( - "ESPHOME_LOOP_TASK_STACK_SIZE", advanced.get(CONF_LOOP_TASK_STACK_SIZE) - ) + cg.add_define("ESPHOME_LOOP_TASK_STACK_SIZE", advanced[CONF_LOOP_TASK_STACK_SIZE]) cg.add_define( "USE_ESP_IDF_VERSION_CODE", diff --git a/tests/components/esp32/test.esp32-p4-idf.yaml b/tests/components/esp32/test.esp32-p4-idf.yaml new file mode 100644 index 0000000000..a4c930f236 --- /dev/null +++ b/tests/components/esp32/test.esp32-p4-idf.yaml @@ -0,0 +1,27 @@ +esp32: + variant: esp32p4 + flash_size: 32MB + cpu_frequency: 400MHz + framework: + type: esp-idf + advanced: + enable_idf_experimental_features: yes + +ota: + platform: esphome + +wifi: + ssid: MySSID + password: password1 + +esp32_hosted: + variant: ESP32C6 + slot: 1 + active_high: true + reset_pin: GPIO15 + cmd_pin: GPIO13 + clk_pin: GPIO12 + d0_pin: GPIO11 + d1_pin: GPIO10 + d2_pin: GPIO9 + d3_pin: GPIO8 From eb54c0026dfcab03fbb841ce5fd374554fa61960 Mon Sep 17 00:00:00 2001 From: Edward Firmo <94725493+edwardtfn@users.noreply.github.com> Date: Thu, 13 Nov 2025 19:08:06 +0100 Subject: [PATCH 045/415] [light] Fix missing `ColorMode::BRIGHTNESS` case in logging (#11836) --- esphome/components/light/light_call.cpp | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/esphome/components/light/light_call.cpp b/esphome/components/light/light_call.cpp index 8365ac77cd..b15ff84b97 100644 --- a/esphome/components/light/light_call.cpp +++ b/esphome/components/light/light_call.cpp @@ -52,8 +52,10 @@ static void log_invalid_parameter(const char *name, const LogString *message) { } static const LogString *color_mode_to_human(ColorMode color_mode) { - if (color_mode == ColorMode::UNKNOWN) - return LOG_STR("Unknown"); + if (color_mode == ColorMode::ON_OFF) + return LOG_STR("On/Off"); + if (color_mode == ColorMode::BRIGHTNESS) + return LOG_STR("Brightness"); if (color_mode == ColorMode::WHITE) return LOG_STR("White"); if (color_mode == ColorMode::COLOR_TEMPERATURE) @@ -68,7 +70,7 @@ static const LogString *color_mode_to_human(ColorMode color_mode) { return LOG_STR("RGB + cold/warm white"); if (color_mode == ColorMode::RGB_COLOR_TEMPERATURE) return LOG_STR("RGB + color temperature"); - return LOG_STR(""); + return LOG_STR("Unknown"); } // Helper to log percentage values From 7ce94c27feeade6a50082ed9f14165c056fb2a71 Mon Sep 17 00:00:00 2001 From: Edward Firmo <94725493+edwardtfn@users.noreply.github.com> Date: Fri, 14 Nov 2025 15:13:48 +0100 Subject: [PATCH 046/415] [wifi] Allow `use_psram` with Arduino (#11902) --- esphome/components/wifi/__init__.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/esphome/components/wifi/__init__.py b/esphome/components/wifi/__init__.py index 4dbb425e4b..11bd7798e2 100644 --- a/esphome/components/wifi/__init__.py +++ b/esphome/components/wifi/__init__.py @@ -12,7 +12,6 @@ from esphome.components.network import ( 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 from esphome.const import ( CONF_AP, CONF_BSSID, @@ -352,7 +351,7 @@ CONFIG_SCHEMA = cv.All( single=True ), cv.Optional(CONF_USE_PSRAM): cv.All( - only_with_esp_idf, cv.requires_component("psram"), cv.boolean + cv.only_on_esp32, cv.requires_component("psram"), cv.boolean ), } ), From 97c4914573b5b7444f44c526e1fb11d13cdcfe38 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Fri, 14 Nov 2025 14:06:08 -0500 Subject: [PATCH 047/415] [uart] Improve error handling and validate buffer size (#11895) Co-authored-by: J. Nick Koston --- esphome/components/uart/__init__.py | 19 ++++++++++ .../uart/uart_component_esp_idf.cpp | 35 +++++++++++++++---- 2 files changed, 47 insertions(+), 7 deletions(-) diff --git a/esphome/components/uart/__init__.py b/esphome/components/uart/__init__.py index cbc11d0db0..7b0d9726b8 100644 --- a/esphome/components/uart/__init__.py +++ b/esphome/components/uart/__init__.py @@ -1,3 +1,4 @@ +from logging import getLogger import math import re @@ -35,6 +36,8 @@ from esphome.core import CORE, ID import esphome.final_validate as fv from esphome.yaml_util import make_data_base +_LOGGER = getLogger(__name__) + CODEOWNERS = ["@esphome/core"] uart_ns = cg.esphome_ns.namespace("uart") UARTComponent = uart_ns.class_("UARTComponent") @@ -130,6 +133,21 @@ def validate_host_config(config): return config +def validate_rx_buffer_size(config): + if CORE.is_esp32: + # ESP32 UART hardware FIFO is 128 bytes (LP UART is 16 bytes, but we use 128 as safe minimum) + # rx_buffer_size must be greater than the hardware FIFO length + min_buffer_size = 128 + if config[CONF_RX_BUFFER_SIZE] <= min_buffer_size: + _LOGGER.warning( + "UART rx_buffer_size (%d bytes) is too small and must be greater than the hardware " + "FIFO size (%d bytes). The buffer size will be automatically adjusted at runtime.", + config[CONF_RX_BUFFER_SIZE], + min_buffer_size, + ) + return config + + def _uart_declare_type(value): if CORE.is_esp8266: return cv.declare_id(ESP8266UartComponent)(value) @@ -247,6 +265,7 @@ CONFIG_SCHEMA = cv.All( ).extend(cv.COMPONENT_SCHEMA), cv.has_at_least_one_key(CONF_TX_PIN, CONF_RX_PIN, CONF_PORT), validate_host_config, + validate_rx_buffer_size, ) diff --git a/esphome/components/uart/uart_component_esp_idf.cpp b/esphome/components/uart/uart_component_esp_idf.cpp index 73813d2d5b..70a13c9e37 100644 --- a/esphome/components/uart/uart_component_esp_idf.cpp +++ b/esphome/components/uart/uart_component_esp_idf.cpp @@ -91,6 +91,16 @@ void IDFUARTComponent::setup() { this->uart_num_ = static_cast(next_uart_num++); this->lock_ = xSemaphoreCreateMutex(); +#if (SOC_UART_LP_NUM >= 1) + size_t fifo_len = ((this->uart_num_ < SOC_UART_HP_NUM) ? SOC_UART_FIFO_LEN : SOC_LP_UART_FIFO_LEN); +#else + size_t fifo_len = SOC_UART_FIFO_LEN; +#endif + if (this->rx_buffer_size_ <= fifo_len) { + ESP_LOGW(TAG, "rx_buffer_size is too small, must be greater than %zu", fifo_len); + this->rx_buffer_size_ = fifo_len * 2; + } + xSemaphoreTake(this->lock_, portMAX_DELAY); this->load_settings(false); @@ -237,8 +247,12 @@ void IDFUARTComponent::set_rx_timeout(size_t rx_timeout) { void IDFUARTComponent::write_array(const uint8_t *data, size_t len) { xSemaphoreTake(this->lock_, portMAX_DELAY); - uart_write_bytes(this->uart_num_, data, len); + int32_t write_len = uart_write_bytes(this->uart_num_, data, len); xSemaphoreGive(this->lock_); + if (write_len != (int32_t) len) { + ESP_LOGW(TAG, "uart_write_bytes failed: %d != %zu", write_len, len); + this->mark_failed(); + } #ifdef USE_UART_DEBUGGER for (size_t i = 0; i < len; i++) { this->debug_callback_.call(UART_DIRECTION_TX, data[i]); @@ -267,6 +281,7 @@ bool IDFUARTComponent::peek_byte(uint8_t *data) { bool IDFUARTComponent::read_array(uint8_t *data, size_t len) { size_t length_to_read = len; + int32_t read_len = 0; if (!this->check_read_timeout_(len)) return false; xSemaphoreTake(this->lock_, portMAX_DELAY); @@ -277,25 +292,31 @@ bool IDFUARTComponent::read_array(uint8_t *data, size_t len) { this->has_peek_ = false; } if (length_to_read > 0) - uart_read_bytes(this->uart_num_, data, length_to_read, 20 / portTICK_PERIOD_MS); + read_len = uart_read_bytes(this->uart_num_, data, length_to_read, 20 / portTICK_PERIOD_MS); xSemaphoreGive(this->lock_); #ifdef USE_UART_DEBUGGER for (size_t i = 0; i < len; i++) { this->debug_callback_.call(UART_DIRECTION_RX, data[i]); } #endif - return true; + return read_len == (int32_t) length_to_read; } int IDFUARTComponent::available() { - size_t available; + size_t available = 0; + esp_err_t err; xSemaphoreTake(this->lock_, portMAX_DELAY); - uart_get_buffered_data_len(this->uart_num_, &available); - if (this->has_peek_) - available++; + err = uart_get_buffered_data_len(this->uart_num_, &available); xSemaphoreGive(this->lock_); + if (err != ESP_OK) { + ESP_LOGW(TAG, "uart_get_buffered_data_len failed: %s", esp_err_to_name(err)); + this->mark_failed(); + } + if (this->has_peek_) { + available++; + } return available; } From 6440b5fbf5d05e7b572d9272c8ea8865b59ed513 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 15 Nov 2025 22:03:43 -0600 Subject: [PATCH 048/415] [ld2412] Fix stuck targets by adding timeout filter (#11919) --- esphome/components/ld2412/sensor.py | 76 ++++++++++++++++++++++++++--- 1 file changed, 68 insertions(+), 8 deletions(-) diff --git a/esphome/components/ld2412/sensor.py b/esphome/components/ld2412/sensor.py index abb823faad..0bfbd9bf1d 100644 --- a/esphome/components/ld2412/sensor.py +++ b/esphome/components/ld2412/sensor.py @@ -31,36 +31,84 @@ CONFIG_SCHEMA = cv.Schema( cv.GenerateID(CONF_LD2412_ID): cv.use_id(LD2412Component), cv.Optional(CONF_DETECTION_DISTANCE): sensor.sensor_schema( device_class=DEVICE_CLASS_DISTANCE, - filters=[{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}], + filters=[ + { + "timeout": { + "timeout": cv.TimePeriod(milliseconds=1000), + "value": "last", + } + }, + {"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}, + ], icon=ICON_SIGNAL, unit_of_measurement=UNIT_CENTIMETER, ), cv.Optional(CONF_LIGHT): sensor.sensor_schema( device_class=DEVICE_CLASS_ILLUMINANCE, entity_category=ENTITY_CATEGORY_DIAGNOSTIC, - filters=[{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}], + filters=[ + { + "timeout": { + "timeout": cv.TimePeriod(milliseconds=1000), + "value": "last", + } + }, + {"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}, + ], icon=ICON_LIGHTBULB, unit_of_measurement=UNIT_EMPTY, # No standard unit for this light sensor ), cv.Optional(CONF_MOVING_DISTANCE): sensor.sensor_schema( device_class=DEVICE_CLASS_DISTANCE, - filters=[{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}], + filters=[ + { + "timeout": { + "timeout": cv.TimePeriod(milliseconds=1000), + "value": "last", + } + }, + {"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}, + ], icon=ICON_SIGNAL, unit_of_measurement=UNIT_CENTIMETER, ), cv.Optional(CONF_MOVING_ENERGY): sensor.sensor_schema( - filters=[{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}], + filters=[ + { + "timeout": { + "timeout": cv.TimePeriod(milliseconds=1000), + "value": "last", + } + }, + {"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}, + ], icon=ICON_MOTION_SENSOR, unit_of_measurement=UNIT_PERCENT, ), cv.Optional(CONF_STILL_DISTANCE): sensor.sensor_schema( device_class=DEVICE_CLASS_DISTANCE, - filters=[{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}], + filters=[ + { + "timeout": { + "timeout": cv.TimePeriod(milliseconds=1000), + "value": "last", + } + }, + {"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}, + ], icon=ICON_SIGNAL, unit_of_measurement=UNIT_CENTIMETER, ), cv.Optional(CONF_STILL_ENERGY): sensor.sensor_schema( - filters=[{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}], + filters=[ + { + "timeout": { + "timeout": cv.TimePeriod(milliseconds=1000), + "value": "last", + } + }, + {"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}, + ], icon=ICON_FLASH, unit_of_measurement=UNIT_PERCENT, ), @@ -74,7 +122,13 @@ CONFIG_SCHEMA = CONFIG_SCHEMA.extend( cv.Optional(CONF_MOVE_ENERGY): sensor.sensor_schema( entity_category=ENTITY_CATEGORY_DIAGNOSTIC, filters=[ - {"throttle_with_priority": cv.TimePeriod(milliseconds=1000)} + { + "timeout": { + "timeout": cv.TimePeriod(milliseconds=1000), + "value": "last", + } + }, + {"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}, ], icon=ICON_MOTION_SENSOR, unit_of_measurement=UNIT_PERCENT, @@ -82,7 +136,13 @@ CONFIG_SCHEMA = CONFIG_SCHEMA.extend( cv.Optional(CONF_STILL_ENERGY): sensor.sensor_schema( entity_category=ENTITY_CATEGORY_DIAGNOSTIC, filters=[ - {"throttle_with_priority": cv.TimePeriod(milliseconds=1000)} + { + "timeout": { + "timeout": cv.TimePeriod(milliseconds=1000), + "value": "last", + } + }, + {"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}, ], icon=ICON_FLASH, unit_of_measurement=UNIT_PERCENT, From d559f9f52e26cc779528fdb5c015cb73560536f1 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 15 Nov 2025 22:04:25 -0600 Subject: [PATCH 049/415] [ld2410] Add timeout filter to prevent stuck targets (#11920) --- esphome/components/ld2410/sensor.py | 76 ++++++++++++++++++++++++++--- 1 file changed, 68 insertions(+), 8 deletions(-) diff --git a/esphome/components/ld2410/sensor.py b/esphome/components/ld2410/sensor.py index fca2b2ceca..3bd34963bc 100644 --- a/esphome/components/ld2410/sensor.py +++ b/esphome/components/ld2410/sensor.py @@ -31,35 +31,83 @@ CONFIG_SCHEMA = cv.Schema( cv.GenerateID(CONF_LD2410_ID): cv.use_id(LD2410Component), cv.Optional(CONF_MOVING_DISTANCE): sensor.sensor_schema( device_class=DEVICE_CLASS_DISTANCE, - filters=[{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}], + filters=[ + { + "timeout": { + "timeout": cv.TimePeriod(milliseconds=1000), + "value": "last", + } + }, + {"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}, + ], icon=ICON_SIGNAL, unit_of_measurement=UNIT_CENTIMETER, ), cv.Optional(CONF_STILL_DISTANCE): sensor.sensor_schema( device_class=DEVICE_CLASS_DISTANCE, - filters=[{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}], + filters=[ + { + "timeout": { + "timeout": cv.TimePeriod(milliseconds=1000), + "value": "last", + } + }, + {"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}, + ], icon=ICON_SIGNAL, unit_of_measurement=UNIT_CENTIMETER, ), cv.Optional(CONF_MOVING_ENERGY): sensor.sensor_schema( - filters=[{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}], + filters=[ + { + "timeout": { + "timeout": cv.TimePeriod(milliseconds=1000), + "value": "last", + } + }, + {"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}, + ], icon=ICON_MOTION_SENSOR, unit_of_measurement=UNIT_PERCENT, ), cv.Optional(CONF_STILL_ENERGY): sensor.sensor_schema( - filters=[{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}], + filters=[ + { + "timeout": { + "timeout": cv.TimePeriod(milliseconds=1000), + "value": "last", + } + }, + {"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}, + ], icon=ICON_FLASH, unit_of_measurement=UNIT_PERCENT, ), cv.Optional(CONF_LIGHT): sensor.sensor_schema( device_class=DEVICE_CLASS_ILLUMINANCE, entity_category=ENTITY_CATEGORY_DIAGNOSTIC, - filters=[{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}], + filters=[ + { + "timeout": { + "timeout": cv.TimePeriod(milliseconds=1000), + "value": "last", + } + }, + {"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}, + ], icon=ICON_LIGHTBULB, ), cv.Optional(CONF_DETECTION_DISTANCE): sensor.sensor_schema( device_class=DEVICE_CLASS_DISTANCE, - filters=[{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}], + filters=[ + { + "timeout": { + "timeout": cv.TimePeriod(milliseconds=1000), + "value": "last", + } + }, + {"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}, + ], icon=ICON_SIGNAL, unit_of_measurement=UNIT_CENTIMETER, ), @@ -73,7 +121,13 @@ CONFIG_SCHEMA = CONFIG_SCHEMA.extend( cv.Optional(CONF_MOVE_ENERGY): sensor.sensor_schema( entity_category=ENTITY_CATEGORY_DIAGNOSTIC, filters=[ - {"throttle_with_priority": cv.TimePeriod(milliseconds=1000)} + { + "timeout": { + "timeout": cv.TimePeriod(milliseconds=1000), + "value": "last", + } + }, + {"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}, ], icon=ICON_MOTION_SENSOR, unit_of_measurement=UNIT_PERCENT, @@ -81,7 +135,13 @@ CONFIG_SCHEMA = CONFIG_SCHEMA.extend( cv.Optional(CONF_STILL_ENERGY): sensor.sensor_schema( entity_category=ENTITY_CATEGORY_DIAGNOSTIC, filters=[ - {"throttle_with_priority": cv.TimePeriod(milliseconds=1000)} + { + "timeout": { + "timeout": cv.TimePeriod(milliseconds=1000), + "value": "last", + } + }, + {"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}, ], icon=ICON_FLASH, unit_of_measurement=UNIT_PERCENT, From 36868ee7b173628e838951d3fb3c899e66b74866 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 15 Nov 2025 22:20:57 -0600 Subject: [PATCH 050/415] [scheduler] Fix timing breakage after 49 days of uptime on ESP8266/RP2040 (#11924) --- esphome/core/scheduler.cpp | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/esphome/core/scheduler.cpp b/esphome/core/scheduler.cpp index d285af2d0e..d2e0f0dab4 100644 --- a/esphome/core/scheduler.cpp +++ b/esphome/core/scheduler.cpp @@ -609,13 +609,12 @@ uint64_t Scheduler::millis_64_(uint32_t now) { if (now < last && (last - now) > HALF_MAX_UINT32) { this->millis_major_++; major++; + this->last_millis_ = now; #ifdef ESPHOME_DEBUG_SCHEDULER ESP_LOGD(TAG, "Detected true 32-bit rollover at %" PRIu32 "ms (was %" PRIu32 ")", now, last); #endif /* ESPHOME_DEBUG_SCHEDULER */ - } - - // Only update if time moved forward - if (now > last) { + } else if (now > last) { + // Only update if time moved forward this->last_millis_ = now; } From f19296ac7f59c4b2610b127cd1663e11648cf645 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 16 Nov 2025 07:35:31 -0600 Subject: [PATCH 051/415] [analyze-memory] Show all core symbols > 100 B instead of top 15 (#11909) --- esphome/analyze_memory/cli.py | 26 ++++++++++++++++++++------ 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/esphome/analyze_memory/cli.py b/esphome/analyze_memory/cli.py index 718f42330d..44ade221f8 100644 --- a/esphome/analyze_memory/cli.py +++ b/esphome/analyze_memory/cli.py @@ -15,6 +15,11 @@ from . import ( class MemoryAnalyzerCLI(MemoryAnalyzer): """Memory analyzer with CLI-specific report generation.""" + # Symbol size threshold for detailed analysis + SYMBOL_SIZE_THRESHOLD: int = ( + 100 # Show symbols larger than this in detailed analysis + ) + # Column width constants COL_COMPONENT: int = 29 COL_FLASH_TEXT: int = 14 @@ -191,14 +196,21 @@ class MemoryAnalyzerCLI(MemoryAnalyzer): f"{len(symbols):>{self.COL_CORE_COUNT}} | {percentage:>{self.COL_CORE_PERCENT - 1}.1f}%" ) - # Top 15 largest core symbols + # All core symbols above threshold lines.append("") - lines.append(f"Top 15 Largest {_COMPONENT_CORE} Symbols:") sorted_core_symbols = sorted( self._esphome_core_symbols, key=lambda x: x[2], reverse=True ) + large_core_symbols = [ + (symbol, demangled, size) + for symbol, demangled, size in sorted_core_symbols + if size > self.SYMBOL_SIZE_THRESHOLD + ] - for i, (symbol, demangled, size) in enumerate(sorted_core_symbols[:15]): + lines.append( + f"{_COMPONENT_CORE} Symbols > {self.SYMBOL_SIZE_THRESHOLD} B ({len(large_core_symbols)} symbols):" + ) + for i, (symbol, demangled, size) in enumerate(large_core_symbols): lines.append(f"{i + 1}. {demangled} ({size:,} B)") lines.append("=" * self.TABLE_WIDTH) @@ -268,13 +280,15 @@ class MemoryAnalyzerCLI(MemoryAnalyzer): lines.append(f"Total size: {comp_mem.flash_total:,} B") lines.append("") - # Show all symbols > 100 bytes for better visibility + # Show all symbols above threshold for better visibility large_symbols = [ - (sym, dem, size) for sym, dem, size in sorted_symbols if size > 100 + (sym, dem, size) + for sym, dem, size in sorted_symbols + if size > self.SYMBOL_SIZE_THRESHOLD ] lines.append( - f"{comp_name} Symbols > 100 B ({len(large_symbols)} symbols):" + f"{comp_name} Symbols > {self.SYMBOL_SIZE_THRESHOLD} B ({len(large_symbols)} symbols):" ) for i, (symbol, demangled, size) in enumerate(large_symbols): lines.append(f"{i + 1}. {demangled} ({size:,} B)") From 91514894817fa318186b29dec48af397f9222ca9 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 16 Nov 2025 18:38:38 -0600 Subject: [PATCH 052/415] [sntp] Merge multiple instances to fix crash and undefined behavior (#11904) --- esphome/components/sntp/time.py | 68 +++++ tests/component_tests/sntp/__init__.py | 1 + .../sntp/config/sntp_test.yaml | 22 ++ tests/component_tests/sntp/test_init.py | 238 ++++++++++++++++++ 4 files changed, 329 insertions(+) create mode 100644 tests/component_tests/sntp/__init__.py create mode 100644 tests/component_tests/sntp/config/sntp_test.yaml create mode 100644 tests/component_tests/sntp/test_init.py diff --git a/esphome/components/sntp/time.py b/esphome/components/sntp/time.py index d27fc9991d..69a2436d3d 100644 --- a/esphome/components/sntp/time.py +++ b/esphome/components/sntp/time.py @@ -1,9 +1,14 @@ +import logging + import esphome.codegen as cg from esphome.components import time as time_ +from esphome.config_helpers import merge_config import esphome.config_validation as cv from esphome.const import ( CONF_ID, + CONF_PLATFORM, CONF_SERVERS, + CONF_TIME, PLATFORM_BK72XX, PLATFORM_ESP32, PLATFORM_ESP8266, @@ -12,13 +17,74 @@ from esphome.const import ( PLATFORM_RTL87XX, ) from esphome.core import CORE +import esphome.final_validate as fv +from esphome.types import ConfigType + +_LOGGER = logging.getLogger(__name__) DEPENDENCIES = ["network"] + +CONF_SNTP = "sntp" + sntp_ns = cg.esphome_ns.namespace("sntp") SNTPComponent = sntp_ns.class_("SNTPComponent", time_.RealTimeClock) DEFAULT_SERVERS = ["0.pool.ntp.org", "1.pool.ntp.org", "2.pool.ntp.org"] + +def _sntp_final_validate(config: ConfigType) -> None: + """Merge multiple SNTP instances into one, similar to OTA merging behavior.""" + full_conf = fv.full_config.get() + time_confs = full_conf.get(CONF_TIME, []) + + sntp_configs: list[ConfigType] = [] + other_time_configs: list[ConfigType] = [] + + for time_conf in time_confs: + if time_conf.get(CONF_PLATFORM) == CONF_SNTP: + sntp_configs.append(time_conf) + else: + other_time_configs.append(time_conf) + + if len(sntp_configs) <= 1: + return + + # Merge all SNTP configs into the first one + merged = sntp_configs[0] + for sntp_conf in sntp_configs[1:]: + # Validate that IDs are consistent if manually specified + if merged[CONF_ID].is_manual and sntp_conf[CONF_ID].is_manual: + raise cv.Invalid( + f"Found multiple SNTP configurations but {CONF_ID} is inconsistent" + ) + merged = merge_config(merged, sntp_conf) + + # Deduplicate servers while preserving order + servers = merged[CONF_SERVERS] + unique_servers = list(dict.fromkeys(servers)) + + # Warn if we're dropping servers due to 3-server limit + if len(unique_servers) > 3: + dropped = unique_servers[3:] + unique_servers = unique_servers[:3] + _LOGGER.warning( + "SNTP supports maximum 3 servers. Dropped excess server(s): %s", + dropped, + ) + + merged[CONF_SERVERS] = unique_servers + + _LOGGER.warning( + "Found and merged %d SNTP time configurations into one instance", + len(sntp_configs), + ) + + # Replace time configs with merged SNTP + other time platforms + other_time_configs.append(merged) + full_conf[CONF_TIME] = other_time_configs + fv.full_config.set(full_conf) + + CONFIG_SCHEMA = cv.All( time_.TIME_SCHEMA.extend( { @@ -40,6 +106,8 @@ CONFIG_SCHEMA = cv.All( ), ) +FINAL_VALIDATE_SCHEMA = _sntp_final_validate + async def to_code(config): servers = config[CONF_SERVERS] diff --git a/tests/component_tests/sntp/__init__.py b/tests/component_tests/sntp/__init__.py new file mode 100644 index 0000000000..7d323a4980 --- /dev/null +++ b/tests/component_tests/sntp/__init__.py @@ -0,0 +1 @@ +"""Tests for SNTP component.""" diff --git a/tests/component_tests/sntp/config/sntp_test.yaml b/tests/component_tests/sntp/config/sntp_test.yaml new file mode 100644 index 0000000000..3942c9606b --- /dev/null +++ b/tests/component_tests/sntp/config/sntp_test.yaml @@ -0,0 +1,22 @@ +esphome: + name: sntp-test + +esp32: + board: esp32dev + framework: + type: esp-idf + +wifi: + ssid: "testssid" + password: "testpassword" + +# Test multiple SNTP instances that should be merged +time: + - platform: sntp + servers: + - 192.168.1.1 + - pool.ntp.org + - platform: sntp + servers: + - pool.ntp.org + - 192.168.1.2 diff --git a/tests/component_tests/sntp/test_init.py b/tests/component_tests/sntp/test_init.py new file mode 100644 index 0000000000..9197ff55d0 --- /dev/null +++ b/tests/component_tests/sntp/test_init.py @@ -0,0 +1,238 @@ +"""Tests for SNTP time configuration validation.""" + +from __future__ import annotations + +import logging +from typing import Any + +import pytest + +from esphome import config_validation as cv +from esphome.components.sntp.time import CONF_SNTP, _sntp_final_validate +from esphome.const import CONF_ID, CONF_PLATFORM, CONF_SERVERS, CONF_TIME +from esphome.core import ID +import esphome.final_validate as fv + + +@pytest.mark.parametrize( + ("time_configs", "expected_count", "expected_servers", "warning_messages"), + [ + pytest.param( + [ + { + CONF_PLATFORM: CONF_SNTP, + CONF_ID: ID("sntp_time", is_manual=False), + CONF_SERVERS: ["192.168.1.1", "pool.ntp.org"], + } + ], + 1, + ["192.168.1.1", "pool.ntp.org"], + [], + id="single_instance_no_merge", + ), + pytest.param( + [ + { + CONF_PLATFORM: CONF_SNTP, + CONF_ID: ID("sntp_time_1", is_manual=False), + CONF_SERVERS: ["192.168.1.1", "pool.ntp.org"], + }, + { + CONF_PLATFORM: CONF_SNTP, + CONF_ID: ID("sntp_time_2", is_manual=False), + CONF_SERVERS: ["192.168.1.2"], + }, + ], + 1, + ["192.168.1.1", "pool.ntp.org", "192.168.1.2"], + ["Found and merged 2 SNTP time configurations into one instance"], + id="two_instances_merged", + ), + pytest.param( + [ + { + CONF_PLATFORM: CONF_SNTP, + CONF_ID: ID("sntp_time_1", is_manual=False), + CONF_SERVERS: ["192.168.1.1", "pool.ntp.org"], + }, + { + CONF_PLATFORM: CONF_SNTP, + CONF_ID: ID("sntp_time_2", is_manual=False), + CONF_SERVERS: ["pool.ntp.org", "192.168.1.2"], + }, + ], + 1, + ["192.168.1.1", "pool.ntp.org", "192.168.1.2"], + ["Found and merged 2 SNTP time configurations into one instance"], + id="deduplication_preserves_order", + ), + pytest.param( + [ + { + CONF_PLATFORM: CONF_SNTP, + CONF_ID: ID("sntp_time_1", is_manual=False), + CONF_SERVERS: ["192.168.1.1", "pool.ntp.org"], + }, + { + CONF_PLATFORM: CONF_SNTP, + CONF_ID: ID("sntp_time_2", is_manual=False), + CONF_SERVERS: ["192.168.1.2", "pool2.ntp.org"], + }, + { + CONF_PLATFORM: CONF_SNTP, + CONF_ID: ID("sntp_time_3", is_manual=False), + CONF_SERVERS: ["pool3.ntp.org"], + }, + ], + 1, + ["192.168.1.1", "pool.ntp.org", "192.168.1.2"], + [ + "SNTP supports maximum 3 servers. Dropped excess server(s): ['pool2.ntp.org', 'pool3.ntp.org']", + "Found and merged 3 SNTP time configurations into one instance", + ], + id="three_instances_drops_excess_servers", + ), + pytest.param( + [ + { + CONF_PLATFORM: CONF_SNTP, + CONF_ID: ID("sntp_time_1", is_manual=False), + CONF_SERVERS: [ + "192.168.1.1", + "pool.ntp.org", + "pool.ntp.org", + "192.168.1.1", + ], + }, + { + CONF_PLATFORM: CONF_SNTP, + CONF_ID: ID("sntp_time_2", is_manual=False), + CONF_SERVERS: ["pool.ntp.org", "192.168.1.2"], + }, + ], + 1, + ["192.168.1.1", "pool.ntp.org", "192.168.1.2"], + ["Found and merged 2 SNTP time configurations into one instance"], + id="deduplication_multiple_duplicates", + ), + ], +) +def test_sntp_instance_merging( + time_configs: list[dict[str, Any]], + expected_count: int, + expected_servers: list[str], + warning_messages: list[str], + caplog: pytest.LogCaptureFixture, +) -> None: + """Test SNTP instance merging behavior.""" + # Create a mock full config with time configs + full_conf = {CONF_TIME: time_configs.copy()} + + # Set the context var + token = fv.full_config.set(full_conf) + try: + with caplog.at_level(logging.WARNING): + _sntp_final_validate({}) + + # Get the updated config + updated_conf = fv.full_config.get() + + # Check if merging occurred + if len(time_configs) > 1: + # Verify only one SNTP instance remains + sntp_instances = [ + tc + for tc in updated_conf[CONF_TIME] + if tc.get(CONF_PLATFORM) == CONF_SNTP + ] + assert len(sntp_instances) == expected_count + + # Verify server list + assert sntp_instances[0][CONF_SERVERS] == expected_servers + + # Verify warnings + for expected_msg in warning_messages: + assert any( + expected_msg in record.message for record in caplog.records + ), f"Expected warning message '{expected_msg}' not found in log" + else: + # Single instance should not trigger merging or warnings + assert len(caplog.records) == 0 + # Config should be unchanged + assert updated_conf[CONF_TIME] == time_configs + finally: + fv.full_config.reset(token) + + +def test_sntp_inconsistent_manual_ids() -> None: + """Test that inconsistent manual IDs raise an error.""" + # Create configs with manual IDs that are inconsistent + time_configs = [ + { + CONF_PLATFORM: CONF_SNTP, + CONF_ID: ID("sntp_time_1", is_manual=True), + CONF_SERVERS: ["192.168.1.1"], + }, + { + CONF_PLATFORM: CONF_SNTP, + CONF_ID: ID("sntp_time_2", is_manual=True), + CONF_SERVERS: ["192.168.1.2"], + }, + ] + + full_conf = {CONF_TIME: time_configs} + + token = fv.full_config.set(full_conf) + try: + with pytest.raises( + cv.Invalid, + match="Found multiple SNTP configurations but id is inconsistent", + ): + _sntp_final_validate({}) + finally: + fv.full_config.reset(token) + + +def test_sntp_with_other_time_platforms(caplog: pytest.LogCaptureFixture) -> None: + """Test that SNTP merging doesn't affect other time platforms.""" + time_configs = [ + { + CONF_PLATFORM: CONF_SNTP, + CONF_ID: ID("sntp_time_1", is_manual=False), + CONF_SERVERS: ["192.168.1.1"], + }, + { + CONF_PLATFORM: "homeassistant", + CONF_ID: ID("homeassistant_time", is_manual=False), + }, + { + CONF_PLATFORM: CONF_SNTP, + CONF_ID: ID("sntp_time_2", is_manual=False), + CONF_SERVERS: ["192.168.1.2"], + }, + ] + + full_conf = {CONF_TIME: time_configs.copy()} + + token = fv.full_config.set(full_conf) + try: + with caplog.at_level(logging.WARNING): + _sntp_final_validate({}) + + updated_conf = fv.full_config.get() + + # Should have 2 time platforms: 1 merged SNTP + 1 homeassistant + assert len(updated_conf[CONF_TIME]) == 2 + + # Find the platforms + platforms = {tc[CONF_PLATFORM] for tc in updated_conf[CONF_TIME]} + assert platforms == {CONF_SNTP, "homeassistant"} + + # Verify SNTP was merged + sntp_instances = [ + tc for tc in updated_conf[CONF_TIME] if tc[CONF_PLATFORM] == CONF_SNTP + ] + assert len(sntp_instances) == 1 + assert sntp_instances[0][CONF_SERVERS] == ["192.168.1.1", "192.168.1.2"] + finally: + fv.full_config.reset(token) From 3fd58f1a917f3d69af101a544f2688d70ec69228 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 16 Nov 2025 18:38:52 -0600 Subject: [PATCH 053/415] [web_server.ota] Merge multiple instances to prevent undefined behavior (#11905) --- esphome/components/web_server/ota/__init__.py | 58 ++++++- .../ota/test_web_server_ota.py | 153 ++++++++++++++++++ 2 files changed, 210 insertions(+), 1 deletion(-) diff --git a/esphome/components/web_server/ota/__init__.py b/esphome/components/web_server/ota/__init__.py index 4a98db8877..260e6aea6d 100644 --- a/esphome/components/web_server/ota/__init__.py +++ b/esphome/components/web_server/ota/__init__.py @@ -1,10 +1,17 @@ +import logging + import esphome.codegen as cg from esphome.components.esp32 import add_idf_component from esphome.components.ota import BASE_OTA_SCHEMA, OTAComponent, ota_to_code +from esphome.config_helpers import merge_config import esphome.config_validation as cv -from esphome.const import CONF_ID +from esphome.const import CONF_ID, CONF_OTA, CONF_PLATFORM, CONF_WEB_SERVER from esphome.core import CORE, coroutine_with_priority from esphome.coroutine import CoroPriority +import esphome.final_validate as fv +from esphome.types import ConfigType + +_LOGGER = logging.getLogger(__name__) CODEOWNERS = ["@esphome/core"] DEPENDENCIES = ["network", "web_server_base"] @@ -12,6 +19,53 @@ DEPENDENCIES = ["network", "web_server_base"] web_server_ns = cg.esphome_ns.namespace("web_server") WebServerOTAComponent = web_server_ns.class_("WebServerOTAComponent", OTAComponent) + +def _web_server_ota_final_validate(config: ConfigType) -> None: + """Merge multiple web_server OTA instances into one. + + Multiple web_server OTA instances register duplicate HTTP handlers for /update, + causing undefined behavior. Merge them into a single instance. + """ + full_conf = fv.full_config.get() + ota_confs = full_conf.get(CONF_OTA, []) + + web_server_ota_configs: list[ConfigType] = [] + other_ota_configs: list[ConfigType] = [] + + for ota_conf in ota_confs: + if ota_conf.get(CONF_PLATFORM) == CONF_WEB_SERVER: + web_server_ota_configs.append(ota_conf) + else: + other_ota_configs.append(ota_conf) + + if len(web_server_ota_configs) <= 1: + return + + # Merge all web_server OTA configs into the first one + merged = web_server_ota_configs[0] + for ota_conf in web_server_ota_configs[1:]: + # Validate that IDs are consistent if manually specified + if ( + merged[CONF_ID].is_manual + and ota_conf[CONF_ID].is_manual + and merged[CONF_ID] != ota_conf[CONF_ID] + ): + raise cv.Invalid( + f"Found multiple web_server OTA configurations but {CONF_ID} is inconsistent" + ) + merged = merge_config(merged, ota_conf) + + _LOGGER.warning( + "Found and merged %d web_server OTA configurations into one instance", + len(web_server_ota_configs), + ) + + # Replace OTA configs with merged web_server + other OTA platforms + other_ota_configs.append(merged) + full_conf[CONF_OTA] = other_ota_configs + fv.full_config.set(full_conf) + + CONFIG_SCHEMA = ( cv.Schema( { @@ -22,6 +76,8 @@ CONFIG_SCHEMA = ( .extend(cv.COMPONENT_SCHEMA) ) +FINAL_VALIDATE_SCHEMA = _web_server_ota_final_validate + @coroutine_with_priority(CoroPriority.WEB_SERVER_OTA) async def to_code(config): diff --git a/tests/component_tests/ota/test_web_server_ota.py b/tests/component_tests/ota/test_web_server_ota.py index 0d8ff6f134..794eaac9be 100644 --- a/tests/component_tests/ota/test_web_server_ota.py +++ b/tests/component_tests/ota/test_web_server_ota.py @@ -1,6 +1,18 @@ """Tests for the web_server OTA platform.""" +from __future__ import annotations + from collections.abc import Callable +import logging +from typing import Any + +import pytest + +from esphome import config_validation as cv +from esphome.components.web_server.ota import _web_server_ota_final_validate +from esphome.const import CONF_ID, CONF_OTA, CONF_PLATFORM, CONF_WEB_SERVER +from esphome.core import ID +import esphome.final_validate as fv def test_web_server_ota_generated(generate_main: Callable[[str], str]) -> None: @@ -100,3 +112,144 @@ def test_web_server_ota_esp8266(generate_main: Callable[[str], str]) -> None: # Check web server OTA component is present assert "WebServerOTAComponent" in main_cpp assert "web_server::WebServerOTAComponent" in main_cpp + + +@pytest.mark.parametrize( + ("ota_configs", "expected_count", "warning_expected"), + [ + pytest.param( + [ + { + CONF_PLATFORM: CONF_WEB_SERVER, + CONF_ID: ID("ota_web", is_manual=False), + } + ], + 1, + False, + id="single_instance_no_merge", + ), + pytest.param( + [ + { + CONF_PLATFORM: CONF_WEB_SERVER, + CONF_ID: ID("ota_web_1", is_manual=False), + }, + { + CONF_PLATFORM: CONF_WEB_SERVER, + CONF_ID: ID("ota_web_2", is_manual=False), + }, + ], + 1, + True, + id="two_instances_merged", + ), + pytest.param( + [ + { + CONF_PLATFORM: CONF_WEB_SERVER, + CONF_ID: ID("ota_web_1", is_manual=False), + }, + { + CONF_PLATFORM: "esphome", + CONF_ID: ID("ota_esphome", is_manual=False), + }, + { + CONF_PLATFORM: CONF_WEB_SERVER, + CONF_ID: ID("ota_web_2", is_manual=False), + }, + ], + 2, + True, + id="mixed_platforms_web_server_merged", + ), + ], +) +def test_web_server_ota_instance_merging( + ota_configs: list[dict[str, Any]], + expected_count: int, + warning_expected: bool, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test web_server OTA instance merging behavior.""" + full_conf = {CONF_OTA: ota_configs.copy()} + + token = fv.full_config.set(full_conf) + try: + with caplog.at_level(logging.WARNING): + _web_server_ota_final_validate({}) + + updated_conf = fv.full_config.get() + + # Verify total number of OTA platforms + assert len(updated_conf[CONF_OTA]) == expected_count + + # Verify warning + if warning_expected: + assert any( + "Found and merged" in record.message + and "web_server OTA" in record.message + for record in caplog.records + ), "Expected merge warning not found in log" + else: + assert len(caplog.records) == 0, "Unexpected warnings logged" + finally: + fv.full_config.reset(token) + + +def test_web_server_ota_consistent_manual_ids( + caplog: pytest.LogCaptureFixture, +) -> None: + """Test that consistent manual IDs can be merged successfully.""" + ota_configs = [ + { + CONF_PLATFORM: CONF_WEB_SERVER, + CONF_ID: ID("ota_web", is_manual=True), + }, + { + CONF_PLATFORM: CONF_WEB_SERVER, + CONF_ID: ID("ota_web", is_manual=True), + }, + ] + + full_conf = {CONF_OTA: ota_configs} + + token = fv.full_config.set(full_conf) + try: + with caplog.at_level(logging.WARNING): + _web_server_ota_final_validate({}) + + updated_conf = fv.full_config.get() + assert len(updated_conf[CONF_OTA]) == 1 + assert updated_conf[CONF_OTA][0][CONF_ID].id == "ota_web" + assert any( + "Found and merged" in record.message and "web_server OTA" in record.message + for record in caplog.records + ) + finally: + fv.full_config.reset(token) + + +def test_web_server_ota_inconsistent_manual_ids() -> None: + """Test that inconsistent manual IDs raise an error.""" + ota_configs = [ + { + CONF_PLATFORM: CONF_WEB_SERVER, + CONF_ID: ID("ota_web_1", is_manual=True), + }, + { + CONF_PLATFORM: CONF_WEB_SERVER, + CONF_ID: ID("ota_web_2", is_manual=True), + }, + ] + + full_conf = {CONF_OTA: ota_configs} + + token = fv.full_config.set(full_conf) + try: + with pytest.raises( + cv.Invalid, + match="Found multiple web_server OTA configurations but id is inconsistent", + ): + _web_server_ota_final_validate({}) + finally: + fv.full_config.reset(token) From 9e02e3191798010befd85006f17ebebc4a03b360 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 16 Nov 2025 18:39:01 -0600 Subject: [PATCH 054/415] [web_server_idf] Fix lwIP assertion crash by shutting down sockets on connection close (#11937) --- .../components/web_server_idf/web_server_idf.cpp | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/esphome/components/web_server_idf/web_server_idf.cpp b/esphome/components/web_server_idf/web_server_idf.cpp index 0dab5e7e8c..ce91569de2 100644 --- a/esphome/components/web_server_idf/web_server_idf.cpp +++ b/esphome/components/web_server_idf/web_server_idf.cpp @@ -489,10 +489,18 @@ AsyncEventSourceResponse::AsyncEventSourceResponse(const AsyncWebServerRequest * void AsyncEventSourceResponse::destroy(void *ptr) { auto *rsp = static_cast(ptr); - ESP_LOGD(TAG, "Event source connection closed (fd: %d)", rsp->fd_.load()); - // Mark as dead by setting fd to 0 - will be cleaned up in the main loop - rsp->fd_.store(0); - // Note: We don't delete or remove from set here to avoid race conditions + int fd = rsp->fd_.exchange(0); // Atomically get and clear fd + + if (fd > 0) { + ESP_LOGD(TAG, "Event source connection closed (fd: %d)", fd); + // Immediately shut down the socket to prevent lwIP from delivering more data + // This prevents "recv_tcp: recv for wrong pcb!" assertions when the TCP stack + // tries to deliver queued data after the session is marked as dead + // See: https://github.com/esphome/esphome/issues/11936 + shutdown(fd, SHUT_RDWR); + // Note: We don't close() the socket - httpd owns it and will close it + } + // Session will be cleaned up in the main loop to avoid race conditions } // helper for allowing only unique entries in the queue From 6c6b03bda05a32519b0690e11b12706f1e2df24d Mon Sep 17 00:00:00 2001 From: Anton Sergunov Date: Mon, 17 Nov 2025 07:25:00 +0600 Subject: [PATCH 055/415] [uart] Setup uart pins only if flags are set (#11914) Co-authored-by: J. Nick Koston --- .../components/uart/uart_component_esp8266.cpp | 18 +++++++++++++----- .../components/uart/uart_component_esp_idf.cpp | 18 +++++++++++++----- .../uart/uart_component_libretiny.cpp | 2 +- .../components/uart/uart_component_rp2040.cpp | 18 +++++++++++++----- 4 files changed, 40 insertions(+), 16 deletions(-) diff --git a/esphome/components/uart/uart_component_esp8266.cpp b/esphome/components/uart/uart_component_esp8266.cpp index 7a453dbb50..c84a877ef4 100644 --- a/esphome/components/uart/uart_component_esp8266.cpp +++ b/esphome/components/uart/uart_component_esp8266.cpp @@ -56,11 +56,19 @@ uint32_t ESP8266UartComponent::get_config() { } void ESP8266UartComponent::setup() { - if (this->rx_pin_) { - this->rx_pin_->setup(); - } - if (this->tx_pin_ && this->rx_pin_ != this->tx_pin_) { - this->tx_pin_->setup(); + auto setup_pin_if_needed = [](InternalGPIOPin *pin) { + if (!pin) { + return; + } + const auto mask = gpio::Flags::FLAG_OPEN_DRAIN | gpio::Flags::FLAG_PULLUP | gpio::Flags::FLAG_PULLDOWN; + if ((pin->get_flags() & mask) != gpio::Flags::FLAG_NONE) { + pin->setup(); + } + }; + + setup_pin_if_needed(this->rx_pin_); + if (this->rx_pin_ != this->tx_pin_) { + setup_pin_if_needed(this->tx_pin_); } // Use Arduino HardwareSerial UARTs if all used pins match the ones diff --git a/esphome/components/uart/uart_component_esp_idf.cpp b/esphome/components/uart/uart_component_esp_idf.cpp index 70a13c9e37..61ca8c1c0c 100644 --- a/esphome/components/uart/uart_component_esp_idf.cpp +++ b/esphome/components/uart/uart_component_esp_idf.cpp @@ -133,11 +133,19 @@ void IDFUARTComponent::load_settings(bool dump_config) { return; } - if (this->rx_pin_) { - this->rx_pin_->setup(); - } - if (this->tx_pin_ && this->rx_pin_ != this->tx_pin_) { - this->tx_pin_->setup(); + auto setup_pin_if_needed = [](InternalGPIOPin *pin) { + if (!pin) { + return; + } + const auto mask = gpio::Flags::FLAG_OPEN_DRAIN | gpio::Flags::FLAG_PULLUP | gpio::Flags::FLAG_PULLDOWN; + if ((pin->get_flags() & mask) != gpio::Flags::FLAG_NONE) { + pin->setup(); + } + }; + + setup_pin_if_needed(this->rx_pin_); + if (this->rx_pin_ != this->tx_pin_) { + setup_pin_if_needed(this->tx_pin_); } int8_t tx = this->tx_pin_ != nullptr ? this->tx_pin_->get_pin() : -1; diff --git a/esphome/components/uart/uart_component_libretiny.cpp b/esphome/components/uart/uart_component_libretiny.cpp index 8d1d28fce4..1e408b169b 100644 --- a/esphome/components/uart/uart_component_libretiny.cpp +++ b/esphome/components/uart/uart_component_libretiny.cpp @@ -53,7 +53,7 @@ void LibreTinyUARTComponent::setup() { auto shouldFallbackToSoftwareSerial = [&]() -> bool { auto hasFlags = [](InternalGPIOPin *pin, const gpio::Flags mask) -> bool { - return pin && pin->get_flags() & mask != gpio::Flags::FLAG_NONE; + return pin && (pin->get_flags() & mask) != gpio::Flags::FLAG_NONE; }; if (hasFlags(this->tx_pin_, gpio::Flags::FLAG_OPEN_DRAIN | gpio::Flags::FLAG_PULLUP | gpio::Flags::FLAG_PULLDOWN) || hasFlags(this->rx_pin_, gpio::Flags::FLAG_OPEN_DRAIN | gpio::Flags::FLAG_PULLUP | gpio::Flags::FLAG_PULLDOWN)) { diff --git a/esphome/components/uart/uart_component_rp2040.cpp b/esphome/components/uart/uart_component_rp2040.cpp index c78691653d..cd3905b5c1 100644 --- a/esphome/components/uart/uart_component_rp2040.cpp +++ b/esphome/components/uart/uart_component_rp2040.cpp @@ -52,11 +52,19 @@ uint16_t RP2040UartComponent::get_config() { } void RP2040UartComponent::setup() { - if (this->rx_pin_) { - this->rx_pin_->setup(); - } - if (this->tx_pin_ && this->rx_pin_ != this->tx_pin_) { - this->tx_pin_->setup(); + auto setup_pin_if_needed = [](InternalGPIOPin *pin) { + if (!pin) { + return; + } + const auto mask = gpio::Flags::FLAG_OPEN_DRAIN | gpio::Flags::FLAG_PULLUP | gpio::Flags::FLAG_PULLDOWN; + if ((pin->get_flags() & mask) != gpio::Flags::FLAG_NONE) { + pin->setup(); + } + }; + + setup_pin_if_needed(this->rx_pin_); + if (this->rx_pin_ != this->tx_pin_) { + setup_pin_if_needed(this->tx_pin_); } uint16_t config = get_config(); From a38c4e0c6e4c4e4bf960db6f1e75f74afdfcb574 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Mon, 17 Nov 2025 15:32:09 +1300 Subject: [PATCH 056/415] Bump version to 2025.11.0b3 --- Doxyfile | 2 +- esphome/const.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Doxyfile b/Doxyfile index 8766b8f00c..04046d9ce6 100644 --- a/Doxyfile +++ b/Doxyfile @@ -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.0b2 +PROJECT_NUMBER = 2025.11.0b3 # Using the PROJECT_BRIEF tag one can provide an optional one line description # for a project that appears at the top of each page and should give viewer a diff --git a/esphome/const.py b/esphome/const.py index 8c6020a868..2ca0018fdb 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -4,7 +4,7 @@ from enum import Enum from esphome.enum import StrEnum -__version__ = "2025.11.0b2" +__version__ = "2025.11.0b3" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" VALID_SUBSTITUTIONS_CHARACTERS = ( From 10bdb47eae14bd3fd1cd761737b0633cd9c9894b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 16 Nov 2025 20:37:06 -0600 Subject: [PATCH 057/415] [cover] Modernize to C++17 nested namespaces (#11935) --- esphome/components/cover/automation.h | 6 ++---- esphome/components/cover/cover.cpp | 6 ++---- esphome/components/cover/cover.h | 6 ++---- esphome/components/cover/cover_traits.h | 6 ++---- 4 files changed, 8 insertions(+), 16 deletions(-) diff --git a/esphome/components/cover/automation.h b/esphome/components/cover/automation.h index 752e0398c1..c0345a7cc6 100644 --- a/esphome/components/cover/automation.h +++ b/esphome/components/cover/automation.h @@ -4,8 +4,7 @@ #include "esphome/core/automation.h" #include "cover.h" -namespace esphome { -namespace cover { +namespace esphome::cover { template class OpenAction : public Action { public: @@ -131,5 +130,4 @@ class CoverClosedTrigger : public Trigger<> { } }; -} // namespace cover -} // namespace esphome +} // namespace esphome::cover diff --git a/esphome/components/cover/cover.cpp b/esphome/components/cover/cover.cpp index 3062dba28a..8f735982f1 100644 --- a/esphome/components/cover/cover.cpp +++ b/esphome/components/cover/cover.cpp @@ -6,8 +6,7 @@ #include "esphome/core/log.h" -namespace esphome { -namespace cover { +namespace esphome::cover { static const char *const TAG = "cover"; @@ -212,5 +211,4 @@ void CoverRestoreState::apply(Cover *cover) { cover->publish_state(); } -} // namespace cover -} // namespace esphome +} // namespace esphome::cover diff --git a/esphome/components/cover/cover.h b/esphome/components/cover/cover.h index d5db6cfb4f..6c69c05e71 100644 --- a/esphome/components/cover/cover.h +++ b/esphome/components/cover/cover.h @@ -7,8 +7,7 @@ #include "cover_traits.h" -namespace esphome { -namespace cover { +namespace esphome::cover { const extern float COVER_OPEN; const extern float COVER_CLOSED; @@ -157,5 +156,4 @@ class Cover : public EntityBase, public EntityBase_DeviceClass { ESPPreferenceObject rtc_; }; -} // namespace cover -} // namespace esphome +} // namespace esphome::cover diff --git a/esphome/components/cover/cover_traits.h b/esphome/components/cover/cover_traits.h index 79001c3b03..723516318b 100644 --- a/esphome/components/cover/cover_traits.h +++ b/esphome/components/cover/cover_traits.h @@ -1,7 +1,6 @@ #pragma once -namespace esphome { -namespace cover { +namespace esphome::cover { class CoverTraits { public: @@ -26,5 +25,4 @@ class CoverTraits { bool supports_stop_{false}; }; -} // namespace cover -} // namespace esphome +} // namespace esphome::cover From 23be23613397bd8ce2ebca7a0497a03d72485ecd Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 16 Nov 2025 20:54:43 -0600 Subject: [PATCH 058/415] [number] Modernize to C++17 nested namespaces --- esphome/components/number/automation.cpp | 6 ++---- esphome/components/number/automation.h | 6 ++---- esphome/components/number/number.cpp | 6 ++---- esphome/components/number/number.h | 6 ++---- esphome/components/number/number_call.cpp | 6 ++---- esphome/components/number/number_call.h | 6 ++---- esphome/components/number/number_traits.cpp | 6 ++---- esphome/components/number/number_traits.h | 6 ++---- 8 files changed, 16 insertions(+), 32 deletions(-) diff --git a/esphome/components/number/automation.cpp b/esphome/components/number/automation.cpp index bfc59d0465..78ffc255fe 100644 --- a/esphome/components/number/automation.cpp +++ b/esphome/components/number/automation.cpp @@ -1,8 +1,7 @@ #include "automation.h" #include "esphome/core/log.h" -namespace esphome { -namespace number { +namespace esphome::number { static const char *const TAG = "number.automation"; @@ -52,5 +51,4 @@ void ValueRangeTrigger::on_state_(float state) { this->rtc_.save(&in_range); } -} // namespace number -} // namespace esphome +} // namespace esphome::number diff --git a/esphome/components/number/automation.h b/esphome/components/number/automation.h index 79eba883c4..a7cd04f083 100644 --- a/esphome/components/number/automation.h +++ b/esphome/components/number/automation.h @@ -4,8 +4,7 @@ #include "esphome/core/automation.h" #include "esphome/core/component.h" -namespace esphome { -namespace number { +namespace esphome::number { class NumberStateTrigger : public Trigger { public: @@ -91,5 +90,4 @@ template class NumberInRangeCondition : public Condition float max_{NAN}; }; -} // namespace number -} // namespace esphome +} // namespace esphome::number diff --git a/esphome/components/number/number.cpp b/esphome/components/number/number.cpp index f12e0e9e1e..992100ead0 100644 --- a/esphome/components/number/number.cpp +++ b/esphome/components/number/number.cpp @@ -3,8 +3,7 @@ #include "esphome/core/controller_registry.h" #include "esphome/core/log.h" -namespace esphome { -namespace number { +namespace esphome::number { static const char *const TAG = "number"; @@ -43,5 +42,4 @@ void Number::add_on_state_callback(std::function &&callback) { this->state_callback_.add(std::move(callback)); } -} // namespace number -} // namespace esphome +} // namespace esphome::number diff --git a/esphome/components/number/number.h b/esphome/components/number/number.h index da91d70d53..472e06ad61 100644 --- a/esphome/components/number/number.h +++ b/esphome/components/number/number.h @@ -6,8 +6,7 @@ #include "number_call.h" #include "number_traits.h" -namespace esphome { -namespace number { +namespace esphome::number { class Number; void log_number(const char *tag, const char *prefix, const char *type, Number *obj); @@ -53,5 +52,4 @@ class Number : public EntityBase { CallbackManager state_callback_; }; -} // namespace number -} // namespace esphome +} // namespace esphome::number diff --git a/esphome/components/number/number_call.cpp b/esphome/components/number/number_call.cpp index 669dd65184..27a857c112 100644 --- a/esphome/components/number/number_call.cpp +++ b/esphome/components/number/number_call.cpp @@ -2,8 +2,7 @@ #include "number.h" #include "esphome/core/log.h" -namespace esphome { -namespace number { +namespace esphome::number { static const char *const TAG = "number"; @@ -125,5 +124,4 @@ void NumberCall::perform() { this->parent_->control(target_value); } -} // namespace number -} // namespace esphome +} // namespace esphome::number diff --git a/esphome/components/number/number_call.h b/esphome/components/number/number_call.h index 807207f0ec..0f6889dcb6 100644 --- a/esphome/components/number/number_call.h +++ b/esphome/components/number/number_call.h @@ -4,8 +4,7 @@ #include "esphome/core/log.h" #include "number_traits.h" -namespace esphome { -namespace number { +namespace esphome::number { class Number; @@ -44,5 +43,4 @@ class NumberCall { bool cycle_; }; -} // namespace number -} // namespace esphome +} // namespace esphome::number diff --git a/esphome/components/number/number_traits.cpp b/esphome/components/number/number_traits.cpp index 89035661f5..1e4239ceca 100644 --- a/esphome/components/number/number_traits.cpp +++ b/esphome/components/number/number_traits.cpp @@ -1,10 +1,8 @@ #include "esphome/core/log.h" #include "number_traits.h" -namespace esphome { -namespace number { +namespace esphome::number { static const char *const TAG = "number"; -} // namespace number -} // namespace esphome +} // namespace esphome::number diff --git a/esphome/components/number/number_traits.h b/esphome/components/number/number_traits.h index fa68c2390a..5ccbb9ba48 100644 --- a/esphome/components/number/number_traits.h +++ b/esphome/components/number/number_traits.h @@ -3,8 +3,7 @@ #include "esphome/core/entity_base.h" #include "esphome/core/helpers.h" -namespace esphome { -namespace number { +namespace esphome::number { enum NumberMode : uint8_t { NUMBER_MODE_AUTO = 0, @@ -35,5 +34,4 @@ class NumberTraits : public EntityBase_DeviceClass, public EntityBase_UnitOfMeas NumberMode mode_{NUMBER_MODE_AUTO}; }; -} // namespace number -} // namespace esphome +} // namespace esphome::number From 9b107e7f2a4c5a0d01d2fee3ab9a0b2445797aca Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 16 Nov 2025 22:05:33 -0600 Subject: [PATCH 059/415] touch ups --- esphome/components/bh1750/bh1750.cpp | 299 ++++++++++++++++++--------- esphome/components/bh1750/bh1750.h | 33 ++- 2 files changed, 226 insertions(+), 106 deletions(-) diff --git a/esphome/components/bh1750/bh1750.cpp b/esphome/components/bh1750/bh1750.cpp index 2fc476c17d..f2766e3c64 100644 --- a/esphome/components/bh1750/bh1750.cpp +++ b/esphome/components/bh1750/bh1750.cpp @@ -1,8 +1,8 @@ #include "bh1750.h" #include "esphome/core/log.h" +#include "esphome/core/application.h" -namespace esphome { -namespace bh1750 { +namespace esphome::bh1750 { static const char *const TAG = "bh1750.sensor"; @@ -13,6 +13,31 @@ static const uint8_t BH1750_COMMAND_ONE_TIME_L = 0b00100011; static const uint8_t BH1750_COMMAND_ONE_TIME_H = 0b00100000; static const uint8_t BH1750_COMMAND_ONE_TIME_H2 = 0b00100001; +static constexpr uint32_t MEASUREMENT_TIMEOUT_MS = 2000; +static constexpr float HIGH_LIGHT_THRESHOLD_LX = 7000.0f; + +// Measurement time constants (datasheet values) +static constexpr uint16_t MTREG_DEFAULT = 69; +static constexpr uint16_t MTREG_MIN = 31; +static constexpr uint16_t MTREG_MAX = 254; +static constexpr uint16_t MEAS_TIME_L_MS = 24; // L-resolution max measurement time @ mtreg=69 +static constexpr uint16_t MEAS_TIME_H_MS = 180; // H/H2-resolution max measurement time @ mtreg=69 + +// Conversion constants (datasheet formulas) +static constexpr float RESOLUTION_DIVISOR = 1.2f; // counts to lux conversion divisor +static constexpr float MODE_H2_DIVISOR = 2.0f; // H2 mode has 2x higher resolution + +// MTreg calculation constants +static constexpr int COUNTS_TARGET = 50000; // Target counts for optimal range (avoid saturation) +static constexpr int COUNTS_NUMERATOR = 10; +static constexpr int COUNTS_DENOMINATOR = 12; + +// MTreg register bit manipulation constants +static constexpr uint8_t MTREG_HI_SHIFT = 5; // High 3 bits start at bit 5 +static constexpr uint8_t MTREG_HI_MASK = 0b111; // 3-bit mask for high bits +static constexpr uint8_t MTREG_LO_SHIFT = 0; // Low 5 bits start at bit 0 +static constexpr uint8_t MTREG_LO_MASK = 0b11111; // 5-bit mask for low bits + /* bh1750 properties: @@ -43,74 +68,7 @@ void BH1750Sensor::setup() { this->mark_failed(); return; } -} - -void BH1750Sensor::read_lx_(BH1750Mode mode, uint8_t mtreg, const std::function &f) { - // turn on (after one-shot sensor automatically powers down) - uint8_t turn_on = BH1750_COMMAND_POWER_ON; - if (this->write(&turn_on, 1) != i2c::ERROR_OK) { - ESP_LOGW(TAG, "Power on failed"); - f(NAN); - return; - } - - if (active_mtreg_ != mtreg) { - // set mtreg - uint8_t mtreg_hi = BH1750_COMMAND_MT_REG_HI | ((mtreg >> 5) & 0b111); - uint8_t mtreg_lo = BH1750_COMMAND_MT_REG_LO | ((mtreg >> 0) & 0b11111); - if (this->write(&mtreg_hi, 1) != i2c::ERROR_OK || this->write(&mtreg_lo, 1) != i2c::ERROR_OK) { - ESP_LOGW(TAG, "Set measurement time failed"); - active_mtreg_ = 0; - f(NAN); - return; - } - active_mtreg_ = mtreg; - } - - uint8_t cmd; - uint16_t meas_time; - switch (mode) { - case BH1750_MODE_L: - cmd = BH1750_COMMAND_ONE_TIME_L; - meas_time = 24 * mtreg / 69; - break; - case BH1750_MODE_H: - cmd = BH1750_COMMAND_ONE_TIME_H; - meas_time = 180 * mtreg / 69; - break; - case BH1750_MODE_H2: - cmd = BH1750_COMMAND_ONE_TIME_H2; - meas_time = 180 * mtreg / 69; - break; - default: - f(NAN); - return; - } - if (this->write(&cmd, 1) != i2c::ERROR_OK) { - ESP_LOGW(TAG, "Start measurement failed"); - f(NAN); - return; - } - - // probably not needed, but adjust for rounding - meas_time++; - - this->set_timeout("read", meas_time, [this, mode, mtreg, f]() { - uint16_t raw_value; - if (this->read(reinterpret_cast(&raw_value), 2) != i2c::ERROR_OK) { - ESP_LOGW(TAG, "Read data failed"); - f(NAN); - return; - } - raw_value = i2c::i2ctohs(raw_value); - - float lx = float(raw_value) / 1.2f; - lx *= 69.0f / mtreg; - if (mode == BH1750_MODE_H2) - lx /= 2.0f; - - f(lx); - }); + this->state_ = IDLE; } void BH1750Sensor::dump_config() { @@ -124,45 +82,186 @@ void BH1750Sensor::dump_config() { } void BH1750Sensor::update() { - // first do a quick measurement in L-mode with full range - // to find right range - this->read_lx_(BH1750_MODE_L, 31, [this](float val) { - if (std::isnan(val)) { - this->status_set_warning(); - this->publish_state(NAN); + // Start coarse measurement to determine optimal mode/mtreg + if (this->state_ != IDLE) { + // Safety timeout: reset if stuck + if (millis() - this->measurement_start_time_ > MEASUREMENT_TIMEOUT_MS) { + ESP_LOGW(TAG, "Measurement timeout, resetting state"); + this->state_ = IDLE; + } else { + ESP_LOGW(TAG, "Previous measurement not complete, skipping update"); return; } + } - BH1750Mode use_mode; - uint8_t use_mtreg; - if (val <= 7000) { - use_mode = BH1750_MODE_H2; - use_mtreg = 254; - } else { - use_mode = BH1750_MODE_H; - // lx = counts / 1.2 * (69 / mtreg) - // -> mtreg = counts / 1.2 * (69 / lx) - // calculate for counts=50000 (allow some range to not saturate, but maximize mtreg) - // -> mtreg = 50000*(10/12)*(69/lx) - int ideal_mtreg = 50000 * 10 * 69 / (12 * (int) val); - use_mtreg = std::min(254, std::max(31, ideal_mtreg)); - } - ESP_LOGV(TAG, "L result: %f -> Calculated mode=%d, mtreg=%d", val, (int) use_mode, use_mtreg); + if (!this->start_measurement_(BH1750_MODE_L, 31)) { + this->status_set_warning(); + this->publish_state(NAN); + return; + } - this->read_lx_(use_mode, use_mtreg, [this](float val) { - if (std::isnan(val)) { + this->state_ = WAITING_COARSE_MEASUREMENT; + this->enable_loop(); // Enable loop while measurement in progress +} + +void BH1750Sensor::loop() { + const uint32_t now = App.get_loop_component_start_time(); + + switch (this->state_) { + case IDLE: + // Disable loop when idle to save cycles + this->disable_loop(); + break; + + case WAITING_COARSE_MEASUREMENT: + if (now - this->measurement_start_time_ >= this->measurement_duration_) { + this->state_ = READING_COARSE_RESULT; + } + break; + + case READING_COARSE_RESULT: { + float lx; + if (!this->read_measurement_(lx)) { this->status_set_warning(); this->publish_state(NAN); - return; + this->state_ = IDLE; + break; } - ESP_LOGD(TAG, "'%s': Illuminance=%.1flx", this->get_name().c_str(), val); + + this->process_coarse_result_(lx); + + // Start fine measurement with optimal settings + if (!this->start_measurement_(this->fine_mode_, this->fine_mtreg_)) { + this->status_set_warning(); + this->publish_state(NAN); + this->state_ = IDLE; + break; + } + + this->state_ = WAITING_FINE_MEASUREMENT; + break; + } + + case WAITING_FINE_MEASUREMENT: + if (now - this->measurement_start_time_ >= this->measurement_duration_) { + this->state_ = READING_FINE_RESULT; + } + break; + + case READING_FINE_RESULT: { + float lx; + if (!this->read_measurement_(lx)) { + this->status_set_warning(); + this->publish_state(NAN); + this->state_ = IDLE; + break; + } + + ESP_LOGD(TAG, "'%s': Illuminance=%.1flx", this->get_name().c_str(), lx); this->status_clear_warning(); - this->publish_state(val); - }); - }); + this->publish_state(lx); + this->state_ = IDLE; + break; + } + } +} + +bool BH1750Sensor::start_measurement_(BH1750Mode mode, uint8_t mtreg) { + // Power on + uint8_t turn_on = BH1750_COMMAND_POWER_ON; + if (this->write(&turn_on, 1) != i2c::ERROR_OK) { + ESP_LOGW(TAG, "Power on failed"); + return false; + } + + // Set MTreg if changed + if (this->active_mtreg_ != mtreg) { + uint8_t mtreg_hi = BH1750_COMMAND_MT_REG_HI | ((mtreg >> MTREG_HI_SHIFT) & MTREG_HI_MASK); + uint8_t mtreg_lo = BH1750_COMMAND_MT_REG_LO | ((mtreg >> MTREG_LO_SHIFT) & MTREG_LO_MASK); + if (this->write(&mtreg_hi, 1) != i2c::ERROR_OK || this->write(&mtreg_lo, 1) != i2c::ERROR_OK) { + ESP_LOGW(TAG, "Set measurement time failed"); + this->active_mtreg_ = 0; + return false; + } + this->active_mtreg_ = mtreg; + } + + // Start measurement + uint8_t cmd; + uint16_t meas_time; + switch (mode) { + case BH1750_MODE_L: + cmd = BH1750_COMMAND_ONE_TIME_L; + meas_time = MEAS_TIME_L_MS * mtreg / MTREG_DEFAULT; + break; + case BH1750_MODE_H: + cmd = BH1750_COMMAND_ONE_TIME_H; + meas_time = MEAS_TIME_H_MS * mtreg / MTREG_DEFAULT; + break; + case BH1750_MODE_H2: + cmd = BH1750_COMMAND_ONE_TIME_H2; + meas_time = MEAS_TIME_H_MS * mtreg / MTREG_DEFAULT; + break; + default: + return false; + } + + if (this->write(&cmd, 1) != i2c::ERROR_OK) { + ESP_LOGW(TAG, "Start measurement failed"); + return false; + } + + // Store current measurement parameters + this->current_mode_ = mode; + this->current_mtreg_ = mtreg; + this->measurement_start_time_ = millis(); + this->measurement_duration_ = meas_time + 1; // Add 1ms for safety + + return true; +} + +bool BH1750Sensor::read_measurement_(float &lx_out) { + uint16_t raw_value; + if (this->read(reinterpret_cast(&raw_value), 2) != i2c::ERROR_OK) { + ESP_LOGW(TAG, "Read data failed"); + return false; + } + raw_value = i2c::i2ctohs(raw_value); + + float lx = float(raw_value) / RESOLUTION_DIVISOR; + lx *= float(MTREG_DEFAULT) / this->current_mtreg_; + if (this->current_mode_ == BH1750_MODE_H2) { + lx /= MODE_H2_DIVISOR; + } + + lx_out = lx; + return true; +} + +void BH1750Sensor::process_coarse_result_(float lx) { + if (std::isnan(lx)) { + // Use defaults if coarse measurement failed + this->fine_mode_ = BH1750_MODE_H2; + this->fine_mtreg_ = MTREG_MAX; + return; + } + + if (lx <= HIGH_LIGHT_THRESHOLD_LX) { + this->fine_mode_ = BH1750_MODE_H2; + this->fine_mtreg_ = MTREG_MAX; + } else { + this->fine_mode_ = BH1750_MODE_H; + // lx = counts / 1.2 * (69 / mtreg) + // -> mtreg = counts / 1.2 * (69 / lx) + // calculate for counts=50000 (allow some range to not saturate, but maximize mtreg) + // -> mtreg = 50000*(10/12)*(69/lx) + int ideal_mtreg = COUNTS_TARGET * COUNTS_NUMERATOR * MTREG_DEFAULT / (COUNTS_DENOMINATOR * (int) lx); + this->fine_mtreg_ = std::min(MTREG_MAX, std::max(MTREG_MIN, ideal_mtreg)); + } + + ESP_LOGV(TAG, "L result: %.1f -> Calculated mode=%d, mtreg=%d", lx, (int) this->fine_mode_, this->fine_mtreg_); } float BH1750Sensor::get_setup_priority() const { return setup_priority::DATA; } -} // namespace bh1750 -} // namespace esphome +} // namespace esphome::bh1750 diff --git a/esphome/components/bh1750/bh1750.h b/esphome/components/bh1750/bh1750.h index a31eb33609..bd6b4c5b9a 100644 --- a/esphome/components/bh1750/bh1750.h +++ b/esphome/components/bh1750/bh1750.h @@ -4,10 +4,9 @@ #include "esphome/components/sensor/sensor.h" #include "esphome/components/i2c/i2c.h" -namespace esphome { -namespace bh1750 { +namespace esphome::bh1750 { -enum BH1750Mode { +enum BH1750Mode : uint8_t { BH1750_MODE_L, BH1750_MODE_H, BH1750_MODE_H2, @@ -21,13 +20,35 @@ class BH1750Sensor : public sensor::Sensor, public PollingComponent, public i2c: void setup() override; void dump_config() override; void update() override; + void loop() override; float get_setup_priority() const override; protected: - void read_lx_(BH1750Mode mode, uint8_t mtreg, const std::function &f); + // State machine states + enum State : uint8_t { + IDLE, + WAITING_COARSE_MEASUREMENT, + READING_COARSE_RESULT, + WAITING_FINE_MEASUREMENT, + READING_FINE_RESULT, + }; + // 4-byte aligned members + uint32_t measurement_start_time_{0}; + uint32_t measurement_duration_{0}; + + // 1-byte members grouped together to minimize padding + State state_{IDLE}; + BH1750Mode current_mode_{BH1750_MODE_L}; + uint8_t current_mtreg_{31}; + BH1750Mode fine_mode_{BH1750_MODE_H2}; + uint8_t fine_mtreg_{254}; uint8_t active_mtreg_{0}; + + // Helper methods + bool start_measurement_(BH1750Mode mode, uint8_t mtreg); + bool read_measurement_(float &lx_out); + void process_coarse_result_(float lx); }; -} // namespace bh1750 -} // namespace esphome +} // namespace esphome::bh1750 From 8934d4b498ec6f1ca02c2099bc87474984beaa18 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 16 Nov 2025 22:08:44 -0600 Subject: [PATCH 060/415] touch ups --- esphome/components/bh1750/bh1750.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/bh1750/bh1750.cpp b/esphome/components/bh1750/bh1750.cpp index f2766e3c64..16ef822b64 100644 --- a/esphome/components/bh1750/bh1750.cpp +++ b/esphome/components/bh1750/bh1750.cpp @@ -256,7 +256,7 @@ void BH1750Sensor::process_coarse_result_(float lx) { // calculate for counts=50000 (allow some range to not saturate, but maximize mtreg) // -> mtreg = 50000*(10/12)*(69/lx) int ideal_mtreg = COUNTS_TARGET * COUNTS_NUMERATOR * MTREG_DEFAULT / (COUNTS_DENOMINATOR * (int) lx); - this->fine_mtreg_ = std::min(MTREG_MAX, std::max(MTREG_MIN, ideal_mtreg)); + this->fine_mtreg_ = std::min((int) MTREG_MAX, std::max((int) MTREG_MIN, ideal_mtreg)); } ESP_LOGV(TAG, "L result: %.1f -> Calculated mode=%d, mtreg=%d", lx, (int) this->fine_mode_, this->fine_mtreg_); From 9b14444dad48e9bf3e19f83660051cfa1397ea09 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 16 Nov 2025 22:30:59 -0600 Subject: [PATCH 061/415] tidy --- esphome/components/bh1750/bh1750.cpp | 12 +++++++----- esphome/components/bh1750/bh1750.h | 2 +- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/esphome/components/bh1750/bh1750.cpp b/esphome/components/bh1750/bh1750.cpp index 16ef822b64..eb91ee0789 100644 --- a/esphome/components/bh1750/bh1750.cpp +++ b/esphome/components/bh1750/bh1750.cpp @@ -82,10 +82,12 @@ void BH1750Sensor::dump_config() { } void BH1750Sensor::update() { + const uint32_t now = millis(); + // Start coarse measurement to determine optimal mode/mtreg if (this->state_ != IDLE) { // Safety timeout: reset if stuck - if (millis() - this->measurement_start_time_ > MEASUREMENT_TIMEOUT_MS) { + if (now - this->measurement_start_time_ > MEASUREMENT_TIMEOUT_MS) { ESP_LOGW(TAG, "Measurement timeout, resetting state"); this->state_ = IDLE; } else { @@ -94,7 +96,7 @@ void BH1750Sensor::update() { } } - if (!this->start_measurement_(BH1750_MODE_L, 31)) { + if (!this->start_measurement_(BH1750_MODE_L, MTREG_MIN, now)) { this->status_set_warning(); this->publish_state(NAN); return; @@ -131,7 +133,7 @@ void BH1750Sensor::loop() { this->process_coarse_result_(lx); // Start fine measurement with optimal settings - if (!this->start_measurement_(this->fine_mode_, this->fine_mtreg_)) { + if (!this->start_measurement_(this->fine_mode_, this->fine_mtreg_, now)) { this->status_set_warning(); this->publish_state(NAN); this->state_ = IDLE; @@ -166,7 +168,7 @@ void BH1750Sensor::loop() { } } -bool BH1750Sensor::start_measurement_(BH1750Mode mode, uint8_t mtreg) { +bool BH1750Sensor::start_measurement_(BH1750Mode mode, uint8_t mtreg, uint32_t now) { // Power on uint8_t turn_on = BH1750_COMMAND_POWER_ON; if (this->write(&turn_on, 1) != i2c::ERROR_OK) { @@ -214,7 +216,7 @@ bool BH1750Sensor::start_measurement_(BH1750Mode mode, uint8_t mtreg) { // Store current measurement parameters this->current_mode_ = mode; this->current_mtreg_ = mtreg; - this->measurement_start_time_ = millis(); + this->measurement_start_time_ = now; this->measurement_duration_ = meas_time + 1; // Add 1ms for safety return true; diff --git a/esphome/components/bh1750/bh1750.h b/esphome/components/bh1750/bh1750.h index bd6b4c5b9a..b9b3fa3f47 100644 --- a/esphome/components/bh1750/bh1750.h +++ b/esphome/components/bh1750/bh1750.h @@ -46,7 +46,7 @@ class BH1750Sensor : public sensor::Sensor, public PollingComponent, public i2c: uint8_t active_mtreg_{0}; // Helper methods - bool start_measurement_(BH1750Mode mode, uint8_t mtreg); + bool start_measurement_(BH1750Mode mode, uint8_t mtreg, uint32_t now); bool read_measurement_(float &lx_out); void process_coarse_result_(float lx); }; From 78a69cb744a265bcc74a2a86c1492e9683bae92b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 16 Nov 2025 22:33:59 -0600 Subject: [PATCH 062/415] tidy --- esphome/components/bh1750/bh1750.cpp | 18 +++++++++--------- esphome/components/bh1750/bh1750.h | 1 + 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/esphome/components/bh1750/bh1750.cpp b/esphome/components/bh1750/bh1750.cpp index eb91ee0789..a2f94e1b4b 100644 --- a/esphome/components/bh1750/bh1750.cpp +++ b/esphome/components/bh1750/bh1750.cpp @@ -124,9 +124,7 @@ void BH1750Sensor::loop() { case READING_COARSE_RESULT: { float lx; if (!this->read_measurement_(lx)) { - this->status_set_warning(); - this->publish_state(NAN); - this->state_ = IDLE; + this->fail_and_reset_(); break; } @@ -134,9 +132,7 @@ void BH1750Sensor::loop() { // Start fine measurement with optimal settings if (!this->start_measurement_(this->fine_mode_, this->fine_mtreg_, now)) { - this->status_set_warning(); - this->publish_state(NAN); - this->state_ = IDLE; + this->fail_and_reset_(); break; } @@ -153,9 +149,7 @@ void BH1750Sensor::loop() { case READING_FINE_RESULT: { float lx; if (!this->read_measurement_(lx)) { - this->status_set_warning(); - this->publish_state(NAN); - this->state_ = IDLE; + this->fail_and_reset_(); break; } @@ -264,6 +258,12 @@ void BH1750Sensor::process_coarse_result_(float lx) { ESP_LOGV(TAG, "L result: %.1f -> Calculated mode=%d, mtreg=%d", lx, (int) this->fine_mode_, this->fine_mtreg_); } +void BH1750Sensor::fail_and_reset_() { + this->status_set_warning(); + this->publish_state(NAN); + this->state_ = IDLE; +} + float BH1750Sensor::get_setup_priority() const { return setup_priority::DATA; } } // namespace esphome::bh1750 diff --git a/esphome/components/bh1750/bh1750.h b/esphome/components/bh1750/bh1750.h index b9b3fa3f47..0460427954 100644 --- a/esphome/components/bh1750/bh1750.h +++ b/esphome/components/bh1750/bh1750.h @@ -49,6 +49,7 @@ class BH1750Sensor : public sensor::Sensor, public PollingComponent, public i2c: bool start_measurement_(BH1750Mode mode, uint8_t mtreg, uint32_t now); bool read_measurement_(float &lx_out); void process_coarse_result_(float lx); + void fail_and_reset_(); }; } // namespace esphome::bh1750 From 9e1f8d83f884b8fb9c168c1ea3909ef3fc365c21 Mon Sep 17 00:00:00 2001 From: Javier Peletier Date: Mon, 17 Nov 2025 08:03:11 +0100 Subject: [PATCH 063/415] [config] Support !remove and !extend with LVGL-style configs (#11534) --- esphome/config.py | 59 +++++++++++-------- .../05-extend-remove.approved.yaml | 24 ++++++++ .../substitutions/05-extend-remove.input.yaml | 44 ++++++++++++++ 3 files changed, 104 insertions(+), 23 deletions(-) diff --git a/esphome/config.py b/esphome/config.py index e508ca585b..4c8019de75 100644 --- a/esphome/config.py +++ b/esphome/config.py @@ -338,21 +338,44 @@ def check_replaceme(value): ) -def _build_list_index(lst): +def _get_item_id(item: Any) -> str | Extend | Remove | None: + """Attempts to get a list item's ID""" + if not isinstance(item, dict): + return None # not a dict, can't have ID + # 1.- Check regular case: + # - id: my_id + item_id = item.get(CONF_ID) + if item_id is None and len(item) == 1: + # 2.- Check single-key dict case: + # - obj: + # id: my_id + item = next(iter(item.values())) + if isinstance(item, dict): + item_id = item.get(CONF_ID) + if isinstance(item_id, Extend): + # Remove instances of Extend so they don't overwrite the original item when merging: + del item[CONF_ID] + return item_id + + +def _build_list_index( + lst: list[Any], +) -> tuple[ + OrderedDict[str | Extend | Remove, Any], list[tuple[int, str, Any]], set[str] +]: index = OrderedDict() extensions, removals = [], set() - for item in lst: + for pos, item in enumerate(lst): if item is None: removals.add(None) continue - item_id = None - if isinstance(item, dict) and (item_id := item.get(CONF_ID)): - if isinstance(item_id, Extend): - extensions.append(item) - continue - if isinstance(item_id, Remove): - removals.add(item_id.value) - continue + item_id = _get_item_id(item) + if isinstance(item_id, Extend): + extensions.append((pos, item_id.value, item)) + continue + if isinstance(item_id, Remove): + removals.add(item_id.value) + continue if not item_id or item_id in index: # no id or duplicate -> pass through with identity-based key item_id = id(item) @@ -360,7 +383,7 @@ def _build_list_index(lst): return index, extensions, removals -def resolve_extend_remove(value, is_key=None): +def resolve_extend_remove(value: Any, is_key: bool = False) -> None: if isinstance(value, ESPLiteralValue): return # do not check inside literal blocks if isinstance(value, list): @@ -368,26 +391,16 @@ def resolve_extend_remove(value, is_key=None): if extensions or removals: # Rebuild the original list after # processing all extensions and removals - for item in extensions: - item_id = item[CONF_ID].value + for pos, item_id, item in extensions: if item_id in removals: continue old = index.get(item_id) if old is None: # Failed to find source for extension - # Find index of item to show error at correct position - i = next( - ( - i - for i, d in enumerate(value) - if d.get(CONF_ID) == item[CONF_ID] - ) - ) - with cv.prepend_path(i): + with cv.prepend_path(pos): raise cv.Invalid( f"Source for extension of ID '{item_id}' was not found." ) - item[CONF_ID] = item_id index[item_id] = merge_config(old, item) for item_id in removals: index.pop(item_id, None) diff --git a/tests/unit_tests/fixtures/substitutions/05-extend-remove.approved.yaml b/tests/unit_tests/fixtures/substitutions/05-extend-remove.approved.yaml index a479370f4b..35e3e6258f 100644 --- a/tests/unit_tests/fixtures/substitutions/05-extend-remove.approved.yaml +++ b/tests/unit_tests/fixtures/substitutions/05-extend-remove.approved.yaml @@ -7,3 +7,27 @@ some_component: value: 2 - id: component2 value: 5 +lvgl: + pages: + - id: page1 + widgets: + - obj: + id: object1 + x: 3 + y: 2 + width: 4 + - obj: + id: object3 + x: 6 + y: 12 + widgets: + - obj: + id: object4 + x: 14 + y: 9 + width: 15 + height: 13 + - obj: + id: object5 + x: 10 + y: 11 diff --git a/tests/unit_tests/fixtures/substitutions/05-extend-remove.input.yaml b/tests/unit_tests/fixtures/substitutions/05-extend-remove.input.yaml index 2e0e60798d..617f09c31c 100644 --- a/tests/unit_tests/fixtures/substitutions/05-extend-remove.input.yaml +++ b/tests/unit_tests/fixtures/substitutions/05-extend-remove.input.yaml @@ -13,6 +13,30 @@ packages: value: 5 - id: component3 value: 6 + - lvgl: + pages: + - id: page1 + widgets: + - obj: + id: object1 + x: 1 + y: 2 + - obj: + id: object2 + x: 5 + - obj: + id: object3 + x: 6 + y: 7 + widgets: + - obj: + id: object4 + x: 8 + y: 9 + - obj: + id: object5 + x: 10 + y: 11 some_component: - id: !extend ${A} @@ -20,3 +44,23 @@ some_component: - id: component2 value: 3 - id: !remove ${C} + +lvgl: + pages: + - id: !extend page1 + widgets: + - obj: + id: !extend object1 + x: 3 + width: 4 + - obj: + id: !remove object2 + - obj: + id: !extend object3 + y: 12 + height: 13 + widgets: + - obj: + id: !extend object4 + x: 14 + width: 15 From a6f416a09e435c77f5614ba5da5bde3624dc8348 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 17 Nov 2025 08:48:13 -0600 Subject: [PATCH 064/415] Update esphome/components/bh1750/bh1750.cpp --- esphome/components/bh1750/bh1750.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/bh1750/bh1750.cpp b/esphome/components/bh1750/bh1750.cpp index a2f94e1b4b..913b1b3cd3 100644 --- a/esphome/components/bh1750/bh1750.cpp +++ b/esphome/components/bh1750/bh1750.cpp @@ -131,7 +131,7 @@ void BH1750Sensor::loop() { this->process_coarse_result_(lx); // Start fine measurement with optimal settings - if (!this->start_measurement_(this->fine_mode_, this->fine_mtreg_, now)) { + if (!this->start_measurement_(this->fine_mode_, this->fine_mtreg_, millis())) { this->fail_and_reset_(); break; } From 41ac12a0e133ac81d02950542f61fd024851726c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 17 Nov 2025 08:48:42 -0600 Subject: [PATCH 065/415] Update esphome/components/bh1750/bh1750.cpp --- esphome/components/bh1750/bh1750.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/esphome/components/bh1750/bh1750.cpp b/esphome/components/bh1750/bh1750.cpp index 913b1b3cd3..bd7c667c25 100644 --- a/esphome/components/bh1750/bh1750.cpp +++ b/esphome/components/bh1750/bh1750.cpp @@ -131,6 +131,7 @@ void BH1750Sensor::loop() { this->process_coarse_result_(lx); // Start fine measurement with optimal settings + // fetch millis() again since the read can take a bit if (!this->start_measurement_(this->fine_mode_, this->fine_mtreg_, millis())) { this->fail_and_reset_(); break; From 43f2405dc3661004af767b9c5c9d33fb2285ddfe Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 17 Nov 2025 09:10:01 -0600 Subject: [PATCH 066/415] [dashboard_import] Store package import URL in .rodata instead of RAM --- esphome/components/dashboard_import/dashboard_import.cpp | 6 +++--- esphome/components/dashboard_import/dashboard_import.h | 6 ++---- esphome/components/mdns/mdns_component.cpp | 3 +-- 3 files changed, 6 insertions(+), 9 deletions(-) diff --git a/esphome/components/dashboard_import/dashboard_import.cpp b/esphome/components/dashboard_import/dashboard_import.cpp index c04696fd53..d4a95b81f6 100644 --- a/esphome/components/dashboard_import/dashboard_import.cpp +++ b/esphome/components/dashboard_import/dashboard_import.cpp @@ -3,10 +3,10 @@ namespace esphome { namespace dashboard_import { -static std::string g_package_import_url; // NOLINT +static const char *g_package_import_url = ""; // NOLINT -const std::string &get_package_import_url() { return g_package_import_url; } -void set_package_import_url(std::string url) { g_package_import_url = std::move(url); } +const char *get_package_import_url() { return g_package_import_url; } +void set_package_import_url(const char *url) { g_package_import_url = url; } } // namespace dashboard_import } // namespace esphome diff --git a/esphome/components/dashboard_import/dashboard_import.h b/esphome/components/dashboard_import/dashboard_import.h index edcda6b803..488bf80a2e 100644 --- a/esphome/components/dashboard_import/dashboard_import.h +++ b/esphome/components/dashboard_import/dashboard_import.h @@ -1,12 +1,10 @@ #pragma once -#include - namespace esphome { namespace dashboard_import { -const std::string &get_package_import_url(); -void set_package_import_url(std::string url); +const char *get_package_import_url(); +void set_package_import_url(const char *url); } // namespace dashboard_import } // namespace esphome diff --git a/esphome/components/mdns/mdns_component.cpp b/esphome/components/mdns/mdns_component.cpp index 2c3150ff5d..b66129404e 100644 --- a/esphome/components/mdns/mdns_component.cpp +++ b/esphome/components/mdns/mdns_component.cpp @@ -135,8 +135,7 @@ void MDNSComponent::compile_records_(StaticVector Date: Mon, 17 Nov 2025 09:35:05 -0600 Subject: [PATCH 067/415] [api] Reduce heap allocations in DeviceInfoResponse --- esphome/components/api/api_connection.cpp | 12 ++++++++---- esphome/components/bluetooth_proxy/bluetooth_proxy.h | 6 ++---- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 4acd2fc15c..b05b790c2a 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -1451,8 +1451,11 @@ bool APIConnection::send_device_info_response(const DeviceInfoRequest &msg) { #ifdef USE_AREAS resp.set_suggested_area(StringRef(App.get_area())); #endif - // mac_address must store temporary string - will be valid during send_message call - std::string mac_address = get_mac_address_pretty(); + // Stack buffer for MAC address (XX:XX:XX:XX:XX:XX\0 = 18 bytes) + char mac_address[18]; + uint8_t mac[6]; + get_mac_address_raw(mac); + format_mac_addr_upper(mac, mac_address); resp.set_mac_address(StringRef(mac_address)); resp.set_esphome_version(ESPHOME_VERSION_REF); @@ -1493,8 +1496,9 @@ bool APIConnection::send_device_info_response(const DeviceInfoRequest &msg) { #endif #ifdef USE_BLUETOOTH_PROXY resp.bluetooth_proxy_feature_flags = bluetooth_proxy::global_bluetooth_proxy->get_feature_flags(); - // bt_mac must store temporary string - will be valid during send_message call - std::string bluetooth_mac = bluetooth_proxy::global_bluetooth_proxy->get_bluetooth_mac_address_pretty(); + // Stack buffer for Bluetooth MAC address (XX:XX:XX:XX:XX:XX\0 = 18 bytes) + char bluetooth_mac[18]; + bluetooth_proxy::global_bluetooth_proxy->get_bluetooth_mac_address_pretty(bluetooth_mac); resp.set_bluetooth_mac_address(StringRef(bluetooth_mac)); #endif #ifdef USE_VOICE_ASSISTANT diff --git a/esphome/components/bluetooth_proxy/bluetooth_proxy.h b/esphome/components/bluetooth_proxy/bluetooth_proxy.h index a5f0fbe32f..4de541fac2 100644 --- a/esphome/components/bluetooth_proxy/bluetooth_proxy.h +++ b/esphome/components/bluetooth_proxy/bluetooth_proxy.h @@ -130,11 +130,9 @@ class BluetoothProxy final : public esp32_ble_tracker::ESPBTDeviceListener, publ return flags; } - std::string get_bluetooth_mac_address_pretty() { + void get_bluetooth_mac_address_pretty(std::span output) { const uint8_t *mac = esp_bt_dev_get_address(); - char buf[18]; - format_mac_addr_upper(mac, buf); - return std::string(buf); + format_mac_addr_upper(mac, output.data()); } protected: From 3d6c361037098f2a7990d1c41c0b4b50788afb64 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Mon, 17 Nov 2025 12:32:08 -0500 Subject: [PATCH 068/415] [core] Add support for setting environment variables (#11953) --- esphome/const.py | 1 + esphome/core/config.py | 15 +++++++++++++++ tests/components/esphome/common.yaml | 3 +++ 3 files changed, 19 insertions(+) diff --git a/esphome/const.py b/esphome/const.py index a25114d80e..b4cd3cfd1c 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -336,6 +336,7 @@ CONF_ENERGY = "energy" CONF_ENTITY_CATEGORY = "entity_category" CONF_ENTITY_ID = "entity_id" CONF_ENUM_DATAPOINT = "enum_datapoint" +CONF_ENVIRONMENT_VARIABLES = "environment_variables" CONF_EQUATION = "equation" CONF_ESP8266_DISABLE_SSL_SUPPORT = "esp8266_disable_ssl_support" CONF_ESPHOME = "esphome" diff --git a/esphome/core/config.py b/esphome/core/config.py index 763f9ebd9f..0a239c5f5e 100644 --- a/esphome/core/config.py +++ b/esphome/core/config.py @@ -17,6 +17,7 @@ from esphome.const import ( CONF_COMPILE_PROCESS_LIMIT, CONF_DEBUG_SCHEDULER, CONF_DEVICES, + CONF_ENVIRONMENT_VARIABLES, CONF_ESPHOME, CONF_FRIENDLY_NAME, CONF_ID, @@ -215,6 +216,11 @@ CONFIG_SCHEMA = cv.All( cv.string_strict: cv.Any([cv.string], cv.string), } ), + cv.Optional(CONF_ENVIRONMENT_VARIABLES, default={}): cv.Schema( + { + cv.string_strict: cv.string, + } + ), cv.Optional(CONF_ON_BOOT): automation.validate_automation( { cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(StartupTrigger), @@ -426,6 +432,12 @@ async def _add_platformio_options(pio_options): cg.add_platformio_option(key, val) +@coroutine_with_priority(CoroPriority.FINAL) +async def _add_environment_variables(env_vars: dict[str, str]) -> None: + # Set environment variables for the build process + os.environ.update(env_vars) + + @coroutine_with_priority(CoroPriority.AUTOMATION) async def _add_automations(config): for conf in config.get(CONF_ON_BOOT, []): @@ -563,6 +575,9 @@ async def to_code(config: ConfigType) -> None: if config[CONF_PLATFORMIO_OPTIONS]: CORE.add_job(_add_platformio_options, config[CONF_PLATFORMIO_OPTIONS]) + if config[CONF_ENVIRONMENT_VARIABLES]: + CORE.add_job(_add_environment_variables, config[CONF_ENVIRONMENT_VARIABLES]) + # Process areas all_areas: list[dict[str, str | core.ID]] = [] if CONF_AREA in config: diff --git a/tests/components/esphome/common.yaml b/tests/components/esphome/common.yaml index b2d7bccaa5..db75b08b38 100644 --- a/tests/components/esphome/common.yaml +++ b/tests/components/esphome/common.yaml @@ -2,6 +2,9 @@ esphome: debug_scheduler: true platformio_options: board_build.flash_mode: dio + environment_variables: + TEST_ENV_VAR: "test_value" + BUILD_NUMBER: "12345" area: id: testing_area name: Testing Area From 7a238028a7de73b37a6edd8abf0083cdca00aeb7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 17 Nov 2025 15:38:44 -0600 Subject: [PATCH 069/415] Bump ruamel-yaml-clib from 0.2.14 to 0.2.15 (#11956) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 40802422f2..6ae050b35b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -16,7 +16,7 @@ aioesphomeapi==42.7.0 zeroconf==0.148.0 puremagic==1.30 ruamel.yaml==0.18.16 # dashboard_import -ruamel.yaml.clib==0.2.14 # dashboard_import +ruamel.yaml.clib==0.2.15 # dashboard_import esphome-glyphsets==0.2.0 pillow==11.3.0 cairosvg==2.8.2 From 23f85162d0dd2216001812d79b60786a12f8a130 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 17 Nov 2025 15:39:01 -0600 Subject: [PATCH 070/415] Bump actions/checkout from 5.0.0 to 5.0.1 (#11957) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/auto-label-pr.yml | 2 +- .github/workflows/ci-api-proto.yml | 2 +- .github/workflows/ci-clang-tidy-hash.yml | 2 +- .github/workflows/ci-docker.yml | 2 +- .../workflows/ci-memory-impact-comment.yml | 2 +- .github/workflows/ci.yml | 30 +++++++++---------- .github/workflows/codeql.yml | 2 +- .github/workflows/release.yml | 8 ++--- .github/workflows/sync-device-classes.yml | 4 +-- 9 files changed, 27 insertions(+), 27 deletions(-) diff --git a/.github/workflows/auto-label-pr.yml b/.github/workflows/auto-label-pr.yml index dd1bc29d83..fb284c9d8c 100644 --- a/.github/workflows/auto-label-pr.yml +++ b/.github/workflows/auto-label-pr.yml @@ -22,7 +22,7 @@ jobs: if: github.event.action != 'labeled' || github.event.sender.type != 'Bot' steps: - name: Checkout - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 - name: Generate a token id: generate-token diff --git a/.github/workflows/ci-api-proto.yml b/.github/workflows/ci-api-proto.yml index 400373679f..be5af1aff1 100644 --- a/.github/workflows/ci-api-proto.yml +++ b/.github/workflows/ci-api-proto.yml @@ -21,7 +21,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 - name: Set up Python uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0 with: diff --git a/.github/workflows/ci-clang-tidy-hash.yml b/.github/workflows/ci-clang-tidy-hash.yml index 78d1c2b87f..aebf07949d 100644 --- a/.github/workflows/ci-clang-tidy-hash.yml +++ b/.github/workflows/ci-clang-tidy-hash.yml @@ -21,7 +21,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 - name: Set up Python uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0 diff --git a/.github/workflows/ci-docker.yml b/.github/workflows/ci-docker.yml index 7111c61dda..9bb983b993 100644 --- a/.github/workflows/ci-docker.yml +++ b/.github/workflows/ci-docker.yml @@ -43,7 +43,7 @@ jobs: - "docker" # - "lint" steps: - - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 - name: Set up Python uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0 with: diff --git a/.github/workflows/ci-memory-impact-comment.yml b/.github/workflows/ci-memory-impact-comment.yml index eea1d2c148..c82ae30f55 100644 --- a/.github/workflows/ci-memory-impact-comment.yml +++ b/.github/workflows/ci-memory-impact-comment.yml @@ -49,7 +49,7 @@ jobs: - name: Check out code from base repository if: steps.pr.outputs.skip != 'true' - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 with: # Always check out from the base repository (esphome/esphome), never from forks # Use the PR's target branch to ensure we run trusted code from the main repo diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 16837b3186..5293c62d34 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -36,7 +36,7 @@ jobs: cache-key: ${{ steps.cache-key.outputs.key }} steps: - name: Check out code from GitHub - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 - name: Generate cache-key id: cache-key run: echo key="${{ hashFiles('requirements.txt', 'requirements_test.txt', '.pre-commit-config.yaml') }}" >> $GITHUB_OUTPUT @@ -70,7 +70,7 @@ jobs: if: needs.determine-jobs.outputs.python-linters == 'true' steps: - name: Check out code from GitHub - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 - name: Restore Python uses: ./.github/actions/restore-python with: @@ -91,7 +91,7 @@ jobs: - common steps: - name: Check out code from GitHub - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 - name: Restore Python uses: ./.github/actions/restore-python with: @@ -132,7 +132,7 @@ jobs: - common steps: - name: Check out code from GitHub - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 - name: Restore Python id: restore-python uses: ./.github/actions/restore-python @@ -183,7 +183,7 @@ jobs: component-test-batches: ${{ steps.determine.outputs.component-test-batches }} steps: - name: Check out code from GitHub - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 with: # Fetch enough history to find the merge base fetch-depth: 2 @@ -237,7 +237,7 @@ jobs: if: needs.determine-jobs.outputs.integration-tests == 'true' steps: - name: Check out code from GitHub - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 - name: Set up Python 3.13 id: python uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0 @@ -273,7 +273,7 @@ jobs: if: github.event_name == 'pull_request' && (needs.determine-jobs.outputs.cpp-unit-tests-run-all == 'true' || needs.determine-jobs.outputs.cpp-unit-tests-components != '[]') steps: - name: Check out code from GitHub - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 - name: Restore Python uses: ./.github/actions/restore-python @@ -321,7 +321,7 @@ jobs: steps: - name: Check out code from GitHub - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 with: # Need history for HEAD~1 to work for checking changed files fetch-depth: 2 @@ -400,7 +400,7 @@ jobs: GH_TOKEN: ${{ github.token }} steps: - name: Check out code from GitHub - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 with: # Need history for HEAD~1 to work for checking changed files fetch-depth: 2 @@ -489,7 +489,7 @@ jobs: steps: - name: Check out code from GitHub - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 with: # Need history for HEAD~1 to work for checking changed files fetch-depth: 2 @@ -577,7 +577,7 @@ jobs: version: 1.0 - name: Check out code from GitHub - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 - name: Restore Python uses: ./.github/actions/restore-python with: @@ -662,7 +662,7 @@ jobs: if: github.event_name == 'pull_request' && !startsWith(github.base_ref, 'beta') && !startsWith(github.base_ref, 'release') steps: - name: Check out code from GitHub - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 - name: Restore Python uses: ./.github/actions/restore-python with: @@ -688,7 +688,7 @@ jobs: skip: ${{ steps.check-script.outputs.skip }} steps: - name: Check out target branch - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 with: ref: ${{ github.base_ref }} @@ -840,7 +840,7 @@ jobs: flash_usage: ${{ steps.extract.outputs.flash_usage }} steps: - name: Check out PR branch - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 - name: Restore Python uses: ./.github/actions/restore-python with: @@ -908,7 +908,7 @@ jobs: GH_TOKEN: ${{ github.token }} steps: - name: Check out code - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 - name: Restore Python uses: ./.github/actions/restore-python with: diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 2273975328..adb2a9d79f 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -54,7 +54,7 @@ jobs: # your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages steps: - name: Checkout repository - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 75d88abf29..a064f6ef3a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -20,7 +20,7 @@ jobs: branch_build: ${{ steps.tag.outputs.branch_build }} deploy_env: ${{ steps.tag.outputs.deploy_env }} steps: - - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 - name: Get tag id: tag # yamllint disable rule:line-length @@ -60,7 +60,7 @@ jobs: contents: read id-token: write steps: - - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 - name: Set up Python uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0 with: @@ -92,7 +92,7 @@ jobs: os: "ubuntu-24.04-arm" steps: - - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 - name: Set up Python uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0 with: @@ -168,7 +168,7 @@ jobs: - ghcr - dockerhub steps: - - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 - name: Download digests uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0 diff --git a/.github/workflows/sync-device-classes.yml b/.github/workflows/sync-device-classes.yml index 9479645ccc..4fc287b067 100644 --- a/.github/workflows/sync-device-classes.yml +++ b/.github/workflows/sync-device-classes.yml @@ -13,10 +13,10 @@ jobs: if: github.repository == 'esphome/esphome' steps: - name: Checkout - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 - name: Checkout Home Assistant - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 with: repository: home-assistant/core path: lib/home-assistant From 1a73f49cd23080a500be9fd9d98de3ebccd02fbe Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 17 Nov 2025 17:20:18 -0600 Subject: [PATCH 071/415] [number] Modernize to C++17 nested namespaces (#11945) --- esphome/components/number/automation.cpp | 6 ++---- esphome/components/number/automation.h | 6 ++---- esphome/components/number/number.cpp | 6 ++---- esphome/components/number/number.h | 6 ++---- esphome/components/number/number_call.cpp | 6 ++---- esphome/components/number/number_call.h | 6 ++---- esphome/components/number/number_traits.cpp | 6 ++---- esphome/components/number/number_traits.h | 6 ++---- 8 files changed, 16 insertions(+), 32 deletions(-) diff --git a/esphome/components/number/automation.cpp b/esphome/components/number/automation.cpp index bfc59d0465..78ffc255fe 100644 --- a/esphome/components/number/automation.cpp +++ b/esphome/components/number/automation.cpp @@ -1,8 +1,7 @@ #include "automation.h" #include "esphome/core/log.h" -namespace esphome { -namespace number { +namespace esphome::number { static const char *const TAG = "number.automation"; @@ -52,5 +51,4 @@ void ValueRangeTrigger::on_state_(float state) { this->rtc_.save(&in_range); } -} // namespace number -} // namespace esphome +} // namespace esphome::number diff --git a/esphome/components/number/automation.h b/esphome/components/number/automation.h index 79eba883c4..a7cd04f083 100644 --- a/esphome/components/number/automation.h +++ b/esphome/components/number/automation.h @@ -4,8 +4,7 @@ #include "esphome/core/automation.h" #include "esphome/core/component.h" -namespace esphome { -namespace number { +namespace esphome::number { class NumberStateTrigger : public Trigger { public: @@ -91,5 +90,4 @@ template class NumberInRangeCondition : public Condition float max_{NAN}; }; -} // namespace number -} // namespace esphome +} // namespace esphome::number diff --git a/esphome/components/number/number.cpp b/esphome/components/number/number.cpp index f12e0e9e1e..992100ead0 100644 --- a/esphome/components/number/number.cpp +++ b/esphome/components/number/number.cpp @@ -3,8 +3,7 @@ #include "esphome/core/controller_registry.h" #include "esphome/core/log.h" -namespace esphome { -namespace number { +namespace esphome::number { static const char *const TAG = "number"; @@ -43,5 +42,4 @@ void Number::add_on_state_callback(std::function &&callback) { this->state_callback_.add(std::move(callback)); } -} // namespace number -} // namespace esphome +} // namespace esphome::number diff --git a/esphome/components/number/number.h b/esphome/components/number/number.h index da91d70d53..472e06ad61 100644 --- a/esphome/components/number/number.h +++ b/esphome/components/number/number.h @@ -6,8 +6,7 @@ #include "number_call.h" #include "number_traits.h" -namespace esphome { -namespace number { +namespace esphome::number { class Number; void log_number(const char *tag, const char *prefix, const char *type, Number *obj); @@ -53,5 +52,4 @@ class Number : public EntityBase { CallbackManager state_callback_; }; -} // namespace number -} // namespace esphome +} // namespace esphome::number diff --git a/esphome/components/number/number_call.cpp b/esphome/components/number/number_call.cpp index 669dd65184..27a857c112 100644 --- a/esphome/components/number/number_call.cpp +++ b/esphome/components/number/number_call.cpp @@ -2,8 +2,7 @@ #include "number.h" #include "esphome/core/log.h" -namespace esphome { -namespace number { +namespace esphome::number { static const char *const TAG = "number"; @@ -125,5 +124,4 @@ void NumberCall::perform() { this->parent_->control(target_value); } -} // namespace number -} // namespace esphome +} // namespace esphome::number diff --git a/esphome/components/number/number_call.h b/esphome/components/number/number_call.h index 807207f0ec..0f6889dcb6 100644 --- a/esphome/components/number/number_call.h +++ b/esphome/components/number/number_call.h @@ -4,8 +4,7 @@ #include "esphome/core/log.h" #include "number_traits.h" -namespace esphome { -namespace number { +namespace esphome::number { class Number; @@ -44,5 +43,4 @@ class NumberCall { bool cycle_; }; -} // namespace number -} // namespace esphome +} // namespace esphome::number diff --git a/esphome/components/number/number_traits.cpp b/esphome/components/number/number_traits.cpp index 89035661f5..1e4239ceca 100644 --- a/esphome/components/number/number_traits.cpp +++ b/esphome/components/number/number_traits.cpp @@ -1,10 +1,8 @@ #include "esphome/core/log.h" #include "number_traits.h" -namespace esphome { -namespace number { +namespace esphome::number { static const char *const TAG = "number"; -} // namespace number -} // namespace esphome +} // namespace esphome::number diff --git a/esphome/components/number/number_traits.h b/esphome/components/number/number_traits.h index fa68c2390a..5ccbb9ba48 100644 --- a/esphome/components/number/number_traits.h +++ b/esphome/components/number/number_traits.h @@ -3,8 +3,7 @@ #include "esphome/core/entity_base.h" #include "esphome/core/helpers.h" -namespace esphome { -namespace number { +namespace esphome::number { enum NumberMode : uint8_t { NUMBER_MODE_AUTO = 0, @@ -35,5 +34,4 @@ class NumberTraits : public EntityBase_DeviceClass, public EntityBase_UnitOfMeas NumberMode mode_{NUMBER_MODE_AUTO}; }; -} // namespace number -} // namespace esphome +} // namespace esphome::number From fdc7ae776071c0638774f122d3f216f2ed45af48 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 17 Nov 2025 17:20:32 -0600 Subject: [PATCH 072/415] [wifi] Skip redundant setter calls for default values (#11943) --- esphome/components/wifi/__init__.py | 9 ++++++--- esphome/components/wifi/wifi_component.h | 2 +- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/esphome/components/wifi/__init__.py b/esphome/components/wifi/__init__.py index 11bd7798e2..f543d972c9 100644 --- a/esphome/components/wifi/__init__.py +++ b/esphome/components/wifi/__init__.py @@ -479,11 +479,14 @@ async def to_code(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])) + # passive_scan defaults to false in C++ - only set if true + if config[CONF_PASSIVE_SCAN]: + cg.add(var.set_passive_scan(True)) if CONF_OUTPUT_POWER in config: cg.add(var.set_output_power(config[CONF_OUTPUT_POWER])) - - cg.add(var.set_enable_on_boot(config[CONF_ENABLE_ON_BOOT])) + # enable_on_boot defaults to true in C++ - only set if false + if not config[CONF_ENABLE_ON_BOOT]: + cg.add(var.set_enable_on_boot(False)) if CORE.is_esp8266: cg.add_library("ESP8266WiFi", None) diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index 2fd7fa6cd4..66e2ccf1cb 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -526,7 +526,7 @@ class WiFiComponent : public Component { bool btm_{false}; bool rrm_{false}; #endif - bool enable_on_boot_; + bool enable_on_boot_{true}; bool got_ipv4_address_{false}; bool keep_scan_results_{false}; From 53bab0085830336c6e5265397af74560450bf863 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 17 Nov 2025 17:27:08 -0600 Subject: [PATCH 073/415] [ld24xx] Use stack allocation for MAC and version formatting --- esphome/components/ld2410/ld2410.cpp | 26 ++++++++++++-------------- esphome/components/ld2412/ld2412.cpp | 26 ++++++++++++-------------- esphome/components/ld2450/ld2450.cpp | 26 ++++++++++++-------------- esphome/components/ld24xx/ld24xx.h | 23 +++++++++++++++++++++++ 4 files changed, 59 insertions(+), 42 deletions(-) diff --git a/esphome/components/ld2410/ld2410.cpp b/esphome/components/ld2410/ld2410.cpp index 608882565f..391f2024cd 100644 --- a/esphome/components/ld2410/ld2410.cpp +++ b/esphome/components/ld2410/ld2410.cpp @@ -13,8 +13,6 @@ namespace esphome { namespace ld2410 { static const char *const TAG = "ld2410"; -static const char *const UNKNOWN_MAC = "unknown"; -static const char *const VERSION_FMT = "%u.%02X.%02X%02X%02X%02X"; enum BaudRate : uint8_t { BAUD_RATE_9600 = 1, @@ -181,15 +179,15 @@ static inline bool validate_header_footer(const uint8_t *header_footer, const ui } void LD2410Component::dump_config() { - std::string mac_str = - mac_address_is_valid(this->mac_address_) ? format_mac_address_pretty(this->mac_address_) : UNKNOWN_MAC; - std::string version = str_sprintf(VERSION_FMT, this->version_[1], this->version_[0], this->version_[5], - this->version_[4], this->version_[3], this->version_[2]); + char mac_s[18]; + char version_s[20]; + const char *mac_str = ld24xx::format_mac_str(this->mac_address_, mac_s); + ld24xx::format_version_str(this->version_, version_s); ESP_LOGCONFIG(TAG, "LD2410:\n" " Firmware version: %s\n" " MAC address: %s", - version.c_str(), mac_str.c_str()); + version_s, mac_str); #ifdef USE_BINARY_SENSOR ESP_LOGCONFIG(TAG, "Binary Sensors:"); LOG_BINARY_SENSOR(" ", "Target", this->target_binary_sensor_); @@ -448,12 +446,12 @@ bool LD2410Component::handle_ack_data_() { case CMD_QUERY_VERSION: { std::memcpy(this->version_, &this->buffer_data_[12], sizeof(this->version_)); - std::string version = str_sprintf(VERSION_FMT, this->version_[1], this->version_[0], this->version_[5], - this->version_[4], this->version_[3], this->version_[2]); - ESP_LOGV(TAG, "Firmware version: %s", version.c_str()); + char version_s[20]; + ld24xx::format_version_str(this->version_, version_s); + ESP_LOGV(TAG, "Firmware version: %s", version_s); #ifdef USE_TEXT_SENSOR if (this->version_text_sensor_ != nullptr) { - this->version_text_sensor_->publish_state(version); + this->version_text_sensor_->publish_state(version_s); } #endif break; @@ -506,9 +504,9 @@ bool LD2410Component::handle_ack_data_() { std::memcpy(this->mac_address_, &this->buffer_data_[10], sizeof(this->mac_address_)); } - std::string mac_str = - mac_address_is_valid(this->mac_address_) ? format_mac_address_pretty(this->mac_address_) : UNKNOWN_MAC; - ESP_LOGV(TAG, "MAC address: %s", mac_str.c_str()); + char mac_s[18]; + const char *mac_str = ld24xx::format_mac_str(this->mac_address_, mac_s); + ESP_LOGV(TAG, "MAC address: %s", mac_str); #ifdef USE_TEXT_SENSOR if (this->mac_text_sensor_ != nullptr) { this->mac_text_sensor_->publish_state(mac_str); diff --git a/esphome/components/ld2412/ld2412.cpp b/esphome/components/ld2412/ld2412.cpp index 5323a9a658..4f2fd7c2bd 100644 --- a/esphome/components/ld2412/ld2412.cpp +++ b/esphome/components/ld2412/ld2412.cpp @@ -14,8 +14,6 @@ namespace esphome { namespace ld2412 { static const char *const TAG = "ld2412"; -static const char *const UNKNOWN_MAC = "unknown"; -static const char *const VERSION_FMT = "%u.%02X.%02X%02X%02X%02X"; enum BaudRate : uint8_t { BAUD_RATE_9600 = 1, @@ -200,15 +198,15 @@ static inline bool validate_header_footer(const uint8_t *header_footer, const ui } void LD2412Component::dump_config() { - std::string mac_str = - mac_address_is_valid(this->mac_address_) ? format_mac_address_pretty(this->mac_address_) : UNKNOWN_MAC; - std::string version = str_sprintf(VERSION_FMT, this->version_[1], this->version_[0], this->version_[5], - this->version_[4], this->version_[3], this->version_[2]); + char mac_s[18]; + char version_s[20]; + const char *mac_str = ld24xx::format_mac_str(this->mac_address_, mac_s); + ld24xx::format_version_str(this->version_, version_s); ESP_LOGCONFIG(TAG, "LD2412:\n" " Firmware version: %s\n" " MAC address: %s", - version.c_str(), mac_str.c_str()); + version_s, mac_str); #ifdef USE_BINARY_SENSOR ESP_LOGCONFIG(TAG, "Binary Sensors:"); LOG_BINARY_SENSOR(" ", "DynamicBackgroundCorrectionStatus", @@ -492,12 +490,12 @@ bool LD2412Component::handle_ack_data_() { case CMD_QUERY_VERSION: { std::memcpy(this->version_, &this->buffer_data_[12], sizeof(this->version_)); - std::string version = str_sprintf(VERSION_FMT, this->version_[1], this->version_[0], this->version_[5], - this->version_[4], this->version_[3], this->version_[2]); - ESP_LOGV(TAG, "Firmware version: %s", version.c_str()); + char version_s[20]; + ld24xx::format_version_str(this->version_, version_s); + ESP_LOGV(TAG, "Firmware version: %s", version_s); #ifdef USE_TEXT_SENSOR if (this->version_text_sensor_ != nullptr) { - this->version_text_sensor_->publish_state(version); + this->version_text_sensor_->publish_state(version_s); } #endif break; @@ -544,9 +542,9 @@ bool LD2412Component::handle_ack_data_() { std::memcpy(this->mac_address_, &this->buffer_data_[10], sizeof(this->mac_address_)); } - std::string mac_str = - mac_address_is_valid(this->mac_address_) ? format_mac_address_pretty(this->mac_address_) : UNKNOWN_MAC; - ESP_LOGV(TAG, "MAC address: %s", mac_str.c_str()); + char mac_s[18]; + const char *mac_str = ld24xx::format_mac_str(this->mac_address_, mac_s); + ESP_LOGV(TAG, "MAC address: %s", mac_str); #ifdef USE_TEXT_SENSOR if (this->mac_text_sensor_ != nullptr) { this->mac_text_sensor_->publish_state(mac_str); diff --git a/esphome/components/ld2450/ld2450.cpp b/esphome/components/ld2450/ld2450.cpp index c9d4da47a4..8e5287aec7 100644 --- a/esphome/components/ld2450/ld2450.cpp +++ b/esphome/components/ld2450/ld2450.cpp @@ -17,8 +17,6 @@ namespace esphome { namespace ld2450 { static const char *const TAG = "ld2450"; -static const char *const UNKNOWN_MAC = "unknown"; -static const char *const VERSION_FMT = "%u.%02X.%02X%02X%02X%02X"; enum BaudRate : uint8_t { BAUD_RATE_9600 = 1, @@ -192,15 +190,15 @@ void LD2450Component::setup() { } void LD2450Component::dump_config() { - std::string mac_str = - mac_address_is_valid(this->mac_address_) ? format_mac_address_pretty(this->mac_address_) : UNKNOWN_MAC; - std::string version = str_sprintf(VERSION_FMT, this->version_[1], this->version_[0], this->version_[5], - this->version_[4], this->version_[3], this->version_[2]); + char mac_s[18]; + char version_s[20]; + const char *mac_str = ld24xx::format_mac_str(this->mac_address_, mac_s); + ld24xx::format_version_str(this->version_, version_s); ESP_LOGCONFIG(TAG, "LD2450:\n" " Firmware version: %s\n" " MAC address: %s", - version.c_str(), mac_str.c_str()); + version_s, mac_str); #ifdef USE_BINARY_SENSOR ESP_LOGCONFIG(TAG, "Binary Sensors:"); LOG_BINARY_SENSOR(" ", "MovingTarget", this->moving_target_binary_sensor_); @@ -642,12 +640,12 @@ bool LD2450Component::handle_ack_data_() { case CMD_QUERY_VERSION: { std::memcpy(this->version_, &this->buffer_data_[12], sizeof(this->version_)); - std::string version = str_sprintf(VERSION_FMT, this->version_[1], this->version_[0], this->version_[5], - this->version_[4], this->version_[3], this->version_[2]); - ESP_LOGV(TAG, "Firmware version: %s", version.c_str()); + char version_s[20]; + ld24xx::format_version_str(this->version_, version_s); + ESP_LOGV(TAG, "Firmware version: %s", version_s); #ifdef USE_TEXT_SENSOR if (this->version_text_sensor_ != nullptr) { - this->version_text_sensor_->publish_state(version); + this->version_text_sensor_->publish_state(version_s); } #endif break; @@ -663,9 +661,9 @@ bool LD2450Component::handle_ack_data_() { std::memcpy(this->mac_address_, &this->buffer_data_[10], sizeof(this->mac_address_)); } - std::string mac_str = - mac_address_is_valid(this->mac_address_) ? format_mac_address_pretty(this->mac_address_) : UNKNOWN_MAC; - ESP_LOGV(TAG, "MAC address: %s", mac_str.c_str()); + char mac_s[18]; + const char *mac_str = ld24xx::format_mac_str(this->mac_address_, mac_s); + ESP_LOGV(TAG, "MAC address: %s", mac_str); #ifdef USE_TEXT_SENSOR if (this->mac_text_sensor_ != nullptr) { this->mac_text_sensor_->publish_state(mac_str); diff --git a/esphome/components/ld24xx/ld24xx.h b/esphome/components/ld24xx/ld24xx.h index 1cd5e01163..e21c05077d 100644 --- a/esphome/components/ld24xx/ld24xx.h +++ b/esphome/components/ld24xx/ld24xx.h @@ -1,8 +1,10 @@ #pragma once #include "esphome/core/defines.h" +#include "esphome/core/helpers.h" #include +#include #ifdef USE_SENSOR #include "esphome/core/helpers.h" @@ -39,6 +41,27 @@ namespace esphome { namespace ld24xx { +static const char *const UNKNOWN_MAC = "unknown"; +static const char *const VERSION_FMT = "%u.%02X.%02X%02X%02X%02X"; + +// Helper function to format MAC address with stack allocation +// Returns pointer to UNKNOWN_MAC constant or formatted buffer +// Buffer must be exactly 18 bytes (17 for "XX:XX:XX:XX:XX:XX" + null terminator) +inline const char *format_mac_str(const uint8_t *mac_address, std::span buffer) { + if (mac_address_is_valid(mac_address)) { + format_mac_addr_upper(mac_address, buffer.data()); + return buffer.data(); + } + return UNKNOWN_MAC; +} + +// Helper function to format firmware version with stack allocation +// Buffer must be exactly 20 bytes (format: "x.xxXXXXXX" fits in 11 + null terminator, 20 for safety) +inline void format_version_str(const uint8_t *version, std::span buffer) { + snprintf(buffer.data(), buffer.size(), VERSION_FMT, version[1], version[0], version[5], version[4], version[3], + version[2]); +} + #ifdef USE_SENSOR // Helper class to store a sensor with a deduplicator & publish state only when the value changes template class SensorWithDedup { From 547f69011b104fd9d7b1740eb23050c2c81dda47 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 17 Nov 2025 17:33:52 -0600 Subject: [PATCH 074/415] tidy --- esphome/components/ld24xx/ld24xx.h | 1 - 1 file changed, 1 deletion(-) diff --git a/esphome/components/ld24xx/ld24xx.h b/esphome/components/ld24xx/ld24xx.h index e21c05077d..e695b00705 100644 --- a/esphome/components/ld24xx/ld24xx.h +++ b/esphome/components/ld24xx/ld24xx.h @@ -7,7 +7,6 @@ #include #ifdef USE_SENSOR -#include "esphome/core/helpers.h" #include "esphome/components/sensor/sensor.h" #define SUB_SENSOR_WITH_DEDUP(name, dedup_type) \ From 6d67fd0b81a83e4e55ae9f45e30af374fcb17a35 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 17 Nov 2025 17:53:23 -0600 Subject: [PATCH 075/415] [wifi/captive_portal/web_server/wifi_info] Use stack allocation for MAC address formatting --- esphome/components/captive_portal/captive_portal.cpp | 6 ++++-- esphome/components/web_server/web_server.cpp | 4 ++-- esphome/components/wifi/wifi_component.cpp | 6 ++++-- esphome/components/wifi_info/wifi_info_text_sensor.h | 5 ++++- esphome/core/helpers.cpp | 5 ++--- esphome/core/helpers.h | 10 ++++++++++ 6 files changed, 26 insertions(+), 10 deletions(-) diff --git a/esphome/components/captive_portal/captive_portal.cpp b/esphome/components/captive_portal/captive_portal.cpp index 30438747f2..9ab9dcce86 100644 --- a/esphome/components/captive_portal/captive_portal.cpp +++ b/esphome/components/captive_portal/captive_portal.cpp @@ -13,14 +13,16 @@ static const char *const TAG = "captive_portal"; void CaptivePortal::handle_config(AsyncWebServerRequest *request) { AsyncResponseStream *stream = request->beginResponseStream(ESPHOME_F("application/json")); stream->addHeader(ESPHOME_F("cache-control"), ESPHOME_F("public, max-age=0, must-revalidate")); + char mac_s[18]; + const char *mac_str = get_mac_address_pretty_into_buffer(mac_s); #ifdef USE_ESP8266 stream->print(ESPHOME_F("{\"mac\":\"")); - stream->print(get_mac_address_pretty().c_str()); + stream->print(mac_str); stream->print(ESPHOME_F("\",\"name\":\"")); stream->print(App.get_name().c_str()); stream->print(ESPHOME_F("\",\"aps\":[{}")); #else - stream->printf(R"({"mac":"%s","name":"%s","aps":[{})", get_mac_address_pretty().c_str(), App.get_name().c_str()); + stream->printf(R"({"mac":"%s","name":"%s","aps":[{})", mac_str, App.get_name().c_str()); #endif for (auto &scan : wifi::global_wifi_component->get_scan_result()) { diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index 5a8128ba43..cc51463fe7 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -359,8 +359,8 @@ void WebServer::handle_pna_cors_request(AsyncWebServerRequest *request) { AsyncWebServerResponse *response = request->beginResponse(200, ""); response->addHeader(HEADER_CORS_ALLOW_PNA, "true"); response->addHeader(HEADER_PNA_NAME, App.get_name().c_str()); - std::string mac = get_mac_address_pretty(); - response->addHeader(HEADER_PNA_ID, mac.c_str()); + char mac_s[18]; + response->addHeader(HEADER_PNA_ID, get_mac_address_pretty_into_buffer(mac_s)); request->send(response); } #endif diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index 51a5a47323..db6b3c5180 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -334,10 +334,11 @@ void WiFiComponent::setup() { } void WiFiComponent::start() { + char mac_s[18]; ESP_LOGCONFIG(TAG, "Starting\n" " Local MAC: %s", - get_mac_address_pretty().c_str()); + get_mac_address_pretty_into_buffer(mac_s)); this->last_connected_ = millis(); uint32_t hash = this->has_sta() ? fnv1_hash(App.get_compilation_time()) : 88491487UL; @@ -800,7 +801,8 @@ void WiFiComponent::print_connect_params_() { char bssid_s[18]; format_mac_addr_upper(bssid.data(), bssid_s); - ESP_LOGCONFIG(TAG, " Local MAC: %s", get_mac_address_pretty().c_str()); + char mac_s[18]; + ESP_LOGCONFIG(TAG, " Local MAC: %s", get_mac_address_pretty_into_buffer(mac_s)); if (this->is_disabled()) { ESP_LOGCONFIG(TAG, " Disabled"); return; diff --git a/esphome/components/wifi_info/wifi_info_text_sensor.h b/esphome/components/wifi_info/wifi_info_text_sensor.h index 04889d6bb3..0814336c43 100644 --- a/esphome/components/wifi_info/wifi_info_text_sensor.h +++ b/esphome/components/wifi_info/wifi_info_text_sensor.h @@ -126,7 +126,10 @@ class BSSIDWiFiInfo : public PollingComponent, public text_sensor::TextSensor { class MacAddressWifiInfo : public Component, public text_sensor::TextSensor { public: - void setup() override { this->publish_state(get_mac_address_pretty()); } + void setup() override { + char mac_s[18]; + this->publish_state(get_mac_address_pretty_into_buffer(mac_s)); + } void dump_config() override; }; diff --git a/esphome/core/helpers.cpp b/esphome/core/helpers.cpp index 568acb9f1b..664ae253f7 100644 --- a/esphome/core/helpers.cpp +++ b/esphome/core/helpers.cpp @@ -638,9 +638,8 @@ std::string get_mac_address() { } std::string get_mac_address_pretty() { - uint8_t mac[6]; - get_mac_address_raw(mac); - return format_mac_address_pretty(mac); + char buf[18]; + return std::string(get_mac_address_pretty_into_buffer(buf)); } void get_mac_address_into_buffer(std::span buf) { diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index 16eab8b8f6..635a7d6a42 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -1052,6 +1052,16 @@ std::string get_mac_address_pretty(); /// Assumes buffer length is 13 (12 digits for hexadecimal representation followed by null terminator). void get_mac_address_into_buffer(std::span buf); +/// Get the device MAC address into the given buffer, in colon-separated uppercase hex notation. +/// Buffer must be exactly 18 bytes (17 for "XX:XX:XX:XX:XX:XX" + null terminator). +/// Returns pointer to the buffer for convenience. +inline const char *get_mac_address_pretty_into_buffer(std::span buf) { + uint8_t mac[6]; + get_mac_address_raw(mac); + format_mac_addr_upper(mac, buf.data()); + return buf.data(); +} + #ifdef USE_ESP32 /// Set the MAC address to use from the provided byte array (6 bytes). void set_mac_address(uint8_t *mac); From dc277e64f4359ef8b36f5fdcaf087f7e983d8967 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 17 Nov 2025 18:00:15 -0600 Subject: [PATCH 076/415] tweak --- esphome/core/helpers.cpp | 7 +++++++ esphome/core/helpers.h | 7 +------ 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/esphome/core/helpers.cpp b/esphome/core/helpers.cpp index 664ae253f7..50af71649c 100644 --- a/esphome/core/helpers.cpp +++ b/esphome/core/helpers.cpp @@ -648,6 +648,13 @@ void get_mac_address_into_buffer(std::span buf) { format_mac_addr_lower_no_sep(mac, buf.data()); } +const char *get_mac_address_pretty_into_buffer(std::span buf) { + uint8_t mac[6]; + get_mac_address_raw(mac); + format_mac_addr_upper(mac, buf.data()); + return buf.data(); +} + #ifndef USE_ESP32 bool has_custom_mac_address() { return false; } #endif diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index 635a7d6a42..d8c1f4647e 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -1055,12 +1055,7 @@ void get_mac_address_into_buffer(std::span buf); /// Get the device MAC address into the given buffer, in colon-separated uppercase hex notation. /// Buffer must be exactly 18 bytes (17 for "XX:XX:XX:XX:XX:XX" + null terminator). /// Returns pointer to the buffer for convenience. -inline const char *get_mac_address_pretty_into_buffer(std::span buf) { - uint8_t mac[6]; - get_mac_address_raw(mac); - format_mac_addr_upper(mac, buf.data()); - return buf.data(); -} +const char *get_mac_address_pretty_into_buffer(std::span buf); #ifdef USE_ESP32 /// Set the MAC address to use from the provided byte array (6 bytes). From b0560894b78c9650e5da58fe3df1ae2ef61f7267 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 17 Nov 2025 19:10:10 -0600 Subject: [PATCH 077/415] [wifi] Fix captive portal unusable when WiFi credentials are wrong --- esphome/components/captive_portal/__init__.py | 5 +++++ esphome/components/wifi/wifi_component.cpp | 19 +++++++++++++------ 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/esphome/components/captive_portal/__init__.py b/esphome/components/captive_portal/__init__.py index 9bd3ef8a05..f9bf93bee8 100644 --- a/esphome/components/captive_portal/__init__.py +++ b/esphome/components/captive_portal/__init__.py @@ -72,6 +72,11 @@ def _final_validate(config: ConfigType) -> ConfigType: "Add 'ap:' to your WiFi configuration to enable the captive portal." ) + # Register socket needs for DNS server (1 UDP socket) + from esphome.components import socket + + socket.consume_sockets(1, "captive_portal")(config) + return config diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index 51a5a47323..d33d80c364 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -199,7 +199,11 @@ 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 uint32_t WIFI_COOLDOWN_DURATION_MS = 500; + +/// Cooldown duration when fallback AP is active and captive portal may be running +/// Longer interval prevents scanning from disrupting AP connections and blocking captive portal +static constexpr uint32_t WIFI_COOLDOWN_WITH_AP_ACTIVE_MS = 30000; static constexpr uint8_t get_max_retries_for_phase(WiFiRetryPhase phase) { switch (phase) { @@ -417,10 +421,6 @@ void WiFiComponent::start() { void WiFiComponent::restart_adapter() { ESP_LOGW(TAG, "Restarting adapter"); this->wifi_mode_(false, {}); - // 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; } @@ -441,7 +441,10 @@ void WiFiComponent::loop() { switch (this->state_) { case WIFI_COMPONENT_STATE_COOLDOWN: { this->status_set_warning(LOG_STR("waiting to reconnect")); - if (now - this->action_started_ > WIFI_COOLDOWN_DURATION_MS) { + // Use longer cooldown when captive portal/improv is active to avoid disrupting user config + bool portal_active = this->is_captive_portal_active_() || this->is_esp32_improv_active_(); + uint32_t cooldown_duration = portal_active ? WIFI_COOLDOWN_WITH_AP_ACTIVE_MS : WIFI_COOLDOWN_DURATION_MS; + if (now - this->action_started_ > cooldown_duration) { // 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 @@ -1319,6 +1322,10 @@ bool WiFiComponent::transition_to_phase_(WiFiRetryPhase new_phase) { if (!this->is_captive_portal_active_() && !this->is_esp32_improv_active_()) { this->restart_adapter(); } + // Always enter cooldown after restart (or skip-restart) to allow stabilization + // Use extended cooldown when AP is active to avoid constant scanning that blocks DNS + this->state_ = WIFI_COMPONENT_STATE_COOLDOWN; + this->action_started_ = millis(); // Return true to indicate we should wait (go to COOLDOWN) instead of immediately connecting return true; From 0923bcd2ca1a77ff8777bd71749a403b37ce00d4 Mon Sep 17 00:00:00 2001 From: strange_v Date: Tue, 18 Nov 2025 02:32:17 +0100 Subject: [PATCH 078/415] [mipi_rgb] Fix GUITION-4848S040 colors (#11709) --- esphome/components/mipi_rgb/mipi_rgb.cpp | 15 ++++++++------- esphome/components/mipi_rgb/models/guition.py | 1 + 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/esphome/components/mipi_rgb/mipi_rgb.cpp b/esphome/components/mipi_rgb/mipi_rgb.cpp index 00c9c8cbff..080fb08c09 100644 --- a/esphome/components/mipi_rgb/mipi_rgb.cpp +++ b/esphome/components/mipi_rgb/mipi_rgb.cpp @@ -350,6 +350,7 @@ void MipiRgb::dump_config() { "\n Width: %u" "\n Height: %u" "\n Rotation: %d degrees" + "\n PCLK Inverted: %s" "\n HSync Pulse Width: %u" "\n HSync Back Porch: %u" "\n HSync Front Porch: %u" @@ -357,18 +358,18 @@ void MipiRgb::dump_config() { "\n VSync Back Porch: %u" "\n VSync Front Porch: %u" "\n Invert Colors: %s" - "\n Pixel Clock: %dMHz" + "\n Pixel Clock: %uMHz" "\n Reset Pin: %s" "\n DE Pin: %s" "\n PCLK Pin: %s" "\n HSYNC Pin: %s" "\n VSYNC Pin: %s", - this->model_, this->width_, this->height_, this->rotation_, this->hsync_pulse_width_, - this->hsync_back_porch_, this->hsync_front_porch_, this->vsync_pulse_width_, this->vsync_back_porch_, - this->vsync_front_porch_, YESNO(this->invert_colors_), this->pclk_frequency_ / 1000000, - get_pin_name(this->reset_pin_).c_str(), get_pin_name(this->de_pin_).c_str(), - get_pin_name(this->pclk_pin_).c_str(), get_pin_name(this->hsync_pin_).c_str(), - get_pin_name(this->vsync_pin_).c_str()); + this->model_, this->width_, this->height_, this->rotation_, YESNO(this->pclk_inverted_), + this->hsync_pulse_width_, this->hsync_back_porch_, this->hsync_front_porch_, this->vsync_pulse_width_, + this->vsync_back_porch_, this->vsync_front_porch_, YESNO(this->invert_colors_), + (unsigned) (this->pclk_frequency_ / 1000000), get_pin_name(this->reset_pin_).c_str(), + get_pin_name(this->de_pin_).c_str(), get_pin_name(this->pclk_pin_).c_str(), + get_pin_name(this->hsync_pin_).c_str(), get_pin_name(this->vsync_pin_).c_str()); if (this->madctl_ & MADCTL_BGR) { this->dump_pins_(8, 13, "Blue", 0); diff --git a/esphome/components/mipi_rgb/models/guition.py b/esphome/components/mipi_rgb/models/guition.py index da433e686e..915b8beda0 100644 --- a/esphome/components/mipi_rgb/models/guition.py +++ b/esphome/components/mipi_rgb/models/guition.py @@ -11,6 +11,7 @@ st7701s.extend( vsync_pin=17, pclk_pin=21, pclk_frequency="12MHz", + pclk_inverted=False, pixel_mode="18bit", mirror_x=True, mirror_y=True, From 0d6c9623ce38f8aed4692006f52135f7e80d490e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 17 Nov 2025 20:02:16 -0600 Subject: [PATCH 079/415] [dashboard_import] Store package import URL in .rodata instead of RAM (#11951) --- esphome/components/dashboard_import/dashboard_import.cpp | 6 +++--- esphome/components/dashboard_import/dashboard_import.h | 6 ++---- esphome/components/mdns/mdns_component.cpp | 3 +-- 3 files changed, 6 insertions(+), 9 deletions(-) diff --git a/esphome/components/dashboard_import/dashboard_import.cpp b/esphome/components/dashboard_import/dashboard_import.cpp index c04696fd53..d4a95b81f6 100644 --- a/esphome/components/dashboard_import/dashboard_import.cpp +++ b/esphome/components/dashboard_import/dashboard_import.cpp @@ -3,10 +3,10 @@ namespace esphome { namespace dashboard_import { -static std::string g_package_import_url; // NOLINT +static const char *g_package_import_url = ""; // NOLINT -const std::string &get_package_import_url() { return g_package_import_url; } -void set_package_import_url(std::string url) { g_package_import_url = std::move(url); } +const char *get_package_import_url() { return g_package_import_url; } +void set_package_import_url(const char *url) { g_package_import_url = url; } } // namespace dashboard_import } // namespace esphome diff --git a/esphome/components/dashboard_import/dashboard_import.h b/esphome/components/dashboard_import/dashboard_import.h index edcda6b803..488bf80a2e 100644 --- a/esphome/components/dashboard_import/dashboard_import.h +++ b/esphome/components/dashboard_import/dashboard_import.h @@ -1,12 +1,10 @@ #pragma once -#include - namespace esphome { namespace dashboard_import { -const std::string &get_package_import_url(); -void set_package_import_url(std::string url); +const char *get_package_import_url(); +void set_package_import_url(const char *url); } // namespace dashboard_import } // namespace esphome diff --git a/esphome/components/mdns/mdns_component.cpp b/esphome/components/mdns/mdns_component.cpp index 2c3150ff5d..b66129404e 100644 --- a/esphome/components/mdns/mdns_component.cpp +++ b/esphome/components/mdns/mdns_component.cpp @@ -135,8 +135,7 @@ void MDNSComponent::compile_records_(StaticVector Date: Mon, 17 Nov 2025 21:03:35 -0600 Subject: [PATCH 080/415] tweak --- esphome/components/captive_portal/__init__.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/esphome/components/captive_portal/__init__.py b/esphome/components/captive_portal/__init__.py index f9bf93bee8..c730624b49 100644 --- a/esphome/components/captive_portal/__init__.py +++ b/esphome/components/captive_portal/__init__.py @@ -72,10 +72,14 @@ def _final_validate(config: ConfigType) -> ConfigType: "Add 'ap:' to your WiFi configuration to enable the captive portal." ) - # Register socket needs for DNS server (1 UDP socket) + # Register socket needs for DNS server and additional HTTP connections + # - 1 UDP socket for DNS server + # - 2 additional TCP sockets for captive portal detection probes + configuration requests + # (OS captive portal detection makes multiple probe requests that stay in TIME_WAIT, + # need headroom for actual user configuration requests) from esphome.components import socket - socket.consume_sockets(1, "captive_portal")(config) + socket.consume_sockets(3, "captive_portal")(config) return config From 27a068e8b5b825255b293d43d068d4b8ddf17a23 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 17 Nov 2025 21:44:18 -0600 Subject: [PATCH 081/415] reduce --- esphome/components/captive_portal/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/captive_portal/__init__.py b/esphome/components/captive_portal/__init__.py index c730624b49..5f7ae32570 100644 --- a/esphome/components/captive_portal/__init__.py +++ b/esphome/components/captive_portal/__init__.py @@ -74,12 +74,12 @@ def _final_validate(config: ConfigType) -> ConfigType: # Register socket needs for DNS server and additional HTTP connections # - 1 UDP socket for DNS server - # - 2 additional TCP sockets for captive portal detection probes + configuration requests + # - 3 additional TCP sockets for captive portal detection probes + configuration requests # (OS captive portal detection makes multiple probe requests that stay in TIME_WAIT, # need headroom for actual user configuration requests) from esphome.components import socket - socket.consume_sockets(3, "captive_portal")(config) + socket.consume_sockets(4, "captive_portal")(config) return config From 7f4205b82ca566dfc670062682556c584a490306 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 17 Nov 2025 21:52:12 -0600 Subject: [PATCH 082/415] reduce --- esphome/components/captive_portal/captive_portal.cpp | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/esphome/components/captive_portal/captive_portal.cpp b/esphome/components/captive_portal/captive_portal.cpp index 30438747f2..e9ca7a8e14 100644 --- a/esphome/components/captive_portal/captive_portal.cpp +++ b/esphome/components/captive_portal/captive_portal.cpp @@ -52,7 +52,13 @@ void CaptivePortal::handle_wifisave(AsyncWebServerRequest *request) { ESP_LOGI(TAG, " Password=" LOG_SECRET("'%s'"), psk.c_str()); wifi::global_wifi_component->save_wifi_sta(ssid, psk); wifi::global_wifi_component->start_scanning(); - request->redirect(ESPHOME_F("/?save")); + + // Add Connection: close header to ensure socket is released immediately + // Without this, sockets can stay in TIME_WAIT and exhaust the socket pool + auto *response = request->beginResponse(302, ESPHOME_F("text/plain"), ESPHOME_F("")); + response->addHeader(ESPHOME_F("Location"), ESPHOME_F("/?save")); + response->addHeader(ESPHOME_F("Connection"), ESPHOME_F("close")); + request->send(response); } void CaptivePortal::setup() { From 11c8865248506e6c0b2ab55920a6cfd426ac0d85 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 17 Nov 2025 21:57:21 -0600 Subject: [PATCH 083/415] fixes --- esphome/components/captive_portal/captive_portal.cpp | 8 +------- esphome/components/web_server_idf/web_server_idf.cpp | 1 + 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/esphome/components/captive_portal/captive_portal.cpp b/esphome/components/captive_portal/captive_portal.cpp index e9ca7a8e14..30438747f2 100644 --- a/esphome/components/captive_portal/captive_portal.cpp +++ b/esphome/components/captive_portal/captive_portal.cpp @@ -52,13 +52,7 @@ void CaptivePortal::handle_wifisave(AsyncWebServerRequest *request) { ESP_LOGI(TAG, " Password=" LOG_SECRET("'%s'"), psk.c_str()); wifi::global_wifi_component->save_wifi_sta(ssid, psk); wifi::global_wifi_component->start_scanning(); - - // Add Connection: close header to ensure socket is released immediately - // Without this, sockets can stay in TIME_WAIT and exhaust the socket pool - auto *response = request->beginResponse(302, ESPHOME_F("text/plain"), ESPHOME_F("")); - response->addHeader(ESPHOME_F("Location"), ESPHOME_F("/?save")); - response->addHeader(ESPHOME_F("Connection"), ESPHOME_F("close")); - request->send(response); + request->redirect(ESPHOME_F("/?save")); } void CaptivePortal::setup() { diff --git a/esphome/components/web_server_idf/web_server_idf.cpp b/esphome/components/web_server_idf/web_server_idf.cpp index ce91569de2..b99e4aa402 100644 --- a/esphome/components/web_server_idf/web_server_idf.cpp +++ b/esphome/components/web_server_idf/web_server_idf.cpp @@ -242,6 +242,7 @@ void AsyncWebServerRequest::send(int code, const char *content_type, const char void AsyncWebServerRequest::redirect(const std::string &url) { httpd_resp_set_status(*this, "302 Found"); httpd_resp_set_hdr(*this, "Location", url.c_str()); + httpd_resp_set_hdr(*this, "Connection", "close"); httpd_resp_send(*this, nullptr, 0); } From bbfff42f765f9bc7f43ec1f66f4cc6ca2a2ad832 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 17 Nov 2025 22:11:27 -0600 Subject: [PATCH 084/415] fixes --- esphome/components/web_server_idf/web_server_idf.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/esphome/components/web_server_idf/web_server_idf.cpp b/esphome/components/web_server_idf/web_server_idf.cpp index b99e4aa402..08528f12ec 100644 --- a/esphome/components/web_server_idf/web_server_idf.cpp +++ b/esphome/components/web_server_idf/web_server_idf.cpp @@ -101,6 +101,9 @@ void AsyncWebServer::begin() { httpd_config_t config = HTTPD_DEFAULT_CONFIG(); config.server_port = this->port_; config.uri_match_fn = [](const char * /*unused*/, const char * /*unused*/, size_t /*unused*/) { return true; }; + // Enable LRU purging to close oldest idle connections when socket limit is reached + // This prevents socket exhaustion when multiple clients connect (e.g., captive portal probes) + config.lru_purge_enable = true; if (httpd_start(&this->server_, &config) == ESP_OK) { const httpd_uri_t handler_get = { .uri = "", From a4242dee64dd4ce62163452d93cb1d758e223c15 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Tue, 18 Nov 2025 14:11:49 +1000 Subject: [PATCH 085/415] [build] Don't clear pio cache unless requested (#11966) --- esphome/writer.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/esphome/writer.py b/esphome/writer.py index 8eee445cf1..b866a804b3 100644 --- a/esphome/writer.py +++ b/esphome/writer.py @@ -121,7 +121,7 @@ def update_storage_json() -> None: ) else: _LOGGER.info("Core config or version changed, cleaning build files...") - clean_build() + clean_build(clear_pio_cache=False) elif storage_should_update_cmake_cache(old, new): _LOGGER.info("Integrations changed, cleaning cmake cache...") clean_cmake_cache() @@ -301,7 +301,7 @@ def clean_cmake_cache(): pioenvs_cmake_path.unlink() -def clean_build(): +def clean_build(clear_pio_cache: bool = True): import shutil # Allow skipping cache cleaning for integration tests @@ -322,6 +322,9 @@ def clean_build(): _LOGGER.info("Deleting %s", dependencies_lock) dependencies_lock.unlink() + if not clear_pio_cache: + return + # Clean PlatformIO cache to resolve CMake compiler detection issues # This helps when toolchain paths change or get corrupted try: From a81f28a73b299f674e256cb33a730b01356f85ee Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 17 Nov 2025 22:14:56 -0600 Subject: [PATCH 086/415] fixes --- .../captive_portal/captive_portal.cpp | 6 ++++++ .../components/captive_portal/captive_portal.h | 4 ++++ .../web_server_idf/web_server_idf.cpp | 17 ++++++++++++++--- .../components/web_server_idf/web_server_idf.h | 4 ++++ 4 files changed, 28 insertions(+), 3 deletions(-) diff --git a/esphome/components/captive_portal/captive_portal.cpp b/esphome/components/captive_portal/captive_portal.cpp index 30438747f2..c2d3f63837 100644 --- a/esphome/components/captive_portal/captive_portal.cpp +++ b/esphome/components/captive_portal/captive_portal.cpp @@ -63,6 +63,12 @@ void CaptivePortal::start() { this->base_->init(); if (!this->initialized_) { this->base_->add_handler(this); +#ifdef USE_ESP_IDF + // Enable LRU socket purging to handle captive portal detection probe bursts + // OS captive portal detection makes many simultaneous HTTP requests which can + // exhaust sockets. LRU purging automatically closes oldest idle connections. + this->base_->get_server()->set_lru_purge_enable(true); +#endif } network::IPAddress ip = wifi::global_wifi_component->wifi_soft_ap_ip(); diff --git a/esphome/components/captive_portal/captive_portal.h b/esphome/components/captive_portal/captive_portal.h index f48c286f0c..e98a2b6511 100644 --- a/esphome/components/captive_portal/captive_portal.h +++ b/esphome/components/captive_portal/captive_portal.h @@ -40,6 +40,10 @@ class CaptivePortal : public AsyncWebHandler, public Component { void end() { this->active_ = false; this->disable_loop(); // Stop processing DNS requests +#ifdef USE_ESP_IDF + // Disable LRU socket purging now that captive portal is done + this->base_->get_server()->set_lru_purge_enable(false); +#endif this->base_->deinit(); if (this->dns_server_ != nullptr) { this->dns_server_->stop(); diff --git a/esphome/components/web_server_idf/web_server_idf.cpp b/esphome/components/web_server_idf/web_server_idf.cpp index 08528f12ec..f5a66f6bd9 100644 --- a/esphome/components/web_server_idf/web_server_idf.cpp +++ b/esphome/components/web_server_idf/web_server_idf.cpp @@ -94,6 +94,18 @@ void AsyncWebServer::end() { } } +void AsyncWebServer::set_lru_purge_enable(bool enable) { + if (this->lru_purge_enable_ == enable) { + return; // No change needed + } + this->lru_purge_enable_ = enable; + // If server is already running, restart it with new config + if (this->server_) { + this->end(); + this->begin(); + } +} + void AsyncWebServer::begin() { if (this->server_) { this->end(); @@ -101,9 +113,8 @@ void AsyncWebServer::begin() { httpd_config_t config = HTTPD_DEFAULT_CONFIG(); config.server_port = this->port_; config.uri_match_fn = [](const char * /*unused*/, const char * /*unused*/, size_t /*unused*/) { return true; }; - // Enable LRU purging to close oldest idle connections when socket limit is reached - // This prevents socket exhaustion when multiple clients connect (e.g., captive portal probes) - config.lru_purge_enable = true; + // Enable LRU purging if requested (e.g., by captive portal to handle probe bursts) + config.lru_purge_enable = this->lru_purge_enable_; if (httpd_start(&this->server_, &config) == ESP_OK) { const httpd_uri_t handler_get = { .uri = "", diff --git a/esphome/components/web_server_idf/web_server_idf.h b/esphome/components/web_server_idf/web_server_idf.h index 5ec6fec009..b9f690b462 100644 --- a/esphome/components/web_server_idf/web_server_idf.h +++ b/esphome/components/web_server_idf/web_server_idf.h @@ -199,9 +199,13 @@ class AsyncWebServer { return *handler; } + void set_lru_purge_enable(bool enable); + httpd_handle_t get_server() { return this->server_; } + protected: uint16_t port_{}; httpd_handle_t server_{}; + bool lru_purge_enable_{false}; static esp_err_t request_handler(httpd_req_t *r); static esp_err_t request_post_handler(httpd_req_t *r); esp_err_t request_handler_(AsyncWebServerRequest *request) const; From 11d0d4d1288c28aeb03b7bd27c36a857f9b308b4 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Tue, 18 Nov 2025 14:27:50 +1000 Subject: [PATCH 087/415] [lvgl] Apply scale to spinbox value (#11946) --- esphome/components/lvgl/widgets/spinbox.py | 5 ++++- tests/components/lvgl/lvgl-package.yaml | 4 +++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/esphome/components/lvgl/widgets/spinbox.py b/esphome/components/lvgl/widgets/spinbox.py index ac23ded723..c6f25e9587 100644 --- a/esphome/components/lvgl/widgets/spinbox.py +++ b/esphome/components/lvgl/widgets/spinbox.py @@ -1,6 +1,7 @@ from esphome import automation import esphome.config_validation as cv from esphome.const import CONF_ID, CONF_RANGE_FROM, CONF_RANGE_TO, CONF_STEP, CONF_VALUE +from esphome.cpp_generator import MockObj from ..automation import action_to_code from ..defines import ( @@ -114,7 +115,9 @@ class SpinboxType(WidgetType): w.obj, digits, digits - config[CONF_DECIMAL_PLACES] ) if (value := config.get(CONF_VALUE)) is not None: - lv.spinbox_set_value(w.obj, await lv_float.process(value)) + lv.spinbox_set_value( + w.obj, MockObj(await lv_float.process(value)) * w.get_scale() + ) def get_scale(self, config): return 10 ** config[CONF_DECIMAL_PLACES] diff --git a/tests/components/lvgl/lvgl-package.yaml b/tests/components/lvgl/lvgl-package.yaml index e42a813b40..eabceff9d9 100644 --- a/tests/components/lvgl/lvgl-package.yaml +++ b/tests/components/lvgl/lvgl-package.yaml @@ -703,7 +703,9 @@ lvgl: on_value: - lvgl.spinbox.update: id: spinbox_id - value: !lambda return x; + value: !lambda |- + static float yyy = 83.0; + return yyy + .8; - button: styles: spin_button id: spin_up From 6f96804a5dd8a1154f4bd2c74ce6b952469f53cc Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 17 Nov 2025 22:31:22 -0600 Subject: [PATCH 088/415] cleanup --- esphome/components/captive_portal/__init__.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/esphome/components/captive_portal/__init__.py b/esphome/components/captive_portal/__init__.py index 5f7ae32570..2c788c4b39 100644 --- a/esphome/components/captive_portal/__init__.py +++ b/esphome/components/captive_portal/__init__.py @@ -74,9 +74,9 @@ def _final_validate(config: ConfigType) -> ConfigType: # Register socket needs for DNS server and additional HTTP connections # - 1 UDP socket for DNS server - # - 3 additional TCP sockets for captive portal detection probes + configuration requests - # (OS captive portal detection makes multiple probe requests that stay in TIME_WAIT, - # need headroom for actual user configuration requests) + # - 3 additional TCP sockets for captive portal HTTP connections + # OS captive portal detection makes multiple simultaneous probe requests. + # LRU purging will reclaim idle sockets, but we need enough for the initial burst. from esphome.components import socket socket.consume_sockets(4, "captive_portal")(config) From f0bae783cfe2e69d9151fc1e65611338b53bc3cb Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 17 Nov 2025 22:31:53 -0600 Subject: [PATCH 089/415] cleanup --- esphome/components/captive_portal/__init__.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/esphome/components/captive_portal/__init__.py b/esphome/components/captive_portal/__init__.py index 2c788c4b39..25d0a22083 100644 --- a/esphome/components/captive_portal/__init__.py +++ b/esphome/components/captive_portal/__init__.py @@ -74,9 +74,10 @@ def _final_validate(config: ConfigType) -> ConfigType: # Register socket needs for DNS server and additional HTTP connections # - 1 UDP socket for DNS server - # - 3 additional TCP sockets for captive portal HTTP connections - # OS captive portal detection makes multiple simultaneous probe requests. - # LRU purging will reclaim idle sockets, but we need enough for the initial burst. + # - 3 additional TCP sockets for captive portal detection probes + configuration requests + # OS captive portal detection makes multiple probe requests that stay in TIME_WAIT. + # Need headroom for actual user configuration requests. + # LRU purging will reclaim idle sockets to prevent exhaustion from repeated attempts. from esphome.components import socket socket.consume_sockets(4, "captive_portal")(config) From 87ccb777c6a2052e4a311f9a93e673626275a9ee Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 17 Nov 2025 22:32:57 -0600 Subject: [PATCH 090/415] esp32 --- esphome/components/captive_portal/captive_portal.cpp | 2 +- esphome/components/captive_portal/captive_portal.h | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/captive_portal/captive_portal.cpp b/esphome/components/captive_portal/captive_portal.cpp index c2d3f63837..0ad06d49ad 100644 --- a/esphome/components/captive_portal/captive_portal.cpp +++ b/esphome/components/captive_portal/captive_portal.cpp @@ -63,7 +63,7 @@ void CaptivePortal::start() { this->base_->init(); if (!this->initialized_) { this->base_->add_handler(this); -#ifdef USE_ESP_IDF +#ifdef USE_ESP32 // Enable LRU socket purging to handle captive portal detection probe bursts // OS captive portal detection makes many simultaneous HTTP requests which can // exhaust sockets. LRU purging automatically closes oldest idle connections. diff --git a/esphome/components/captive_portal/captive_portal.h b/esphome/components/captive_portal/captive_portal.h index e98a2b6511..ae9b9dfba0 100644 --- a/esphome/components/captive_portal/captive_portal.h +++ b/esphome/components/captive_portal/captive_portal.h @@ -40,7 +40,7 @@ class CaptivePortal : public AsyncWebHandler, public Component { void end() { this->active_ = false; this->disable_loop(); // Stop processing DNS requests -#ifdef USE_ESP_IDF +#ifdef USE_ESP32 // Disable LRU socket purging now that captive portal is done this->base_->get_server()->set_lru_purge_enable(false); #endif From bfe6fc0dd0fe69004cacc7e3722e76fa87ec299f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 17 Nov 2025 23:00:22 -0600 Subject: [PATCH 091/415] skip scan when ap mode --- esphome/components/wifi/wifi_component.cpp | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index d33d80c364..a31f10f8cd 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -1232,9 +1232,16 @@ WiFiRetryPhase WiFiComponent::determine_next_phase_() { return WiFiRetryPhase::RESTARTING_ADAPTER; case WiFiRetryPhase::RESTARTING_ADAPTER: - // After restart, 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; + // After restart, go back to explicit hidden if we went through it initially + if (this->went_through_explicit_hidden_phase_()) { + return WiFiRetryPhase::EXPLICIT_HIDDEN; + } + // Skip scanning when captive portal/improv is active to avoid disrupting AP + // Even passive scans can cause brief AP disconnections on ESP32 + if (this->is_captive_portal_active_() || this->is_esp32_improv_active_()) { + return WiFiRetryPhase::RETRY_HIDDEN; + } + return WiFiRetryPhase::SCAN_CONNECTING; } // Should never reach here From 1815a7cf90c7b6570dd11812b4e18e452a5e8511 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 17 Nov 2025 23:03:38 -0600 Subject: [PATCH 092/415] skip scan when ap mode --- esphome/components/wifi/wifi_component.cpp | 10 +++++++++- esphome/components/wifi/wifi_component.h | 1 + 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index a31f10f8cd..f9043058fd 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -669,6 +669,10 @@ void WiFiComponent::save_wifi_sta(const std::string &ssid, const std::string &pa sta.set_ssid(ssid); sta.set_password(password); this->set_sta(sta); + + // Force scan on next attempt even if captive portal is still active + // This ensures new credentials are tried with proper BSSID selection after provisioning + this->force_scan_after_provision_ = true; } void WiFiComponent::start_connecting(const WiFiAP &ap) { @@ -867,6 +871,8 @@ void WiFiComponent::start_scanning() { ESP_LOGD(TAG, "Starting scan"); this->wifi_scan_start_(this->passive_scan_); this->state_ = WIFI_COMPONENT_STATE_STA_SCANNING; + // Clear the force scan flag after starting the scan + this->force_scan_after_provision_ = false; } /// Comparator for WiFi scan result sorting - determines which network should be tried first @@ -1238,7 +1244,9 @@ WiFiRetryPhase WiFiComponent::determine_next_phase_() { } // Skip scanning when captive portal/improv is active to avoid disrupting AP // Even passive scans can cause brief AP disconnections on ESP32 - if (this->is_captive_portal_active_() || this->is_esp32_improv_active_()) { + // UNLESS new credentials were just provisioned - then we need to scan + if ((this->is_captive_portal_active_() || this->is_esp32_improv_active_()) && + !this->force_scan_after_provision_) { return WiFiRetryPhase::RETRY_HIDDEN; } return WiFiRetryPhase::SCAN_CONNECTING; diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index 2fd7fa6cd4..1430794112 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -529,6 +529,7 @@ class WiFiComponent : public Component { bool enable_on_boot_; bool got_ipv4_address_{false}; bool keep_scan_results_{false}; + bool force_scan_after_provision_{false}; // Pointers at the end (naturally aligned) Trigger<> *connect_trigger_{new Trigger<>()}; From 3d83975d460878a1425b432110702109bc77f03a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 17 Nov 2025 23:11:23 -0600 Subject: [PATCH 093/415] fix thread safety issue --- esphome/components/captive_portal/captive_portal.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/esphome/components/captive_portal/captive_portal.cpp b/esphome/components/captive_portal/captive_portal.cpp index 0ad06d49ad..2c0ef86fa9 100644 --- a/esphome/components/captive_portal/captive_portal.cpp +++ b/esphome/components/captive_portal/captive_portal.cpp @@ -51,7 +51,8 @@ void CaptivePortal::handle_wifisave(AsyncWebServerRequest *request) { ESP_LOGI(TAG, " SSID='%s'", ssid.c_str()); ESP_LOGI(TAG, " Password=" LOG_SECRET("'%s'"), psk.c_str()); wifi::global_wifi_component->save_wifi_sta(ssid, psk); - wifi::global_wifi_component->start_scanning(); + // Don't call start_scanning() from HTTP thread - WiFi operations must happen on main loop + // The force_scan_after_provision_ flag will trigger scan on next retry cycle request->redirect(ESPHOME_F("/?save")); } From 8d7090fcd6e0f0bcaa473497275583088a283bb8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 17 Nov 2025 23:11:55 -0600 Subject: [PATCH 094/415] fix thread safety issue --- esphome/components/wifi/wifi_component.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index f9043058fd..981aa0b74f 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -202,8 +202,8 @@ static constexpr uint8_t WIFI_RETRY_COUNT_PER_AP = 1; static constexpr uint32_t WIFI_COOLDOWN_DURATION_MS = 500; /// Cooldown duration when fallback AP is active and captive portal may be running -/// Longer interval prevents scanning from disrupting AP connections and blocking captive portal -static constexpr uint32_t WIFI_COOLDOWN_WITH_AP_ACTIVE_MS = 30000; +/// Longer interval gives users time to configure WiFi without constant connection attempts +static constexpr uint32_t WIFI_COOLDOWN_WITH_AP_ACTIVE_MS = 5000; static constexpr uint8_t get_max_retries_for_phase(WiFiRetryPhase phase) { switch (phase) { From 2ee5cc6f2243b1a8f59dccbe3131bfacfecb3a75 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 17 Nov 2025 23:17:36 -0600 Subject: [PATCH 095/415] anotehr thread safety issue --- esphome/components/captive_portal/captive_portal.cpp | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/esphome/components/captive_portal/captive_portal.cpp b/esphome/components/captive_portal/captive_portal.cpp index 2c0ef86fa9..459ac557c8 100644 --- a/esphome/components/captive_portal/captive_portal.cpp +++ b/esphome/components/captive_portal/captive_portal.cpp @@ -50,9 +50,8 @@ void CaptivePortal::handle_wifisave(AsyncWebServerRequest *request) { ESP_LOGI(TAG, "Requested WiFi Settings Change:"); ESP_LOGI(TAG, " SSID='%s'", ssid.c_str()); ESP_LOGI(TAG, " Password=" LOG_SECRET("'%s'"), psk.c_str()); - wifi::global_wifi_component->save_wifi_sta(ssid, psk); - // Don't call start_scanning() from HTTP thread - WiFi operations must happen on main loop - // The force_scan_after_provision_ flag will trigger scan on next retry cycle + // Defer save to main loop thread to avoid NVS operations from HTTP thread + this->defer([ssid, psk]() { wifi::global_wifi_component->save_wifi_sta(ssid, psk); }); request->redirect(ESPHOME_F("/?save")); } From 3f799a01a25711eab9d12afc593d6241da9a6eea Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 17 Nov 2025 23:23:49 -0600 Subject: [PATCH 096/415] anotehr thread safety issue --- esphome/components/wifi/wifi_component.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index 981aa0b74f..fa2ca92e55 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -203,7 +203,8 @@ static constexpr uint32_t WIFI_COOLDOWN_DURATION_MS = 500; /// Cooldown duration when fallback AP is active and captive portal may be running /// Longer interval gives users time to configure WiFi without constant connection attempts -static constexpr uint32_t WIFI_COOLDOWN_WITH_AP_ACTIVE_MS = 5000; +/// While connecting, WiFi can't beacon the AP properly, so needs longer cooldown +static constexpr uint32_t WIFI_COOLDOWN_WITH_AP_ACTIVE_MS = 30000; static constexpr uint8_t get_max_retries_for_phase(WiFiRetryPhase phase) { switch (phase) { From b9af06f5a42742a65ce707ab85a107f2e67cd2d0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 17 Nov 2025 23:35:26 -0600 Subject: [PATCH 097/415] no delay --- esphome/components/captive_portal/captive_portal.cpp | 6 +++++- esphome/components/esp32_improv/esp32_improv_component.cpp | 2 ++ .../components/improv_serial/improv_serial_component.cpp | 2 ++ esphome/components/wifi/wifi_component.h | 1 + 4 files changed, 10 insertions(+), 1 deletion(-) diff --git a/esphome/components/captive_portal/captive_portal.cpp b/esphome/components/captive_portal/captive_portal.cpp index 459ac557c8..a377abe380 100644 --- a/esphome/components/captive_portal/captive_portal.cpp +++ b/esphome/components/captive_portal/captive_portal.cpp @@ -51,7 +51,11 @@ void CaptivePortal::handle_wifisave(AsyncWebServerRequest *request) { ESP_LOGI(TAG, " SSID='%s'", ssid.c_str()); ESP_LOGI(TAG, " Password=" LOG_SECRET("'%s'"), psk.c_str()); // Defer save to main loop thread to avoid NVS operations from HTTP thread - this->defer([ssid, psk]() { wifi::global_wifi_component->save_wifi_sta(ssid, psk); }); + this->defer([ssid, psk]() { + wifi::global_wifi_component->save_wifi_sta(ssid, psk); + // Trigger immediate retry to attempt connection with new credentials + wifi::global_wifi_component->retry_connect(); + }); request->redirect(ESPHOME_F("/?save")); } diff --git a/esphome/components/esp32_improv/esp32_improv_component.cpp b/esphome/components/esp32_improv/esp32_improv_component.cpp index 398b1d4251..f4c19b04d4 100644 --- a/esphome/components/esp32_improv/esp32_improv_component.cpp +++ b/esphome/components/esp32_improv/esp32_improv_component.cpp @@ -381,6 +381,8 @@ void ESP32ImprovComponent::check_wifi_connection_() { if (this->state_ == improv::STATE_PROVISIONING) { wifi::global_wifi_component->save_wifi_sta(this->connecting_sta_.get_ssid(), this->connecting_sta_.get_password()); + // Trigger immediate retry to attempt connection with new credentials + wifi::global_wifi_component->retry_connect(); this->connecting_sta_ = {}; this->cancel_timeout("wifi-connect-timeout"); diff --git a/esphome/components/improv_serial/improv_serial_component.cpp b/esphome/components/improv_serial/improv_serial_component.cpp index 70260eeab3..52e223670d 100644 --- a/esphome/components/improv_serial/improv_serial_component.cpp +++ b/esphome/components/improv_serial/improv_serial_component.cpp @@ -50,6 +50,8 @@ void ImprovSerialComponent::loop() { if (wifi::global_wifi_component->is_connected()) { wifi::global_wifi_component->save_wifi_sta(this->connecting_sta_.get_ssid(), this->connecting_sta_.get_password()); + // Trigger immediate retry to attempt connection with new credentials + wifi::global_wifi_component->retry_connect(); this->connecting_sta_ = {}; this->cancel_timeout("wifi-connect-timeout"); this->set_state_(improv::STATE_PROVISIONED); diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index 1430794112..270404dff5 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -291,6 +291,7 @@ class WiFiComponent : public Component { void set_passive_scan(bool passive); void save_wifi_sta(const std::string &ssid, const std::string &password); + // ========== INTERNAL METHODS ========== // (In most use cases you won't need these) /// Setup WiFi interface. From 29ef0a67405395522af039f84c3d87ed045b587c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 17 Nov 2025 23:43:33 -0600 Subject: [PATCH 098/415] no delay --- esphome/components/captive_portal/captive_portal.cpp | 4 ++-- .../components/esp32_improv/esp32_improv_component.cpp | 4 ++-- .../components/improv_serial/improv_serial_component.cpp | 4 ++-- esphome/components/wifi/wifi_component.cpp | 8 ++++++++ esphome/components/wifi/wifi_component.h | 3 +++ 5 files changed, 17 insertions(+), 6 deletions(-) diff --git a/esphome/components/captive_portal/captive_portal.cpp b/esphome/components/captive_portal/captive_portal.cpp index a377abe380..b70fa4d0ca 100644 --- a/esphome/components/captive_portal/captive_portal.cpp +++ b/esphome/components/captive_portal/captive_portal.cpp @@ -53,8 +53,8 @@ void CaptivePortal::handle_wifisave(AsyncWebServerRequest *request) { // Defer save to main loop thread to avoid NVS operations from HTTP thread this->defer([ssid, psk]() { wifi::global_wifi_component->save_wifi_sta(ssid, psk); - // Trigger immediate retry to attempt connection with new credentials - wifi::global_wifi_component->retry_connect(); + // Trigger connection attempt (exits cooldown if needed) + wifi::global_wifi_component->connect_soon(); }); request->redirect(ESPHOME_F("/?save")); } diff --git a/esphome/components/esp32_improv/esp32_improv_component.cpp b/esphome/components/esp32_improv/esp32_improv_component.cpp index f4c19b04d4..c456ad7d33 100644 --- a/esphome/components/esp32_improv/esp32_improv_component.cpp +++ b/esphome/components/esp32_improv/esp32_improv_component.cpp @@ -381,8 +381,8 @@ void ESP32ImprovComponent::check_wifi_connection_() { if (this->state_ == improv::STATE_PROVISIONING) { wifi::global_wifi_component->save_wifi_sta(this->connecting_sta_.get_ssid(), this->connecting_sta_.get_password()); - // Trigger immediate retry to attempt connection with new credentials - wifi::global_wifi_component->retry_connect(); + // Trigger connection attempt (exits cooldown if needed, no-op if already connected) + wifi::global_wifi_component->connect_soon(); this->connecting_sta_ = {}; this->cancel_timeout("wifi-connect-timeout"); diff --git a/esphome/components/improv_serial/improv_serial_component.cpp b/esphome/components/improv_serial/improv_serial_component.cpp index 52e223670d..51f8c8b839 100644 --- a/esphome/components/improv_serial/improv_serial_component.cpp +++ b/esphome/components/improv_serial/improv_serial_component.cpp @@ -50,8 +50,8 @@ void ImprovSerialComponent::loop() { if (wifi::global_wifi_component->is_connected()) { wifi::global_wifi_component->save_wifi_sta(this->connecting_sta_.get_ssid(), this->connecting_sta_.get_password()); - // Trigger immediate retry to attempt connection with new credentials - wifi::global_wifi_component->retry_connect(); + // Trigger connection attempt (exits cooldown if needed, no-op if already connected) + wifi::global_wifi_component->connect_soon(); this->connecting_sta_ = {}; this->cancel_timeout("wifi-connect-timeout"); this->set_state_(improv::STATE_PROVISIONED); diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index fa2ca92e55..401c60267e 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -676,6 +676,14 @@ void WiFiComponent::save_wifi_sta(const std::string &ssid, const std::string &pa this->force_scan_after_provision_ = true; } +void WiFiComponent::connect_soon() { + // Only trigger retry if we're in cooldown - if already connecting/connected, do nothing + if (this->state_ == WIFI_COMPONENT_STATE_COOLDOWN) { + ESP_LOGD(TAG, "Exiting cooldown early due to new WiFi credentials"); + this->retry_connect(); + } +} + void WiFiComponent::start_connecting(const WiFiAP &ap) { // Log connection attempt at INFO level with priority char bssid_s[18]; diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index 270404dff5..c014fc6343 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -292,6 +292,9 @@ class WiFiComponent : public Component { void save_wifi_sta(const std::string &ssid, const std::string &password); + /// Trigger connection attempt soon (exits cooldown if needed, otherwise no-op if already connecting/connected) + void connect_soon(); + // ========== INTERNAL METHODS ========== // (In most use cases you won't need these) /// Setup WiFi interface. From efbf696f8874bd09ea782d6fcd5832fe0fb3e42c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 17 Nov 2025 23:45:25 -0600 Subject: [PATCH 099/415] no delay --- esphome/components/captive_portal/captive_portal.cpp | 6 +----- esphome/components/esp32_improv/esp32_improv_component.cpp | 2 -- .../components/improv_serial/improv_serial_component.cpp | 2 -- esphome/components/wifi/wifi_component.cpp | 3 +++ 4 files changed, 4 insertions(+), 9 deletions(-) diff --git a/esphome/components/captive_portal/captive_portal.cpp b/esphome/components/captive_portal/captive_portal.cpp index b70fa4d0ca..459ac557c8 100644 --- a/esphome/components/captive_portal/captive_portal.cpp +++ b/esphome/components/captive_portal/captive_portal.cpp @@ -51,11 +51,7 @@ void CaptivePortal::handle_wifisave(AsyncWebServerRequest *request) { ESP_LOGI(TAG, " SSID='%s'", ssid.c_str()); ESP_LOGI(TAG, " Password=" LOG_SECRET("'%s'"), psk.c_str()); // Defer save to main loop thread to avoid NVS operations from HTTP thread - this->defer([ssid, psk]() { - wifi::global_wifi_component->save_wifi_sta(ssid, psk); - // Trigger connection attempt (exits cooldown if needed) - wifi::global_wifi_component->connect_soon(); - }); + this->defer([ssid, psk]() { wifi::global_wifi_component->save_wifi_sta(ssid, psk); }); request->redirect(ESPHOME_F("/?save")); } diff --git a/esphome/components/esp32_improv/esp32_improv_component.cpp b/esphome/components/esp32_improv/esp32_improv_component.cpp index c456ad7d33..398b1d4251 100644 --- a/esphome/components/esp32_improv/esp32_improv_component.cpp +++ b/esphome/components/esp32_improv/esp32_improv_component.cpp @@ -381,8 +381,6 @@ void ESP32ImprovComponent::check_wifi_connection_() { if (this->state_ == improv::STATE_PROVISIONING) { wifi::global_wifi_component->save_wifi_sta(this->connecting_sta_.get_ssid(), this->connecting_sta_.get_password()); - // Trigger connection attempt (exits cooldown if needed, no-op if already connected) - wifi::global_wifi_component->connect_soon(); this->connecting_sta_ = {}; this->cancel_timeout("wifi-connect-timeout"); diff --git a/esphome/components/improv_serial/improv_serial_component.cpp b/esphome/components/improv_serial/improv_serial_component.cpp index 51f8c8b839..70260eeab3 100644 --- a/esphome/components/improv_serial/improv_serial_component.cpp +++ b/esphome/components/improv_serial/improv_serial_component.cpp @@ -50,8 +50,6 @@ void ImprovSerialComponent::loop() { if (wifi::global_wifi_component->is_connected()) { wifi::global_wifi_component->save_wifi_sta(this->connecting_sta_.get_ssid(), this->connecting_sta_.get_password()); - // Trigger connection attempt (exits cooldown if needed, no-op if already connected) - wifi::global_wifi_component->connect_soon(); this->connecting_sta_ = {}; this->cancel_timeout("wifi-connect-timeout"); this->set_state_(improv::STATE_PROVISIONED); diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index 401c60267e..5adc0c0489 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -674,6 +674,9 @@ void WiFiComponent::save_wifi_sta(const std::string &ssid, const std::string &pa // Force scan on next attempt even if captive portal is still active // This ensures new credentials are tried with proper BSSID selection after provisioning this->force_scan_after_provision_ = true; + + // Trigger connection attempt (exits cooldown if needed, no-op if already connecting/connected) + this->connect_soon(); } void WiFiComponent::connect_soon() { From 3f763b24c55ec740f71bd569349f5ba00f74d499 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 17 Nov 2025 23:45:32 -0600 Subject: [PATCH 100/415] no delay --- esphome/components/wifi/wifi_component.h | 3 --- 1 file changed, 3 deletions(-) diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index c014fc6343..270404dff5 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -292,9 +292,6 @@ class WiFiComponent : public Component { void save_wifi_sta(const std::string &ssid, const std::string &password); - /// Trigger connection attempt soon (exits cooldown if needed, otherwise no-op if already connecting/connected) - void connect_soon(); - // ========== INTERNAL METHODS ========== // (In most use cases you won't need these) /// Setup WiFi interface. From 048533a1fda385c5d52a52db9af9931e9c78977c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 17 Nov 2025 23:45:59 -0600 Subject: [PATCH 101/415] no delay --- esphome/components/wifi/wifi_component.cpp | 4 ++-- esphome/components/wifi/wifi_component.h | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index 5adc0c0489..c7c87c2343 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -676,10 +676,10 @@ void WiFiComponent::save_wifi_sta(const std::string &ssid, const std::string &pa this->force_scan_after_provision_ = true; // Trigger connection attempt (exits cooldown if needed, no-op if already connecting/connected) - this->connect_soon(); + this->connect_soon_(); } -void WiFiComponent::connect_soon() { +void WiFiComponent::connect_soon_() { // Only trigger retry if we're in cooldown - if already connecting/connected, do nothing if (this->state_ == WIFI_COMPONENT_STATE_COOLDOWN) { ESP_LOGD(TAG, "Exiting cooldown early due to new WiFi credentials"); diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index 270404dff5..f5d21af99d 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -425,6 +425,8 @@ class WiFiComponent : public Component { return true; } + void connect_soon_(); + void wifi_loop_(); bool wifi_mode_(optional sta, optional ap); bool wifi_sta_pre_setup_(); From 66fcf364a6b661bf1e3eddc9595fcf0d4689f179 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 18 Nov 2025 00:09:51 -0600 Subject: [PATCH 102/415] tweak --- esphome/components/wifi/__init__.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/esphome/components/wifi/__init__.py b/esphome/components/wifi/__init__.py index 11bd7798e2..5336a43ca2 100644 --- a/esphome/components/wifi/__init__.py +++ b/esphome/components/wifi/__init__.py @@ -69,6 +69,12 @@ CONF_MIN_AUTH_MODE = "min_auth_mode" # Limited to 127 because selected_sta_index_ is int8_t in C++ MAX_WIFI_NETWORKS = 127 +# Default AP timeout - allows sufficient time to try all BSSIDs during initial connection +# After AP starts, WiFi scanning is skipped to avoid disrupting the AP, so we only +# get best-effort connection attempts. Longer timeout ensures we exhaust all options +# before falling back to AP mode. +DEFAULT_AP_TIMEOUT = "2min" + wifi_ns = cg.esphome_ns.namespace("wifi") EAPAuth = wifi_ns.struct("EAPAuth") ManualIP = wifi_ns.struct("ManualIP") @@ -177,7 +183,7 @@ CONF_AP_TIMEOUT = "ap_timeout" WIFI_NETWORK_AP = WIFI_NETWORK_BASE.extend( { cv.Optional( - CONF_AP_TIMEOUT, default="1min" + CONF_AP_TIMEOUT, default=DEFAULT_AP_TIMEOUT ): cv.positive_time_period_milliseconds, } ) From 79b9e34f654af66609c55ef17b307231a24dc577 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 18 Nov 2025 09:13:29 -0600 Subject: [PATCH 103/415] do not skip ssids in retry_hidden if we did not scan --- esphome/components/wifi/wifi_component.cpp | 7 ++++++- esphome/components/wifi/wifi_component.h | 1 + 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index c7c87c2343..430f3e9993 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -280,7 +280,9 @@ int8_t WiFiComponent::find_next_hidden_sta_(int8_t start_index) { } } - if (!this->ssid_was_seen_in_scan_(sta.get_ssid())) { + // If we didn't scan this cycle, treat all networks as potentially hidden + // Otherwise, only retry networks that weren't seen in the scan + if (!this->did_scan_this_cycle_ || !this->ssid_was_seen_in_scan_(sta.get_ssid())) { ESP_LOGD(TAG, "Hidden candidate " LOG_SECRET("'%s'") " at index %d", sta.get_ssid().c_str(), static_cast(i)); return static_cast(i); } @@ -984,6 +986,7 @@ void WiFiComponent::check_scanning_finished() { return; } this->scan_done_ = false; + this->did_scan_this_cycle_ = true; if (this->scan_result_.empty()) { ESP_LOGW(TAG, "No networks found"); @@ -1349,6 +1352,8 @@ bool WiFiComponent::transition_to_phase_(WiFiRetryPhase new_phase) { if (!this->is_captive_portal_active_() && !this->is_esp32_improv_active_()) { this->restart_adapter(); } + // Clear scan flag - we're starting a new retry cycle + this->did_scan_this_cycle_ = false; // Always enter cooldown after restart (or skip-restart) to allow stabilization // Use extended cooldown when AP is active to avoid constant scanning that blocks DNS this->state_ = WIFI_COMPONENT_STATE_COOLDOWN; diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index f5d21af99d..c12f32c052 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -533,6 +533,7 @@ class WiFiComponent : public Component { bool got_ipv4_address_{false}; bool keep_scan_results_{false}; bool force_scan_after_provision_{false}; + bool did_scan_this_cycle_{false}; // Pointers at the end (naturally aligned) Trigger<> *connect_trigger_{new Trigger<>()}; From 407f07a643e594736dbe165f5678dbc3c72a1f22 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 18 Nov 2025 09:25:24 -0600 Subject: [PATCH 104/415] realign timeouts --- esphome/components/esp32_improv/__init__.py | 6 +++++- esphome/components/wifi/__init__.py | 4 ++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/esphome/components/esp32_improv/__init__.py b/esphome/components/esp32_improv/__init__.py index 1a7194da81..2e69d400ca 100644 --- a/esphome/components/esp32_improv/__init__.py +++ b/esphome/components/esp32_improv/__init__.py @@ -20,6 +20,10 @@ CONF_ON_STOP = "on_stop" CONF_STATUS_INDICATOR = "status_indicator" CONF_WIFI_TIMEOUT = "wifi_timeout" +# Default WiFi timeout - aligned with WiFi component ap_timeout +# Allows sufficient time to try all BSSIDs before starting provisioning mode +DEFAULT_WIFI_TIMEOUT = "90s" + improv_ns = cg.esphome_ns.namespace("improv") Error = improv_ns.enum("Error") @@ -59,7 +63,7 @@ CONFIG_SCHEMA = ( CONF_AUTHORIZED_DURATION, default="1min" ): cv.positive_time_period_milliseconds, cv.Optional( - CONF_WIFI_TIMEOUT, default="1min" + CONF_WIFI_TIMEOUT, default=DEFAULT_WIFI_TIMEOUT ): cv.positive_time_period_milliseconds, cv.Optional(CONF_ON_PROVISIONED): automation.validate_automation( { diff --git a/esphome/components/wifi/__init__.py b/esphome/components/wifi/__init__.py index 5336a43ca2..5b3b30e0e9 100644 --- a/esphome/components/wifi/__init__.py +++ b/esphome/components/wifi/__init__.py @@ -72,8 +72,8 @@ MAX_WIFI_NETWORKS = 127 # Default AP timeout - allows sufficient time to try all BSSIDs during initial connection # After AP starts, WiFi scanning is skipped to avoid disrupting the AP, so we only # get best-effort connection attempts. Longer timeout ensures we exhaust all options -# before falling back to AP mode. -DEFAULT_AP_TIMEOUT = "2min" +# before falling back to AP mode. Aligned with improv wifi_timeout default. +DEFAULT_AP_TIMEOUT = "90s" wifi_ns = cg.esphome_ns.namespace("wifi") EAPAuth = wifi_ns.struct("EAPAuth") From 17b72061ad479fafd8a2fabcff394adf96381b31 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 18 Nov 2025 09:26:53 -0600 Subject: [PATCH 105/415] realign timeouts --- esphome/components/wifi/wifi_component.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index 430f3e9993..a1f48848b5 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -503,6 +503,7 @@ void WiFiComponent::loop() { #ifdef USE_IMPROV if (esp32_improv::global_improv_component != nullptr && !esp32_improv::global_improv_component->is_active()) { if (now - this->last_connected_ > esp32_improv::global_improv_component->get_wifi_timeout()) { + ESP_LOGI(TAG, "Starting Improv"); if (this->wifi_mode_(true, {})) esp32_improv::global_improv_component->start(); } From eaaaeecc92cc299955d78259759652f96598a86e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 18 Nov 2025 09:32:50 -0600 Subject: [PATCH 106/415] there is a tight loop in improv --- esphome/components/wifi/wifi_component.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index a1f48848b5..430f3e9993 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -503,7 +503,6 @@ void WiFiComponent::loop() { #ifdef USE_IMPROV if (esp32_improv::global_improv_component != nullptr && !esp32_improv::global_improv_component->is_active()) { if (now - this->last_connected_ > esp32_improv::global_improv_component->get_wifi_timeout()) { - ESP_LOGI(TAG, "Starting Improv"); if (this->wifi_mode_(true, {})) esp32_improv::global_improv_component->start(); } From 19bd28227413dfa21e7a302d2a0ddd470c73ef32 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 18 Nov 2025 09:35:14 -0600 Subject: [PATCH 107/415] fix tight loop --- esphome/components/esp32_improv/esp32_improv_component.h | 1 + esphome/components/wifi/wifi_component.cpp | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/esphome/components/esp32_improv/esp32_improv_component.h b/esphome/components/esp32_improv/esp32_improv_component.h index 989552ea56..8f4cfd7958 100644 --- a/esphome/components/esp32_improv/esp32_improv_component.h +++ b/esphome/components/esp32_improv/esp32_improv_component.h @@ -45,6 +45,7 @@ class ESP32ImprovComponent : public Component, public improv_base::ImprovBase { void start(); void stop(); bool is_active() const { return this->state_ != improv::STATE_STOPPED; } + bool should_start() const { return this->should_start_; } #ifdef USE_ESP32_IMPROV_STATE_CALLBACK void add_on_state_callback(std::function &&callback) { diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index 430f3e9993..54a0510e07 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -501,7 +501,8 @@ void WiFiComponent::loop() { #endif // USE_WIFI_AP #ifdef USE_IMPROV - if (esp32_improv::global_improv_component != nullptr && !esp32_improv::global_improv_component->is_active()) { + if (esp32_improv::global_improv_component != nullptr && !esp32_improv::global_improv_component->is_active() && + !esp32_improv::global_improv_component->should_start()) { if (now - this->last_connected_ > esp32_improv::global_improv_component->get_wifi_timeout()) { if (this->wifi_mode_(true, {})) esp32_improv::global_improv_component->start(); From 516f94671db639693fc28ee3ae87cb643cb8e898 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 18 Nov 2025 09:37:27 -0600 Subject: [PATCH 108/415] fixes --- esphome/components/esp32_improv/esp32_improv_component.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/esphome/components/esp32_improv/esp32_improv_component.cpp b/esphome/components/esp32_improv/esp32_improv_component.cpp index 398b1d4251..0ad54bbb15 100644 --- a/esphome/components/esp32_improv/esp32_improv_component.cpp +++ b/esphome/components/esp32_improv/esp32_improv_component.cpp @@ -127,6 +127,7 @@ void ESP32ImprovComponent::loop() { // Set initial state based on whether we have an authorizer this->set_state_(this->get_initial_state_(), false); this->set_error_(improv::ERROR_NONE); + this->should_start_ = false; // Clear flag after starting ESP_LOGD(TAG, "Service started!"); } } From 303792bf8d8b96b708743f2d95f2a86128a4c677 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 18 Nov 2025 09:58:34 -0600 Subject: [PATCH 109/415] make sure improv works if we are in connect loop --- esphome/components/wifi/wifi_component.cpp | 17 +++++++++++++++++ esphome/components/wifi/wifi_component.h | 1 + 2 files changed, 18 insertions(+) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index 54a0510e07..dcb81a80a0 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -444,6 +444,12 @@ void WiFiComponent::loop() { switch (this->state_) { case WIFI_COMPONENT_STATE_COOLDOWN: { this->status_set_warning(LOG_STR("waiting to reconnect")); + // Skip cooldown if new credentials were provided while connecting + if (this->skip_cooldown_next_cycle_) { + this->skip_cooldown_next_cycle_ = false; + this->check_connecting_finished(); + break; + } // Use longer cooldown when captive portal/improv is active to avoid disrupting user config bool portal_active = this->is_captive_portal_active_() || this->is_esp32_improv_active_(); uint32_t cooldown_duration = portal_active ? WIFI_COOLDOWN_WITH_AP_ACTIVE_MS : WIFI_COOLDOWN_DURATION_MS; @@ -612,6 +618,9 @@ void WiFiComponent::set_sta(const WiFiAP &ap) { this->init_sta(1); this->add_sta(ap); this->selected_sta_index_ = 0; + // Force scan on next attempt even if captive portal is still active + // This ensures new credentials are tried with proper BSSID selection after provisioning + this->force_scan_after_provision_ = true; } WiFiAP WiFiComponent::build_params_for_current_phase_() { @@ -691,6 +700,14 @@ void WiFiComponent::connect_soon_() { } void WiFiComponent::start_connecting(const WiFiAP &ap) { + // If already connecting/connected, set flag to skip cooldown on next cycle + // Caller (e.g., improv) already called set_sta() with new credentials, state machine will retry + if (this->state_ == WIFI_COMPONENT_STATE_STA_CONNECTING || this->state_ == WIFI_COMPONENT_STATE_STA_CONNECTED) { + ESP_LOGD(TAG, "Already connecting, will retry on next cycle"); + this->skip_cooldown_next_cycle_ = true; + return; + } + // Log connection attempt at INFO level with priority char bssid_s[18]; int8_t priority = 0; diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index c12f32c052..5e58c41bc0 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -534,6 +534,7 @@ class WiFiComponent : public Component { bool keep_scan_results_{false}; bool force_scan_after_provision_{false}; bool did_scan_this_cycle_{false}; + bool skip_cooldown_next_cycle_{false}; // Pointers at the end (naturally aligned) Trigger<> *connect_trigger_{new Trigger<>()}; From 1f6aca5c17ff1c3c3280adf362a2c912c2b9bd07 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 18 Nov 2025 10:08:44 -0600 Subject: [PATCH 110/415] fixes --- esphome/components/wifi/wifi_component.cpp | 7 ------- esphome/components/wifi/wifi_component.h | 1 - 2 files changed, 8 deletions(-) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index dcb81a80a0..f8a3728288 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -618,9 +618,6 @@ void WiFiComponent::set_sta(const WiFiAP &ap) { this->init_sta(1); this->add_sta(ap); this->selected_sta_index_ = 0; - // Force scan on next attempt even if captive portal is still active - // This ensures new credentials are tried with proper BSSID selection after provisioning - this->force_scan_after_provision_ = true; } WiFiAP WiFiComponent::build_params_for_current_phase_() { @@ -683,10 +680,6 @@ void WiFiComponent::save_wifi_sta(const std::string &ssid, const std::string &pa sta.set_password(password); this->set_sta(sta); - // Force scan on next attempt even if captive portal is still active - // This ensures new credentials are tried with proper BSSID selection after provisioning - this->force_scan_after_provision_ = true; - // Trigger connection attempt (exits cooldown if needed, no-op if already connecting/connected) this->connect_soon_(); } diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index 5e58c41bc0..ff438b2927 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -532,7 +532,6 @@ class WiFiComponent : public Component { bool enable_on_boot_; bool got_ipv4_address_{false}; bool keep_scan_results_{false}; - bool force_scan_after_provision_{false}; bool did_scan_this_cycle_{false}; bool skip_cooldown_next_cycle_{false}; From 53a3a5ddeab5963e8613b5ed140533c7b51841bc Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 18 Nov 2025 10:08:53 -0600 Subject: [PATCH 111/415] fixes --- esphome/components/wifi/wifi_component.cpp | 2 -- 1 file changed, 2 deletions(-) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index f8a3728288..45ad331c37 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -896,8 +896,6 @@ void WiFiComponent::start_scanning() { ESP_LOGD(TAG, "Starting scan"); this->wifi_scan_start_(this->passive_scan_); this->state_ = WIFI_COMPONENT_STATE_STA_SCANNING; - // Clear the force scan flag after starting the scan - this->force_scan_after_provision_ = false; } /// Comparator for WiFi scan result sorting - determines which network should be tried first From 62c9d83777fd8ea9d3a2bd64fb9a92503414fdec Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 18 Nov 2025 10:09:14 -0600 Subject: [PATCH 112/415] fixes --- esphome/components/wifi/wifi_component.cpp | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index 45ad331c37..075b70a266 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -1268,9 +1268,7 @@ WiFiRetryPhase WiFiComponent::determine_next_phase_() { } // Skip scanning when captive portal/improv is active to avoid disrupting AP // Even passive scans can cause brief AP disconnections on ESP32 - // UNLESS new credentials were just provisioned - then we need to scan - if ((this->is_captive_portal_active_() || this->is_esp32_improv_active_()) && - !this->force_scan_after_provision_) { + if (this->is_captive_portal_active_() || this->is_esp32_improv_active_()) { return WiFiRetryPhase::RETRY_HIDDEN; } return WiFiRetryPhase::SCAN_CONNECTING; From 92e19c497eb87a5b2898d0af7f18a2880b0d1404 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 18 Nov 2025 10:13:03 -0600 Subject: [PATCH 113/415] fixes --- esphome/components/wifi/wifi_component.cpp | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index 075b70a266..30340601fb 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -618,6 +618,8 @@ void WiFiComponent::set_sta(const WiFiAP &ap) { this->init_sta(1); this->add_sta(ap); this->selected_sta_index_ = 0; + // When new credentials are set (e.g., from improv), skip cooldown to retry immediately + this->skip_cooldown_next_cycle_ = true; } WiFiAP WiFiComponent::build_params_for_current_phase_() { @@ -693,14 +695,6 @@ void WiFiComponent::connect_soon_() { } void WiFiComponent::start_connecting(const WiFiAP &ap) { - // If already connecting/connected, set flag to skip cooldown on next cycle - // Caller (e.g., improv) already called set_sta() with new credentials, state machine will retry - if (this->state_ == WIFI_COMPONENT_STATE_STA_CONNECTING || this->state_ == WIFI_COMPONENT_STATE_STA_CONNECTED) { - ESP_LOGD(TAG, "Already connecting, will retry on next cycle"); - this->skip_cooldown_next_cycle_ = true; - return; - } - // Log connection attempt at INFO level with priority char bssid_s[18]; int8_t priority = 0; From 33983b051bf9a979e929c983ed4661d57ebbed1f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 18 Nov 2025 10:51:47 -0600 Subject: [PATCH 114/415] [ld24xx] Use stack allocation for MAC and version formatting (#11961) --- esphome/components/ld2410/ld2410.cpp | 26 ++++++++++++-------------- esphome/components/ld2412/ld2412.cpp | 26 ++++++++++++-------------- esphome/components/ld2450/ld2450.cpp | 26 ++++++++++++-------------- esphome/components/ld24xx/ld24xx.h | 24 +++++++++++++++++++++++- 4 files changed, 59 insertions(+), 43 deletions(-) diff --git a/esphome/components/ld2410/ld2410.cpp b/esphome/components/ld2410/ld2410.cpp index 608882565f..391f2024cd 100644 --- a/esphome/components/ld2410/ld2410.cpp +++ b/esphome/components/ld2410/ld2410.cpp @@ -13,8 +13,6 @@ namespace esphome { namespace ld2410 { static const char *const TAG = "ld2410"; -static const char *const UNKNOWN_MAC = "unknown"; -static const char *const VERSION_FMT = "%u.%02X.%02X%02X%02X%02X"; enum BaudRate : uint8_t { BAUD_RATE_9600 = 1, @@ -181,15 +179,15 @@ static inline bool validate_header_footer(const uint8_t *header_footer, const ui } void LD2410Component::dump_config() { - std::string mac_str = - mac_address_is_valid(this->mac_address_) ? format_mac_address_pretty(this->mac_address_) : UNKNOWN_MAC; - std::string version = str_sprintf(VERSION_FMT, this->version_[1], this->version_[0], this->version_[5], - this->version_[4], this->version_[3], this->version_[2]); + char mac_s[18]; + char version_s[20]; + const char *mac_str = ld24xx::format_mac_str(this->mac_address_, mac_s); + ld24xx::format_version_str(this->version_, version_s); ESP_LOGCONFIG(TAG, "LD2410:\n" " Firmware version: %s\n" " MAC address: %s", - version.c_str(), mac_str.c_str()); + version_s, mac_str); #ifdef USE_BINARY_SENSOR ESP_LOGCONFIG(TAG, "Binary Sensors:"); LOG_BINARY_SENSOR(" ", "Target", this->target_binary_sensor_); @@ -448,12 +446,12 @@ bool LD2410Component::handle_ack_data_() { case CMD_QUERY_VERSION: { std::memcpy(this->version_, &this->buffer_data_[12], sizeof(this->version_)); - std::string version = str_sprintf(VERSION_FMT, this->version_[1], this->version_[0], this->version_[5], - this->version_[4], this->version_[3], this->version_[2]); - ESP_LOGV(TAG, "Firmware version: %s", version.c_str()); + char version_s[20]; + ld24xx::format_version_str(this->version_, version_s); + ESP_LOGV(TAG, "Firmware version: %s", version_s); #ifdef USE_TEXT_SENSOR if (this->version_text_sensor_ != nullptr) { - this->version_text_sensor_->publish_state(version); + this->version_text_sensor_->publish_state(version_s); } #endif break; @@ -506,9 +504,9 @@ bool LD2410Component::handle_ack_data_() { std::memcpy(this->mac_address_, &this->buffer_data_[10], sizeof(this->mac_address_)); } - std::string mac_str = - mac_address_is_valid(this->mac_address_) ? format_mac_address_pretty(this->mac_address_) : UNKNOWN_MAC; - ESP_LOGV(TAG, "MAC address: %s", mac_str.c_str()); + char mac_s[18]; + const char *mac_str = ld24xx::format_mac_str(this->mac_address_, mac_s); + ESP_LOGV(TAG, "MAC address: %s", mac_str); #ifdef USE_TEXT_SENSOR if (this->mac_text_sensor_ != nullptr) { this->mac_text_sensor_->publish_state(mac_str); diff --git a/esphome/components/ld2412/ld2412.cpp b/esphome/components/ld2412/ld2412.cpp index 5323a9a658..4f2fd7c2bd 100644 --- a/esphome/components/ld2412/ld2412.cpp +++ b/esphome/components/ld2412/ld2412.cpp @@ -14,8 +14,6 @@ namespace esphome { namespace ld2412 { static const char *const TAG = "ld2412"; -static const char *const UNKNOWN_MAC = "unknown"; -static const char *const VERSION_FMT = "%u.%02X.%02X%02X%02X%02X"; enum BaudRate : uint8_t { BAUD_RATE_9600 = 1, @@ -200,15 +198,15 @@ static inline bool validate_header_footer(const uint8_t *header_footer, const ui } void LD2412Component::dump_config() { - std::string mac_str = - mac_address_is_valid(this->mac_address_) ? format_mac_address_pretty(this->mac_address_) : UNKNOWN_MAC; - std::string version = str_sprintf(VERSION_FMT, this->version_[1], this->version_[0], this->version_[5], - this->version_[4], this->version_[3], this->version_[2]); + char mac_s[18]; + char version_s[20]; + const char *mac_str = ld24xx::format_mac_str(this->mac_address_, mac_s); + ld24xx::format_version_str(this->version_, version_s); ESP_LOGCONFIG(TAG, "LD2412:\n" " Firmware version: %s\n" " MAC address: %s", - version.c_str(), mac_str.c_str()); + version_s, mac_str); #ifdef USE_BINARY_SENSOR ESP_LOGCONFIG(TAG, "Binary Sensors:"); LOG_BINARY_SENSOR(" ", "DynamicBackgroundCorrectionStatus", @@ -492,12 +490,12 @@ bool LD2412Component::handle_ack_data_() { case CMD_QUERY_VERSION: { std::memcpy(this->version_, &this->buffer_data_[12], sizeof(this->version_)); - std::string version = str_sprintf(VERSION_FMT, this->version_[1], this->version_[0], this->version_[5], - this->version_[4], this->version_[3], this->version_[2]); - ESP_LOGV(TAG, "Firmware version: %s", version.c_str()); + char version_s[20]; + ld24xx::format_version_str(this->version_, version_s); + ESP_LOGV(TAG, "Firmware version: %s", version_s); #ifdef USE_TEXT_SENSOR if (this->version_text_sensor_ != nullptr) { - this->version_text_sensor_->publish_state(version); + this->version_text_sensor_->publish_state(version_s); } #endif break; @@ -544,9 +542,9 @@ bool LD2412Component::handle_ack_data_() { std::memcpy(this->mac_address_, &this->buffer_data_[10], sizeof(this->mac_address_)); } - std::string mac_str = - mac_address_is_valid(this->mac_address_) ? format_mac_address_pretty(this->mac_address_) : UNKNOWN_MAC; - ESP_LOGV(TAG, "MAC address: %s", mac_str.c_str()); + char mac_s[18]; + const char *mac_str = ld24xx::format_mac_str(this->mac_address_, mac_s); + ESP_LOGV(TAG, "MAC address: %s", mac_str); #ifdef USE_TEXT_SENSOR if (this->mac_text_sensor_ != nullptr) { this->mac_text_sensor_->publish_state(mac_str); diff --git a/esphome/components/ld2450/ld2450.cpp b/esphome/components/ld2450/ld2450.cpp index c9d4da47a4..8e5287aec7 100644 --- a/esphome/components/ld2450/ld2450.cpp +++ b/esphome/components/ld2450/ld2450.cpp @@ -17,8 +17,6 @@ namespace esphome { namespace ld2450 { static const char *const TAG = "ld2450"; -static const char *const UNKNOWN_MAC = "unknown"; -static const char *const VERSION_FMT = "%u.%02X.%02X%02X%02X%02X"; enum BaudRate : uint8_t { BAUD_RATE_9600 = 1, @@ -192,15 +190,15 @@ void LD2450Component::setup() { } void LD2450Component::dump_config() { - std::string mac_str = - mac_address_is_valid(this->mac_address_) ? format_mac_address_pretty(this->mac_address_) : UNKNOWN_MAC; - std::string version = str_sprintf(VERSION_FMT, this->version_[1], this->version_[0], this->version_[5], - this->version_[4], this->version_[3], this->version_[2]); + char mac_s[18]; + char version_s[20]; + const char *mac_str = ld24xx::format_mac_str(this->mac_address_, mac_s); + ld24xx::format_version_str(this->version_, version_s); ESP_LOGCONFIG(TAG, "LD2450:\n" " Firmware version: %s\n" " MAC address: %s", - version.c_str(), mac_str.c_str()); + version_s, mac_str); #ifdef USE_BINARY_SENSOR ESP_LOGCONFIG(TAG, "Binary Sensors:"); LOG_BINARY_SENSOR(" ", "MovingTarget", this->moving_target_binary_sensor_); @@ -642,12 +640,12 @@ bool LD2450Component::handle_ack_data_() { case CMD_QUERY_VERSION: { std::memcpy(this->version_, &this->buffer_data_[12], sizeof(this->version_)); - std::string version = str_sprintf(VERSION_FMT, this->version_[1], this->version_[0], this->version_[5], - this->version_[4], this->version_[3], this->version_[2]); - ESP_LOGV(TAG, "Firmware version: %s", version.c_str()); + char version_s[20]; + ld24xx::format_version_str(this->version_, version_s); + ESP_LOGV(TAG, "Firmware version: %s", version_s); #ifdef USE_TEXT_SENSOR if (this->version_text_sensor_ != nullptr) { - this->version_text_sensor_->publish_state(version); + this->version_text_sensor_->publish_state(version_s); } #endif break; @@ -663,9 +661,9 @@ bool LD2450Component::handle_ack_data_() { std::memcpy(this->mac_address_, &this->buffer_data_[10], sizeof(this->mac_address_)); } - std::string mac_str = - mac_address_is_valid(this->mac_address_) ? format_mac_address_pretty(this->mac_address_) : UNKNOWN_MAC; - ESP_LOGV(TAG, "MAC address: %s", mac_str.c_str()); + char mac_s[18]; + const char *mac_str = ld24xx::format_mac_str(this->mac_address_, mac_s); + ESP_LOGV(TAG, "MAC address: %s", mac_str); #ifdef USE_TEXT_SENSOR if (this->mac_text_sensor_ != nullptr) { this->mac_text_sensor_->publish_state(mac_str); diff --git a/esphome/components/ld24xx/ld24xx.h b/esphome/components/ld24xx/ld24xx.h index 1cd5e01163..e695b00705 100644 --- a/esphome/components/ld24xx/ld24xx.h +++ b/esphome/components/ld24xx/ld24xx.h @@ -1,11 +1,12 @@ #pragma once #include "esphome/core/defines.h" +#include "esphome/core/helpers.h" #include +#include #ifdef USE_SENSOR -#include "esphome/core/helpers.h" #include "esphome/components/sensor/sensor.h" #define SUB_SENSOR_WITH_DEDUP(name, dedup_type) \ @@ -39,6 +40,27 @@ namespace esphome { namespace ld24xx { +static const char *const UNKNOWN_MAC = "unknown"; +static const char *const VERSION_FMT = "%u.%02X.%02X%02X%02X%02X"; + +// Helper function to format MAC address with stack allocation +// Returns pointer to UNKNOWN_MAC constant or formatted buffer +// Buffer must be exactly 18 bytes (17 for "XX:XX:XX:XX:XX:XX" + null terminator) +inline const char *format_mac_str(const uint8_t *mac_address, std::span buffer) { + if (mac_address_is_valid(mac_address)) { + format_mac_addr_upper(mac_address, buffer.data()); + return buffer.data(); + } + return UNKNOWN_MAC; +} + +// Helper function to format firmware version with stack allocation +// Buffer must be exactly 20 bytes (format: "x.xxXXXXXX" fits in 11 + null terminator, 20 for safety) +inline void format_version_str(const uint8_t *version, std::span buffer) { + snprintf(buffer.data(), buffer.size(), VERSION_FMT, version[1], version[0], version[5], version[4], version[3], + version[2]); +} + #ifdef USE_SENSOR // Helper class to store a sensor with a deduplicator & publish state only when the value changes template class SensorWithDedup { From d83a698398826a7c4f70f859de58de7c1a2ec2b0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 18 Nov 2025 11:31:01 -0600 Subject: [PATCH 115/415] [scheduler] Add defensive nullptr checks and explicit locking requirements --- esphome/core/scheduler.cpp | 14 ++++++++------ esphome/core/scheduler.h | 35 +++++++++++++++++++++++------------ 2 files changed, 31 insertions(+), 18 deletions(-) diff --git a/esphome/core/scheduler.cpp b/esphome/core/scheduler.cpp index d2e0f0dab4..09d50ee7c8 100644 --- a/esphome/core/scheduler.cpp +++ b/esphome/core/scheduler.cpp @@ -154,8 +154,8 @@ void HOT Scheduler::set_timer_common_(Component *component, SchedulerItem::Type // For retries, check if there's a cancelled timeout first if (is_retry && name_cstr != nullptr && type == SchedulerItem::TIMEOUT && - (has_cancelled_timeout_in_container_(this->items_, component, name_cstr, /* match_retry= */ true) || - has_cancelled_timeout_in_container_(this->to_add_, component, name_cstr, /* match_retry= */ true))) { + (has_cancelled_timeout_in_container_locked_(this->items_, component, name_cstr, /* match_retry= */ true) || + has_cancelled_timeout_in_container_locked_(this->to_add_, component, name_cstr, /* match_retry= */ true))) { // Skip scheduling - the retry was cancelled #ifdef ESPHOME_DEBUG_SCHEDULER ESP_LOGD(TAG, "Skipping retry '%s' - found cancelled item", name_cstr); @@ -556,7 +556,8 @@ bool HOT Scheduler::cancel_item_locked_(Component *component, const char *name_c #ifndef ESPHOME_THREAD_SINGLE // Mark items in defer queue as cancelled (they'll be skipped when processed) if (type == SchedulerItem::TIMEOUT) { - total_cancelled += this->mark_matching_items_removed_(this->defer_queue_, component, name_cstr, type, match_retry); + total_cancelled += + this->mark_matching_items_removed_locked_(this->defer_queue_, component, name_cstr, type, match_retry); } #endif /* not ESPHOME_THREAD_SINGLE */ @@ -565,19 +566,20 @@ bool HOT Scheduler::cancel_item_locked_(Component *component, const char *name_c // (removing the last element doesn't break heap structure) if (!this->items_.empty()) { auto &last_item = this->items_.back(); - if (this->matches_item_(last_item, component, name_cstr, type, match_retry)) { + if (this->matches_item_locked_(last_item, component, name_cstr, type, match_retry)) { this->recycle_item_(std::move(this->items_.back())); this->items_.pop_back(); total_cancelled++; } // For other items in heap, we can only mark for removal (can't remove from middle of heap) - size_t heap_cancelled = this->mark_matching_items_removed_(this->items_, component, name_cstr, type, match_retry); + size_t heap_cancelled = + this->mark_matching_items_removed_locked_(this->items_, component, name_cstr, type, match_retry); total_cancelled += heap_cancelled; this->to_remove_ += heap_cancelled; // Track removals for heap items } // Cancel items in to_add_ - total_cancelled += this->mark_matching_items_removed_(this->to_add_, component, name_cstr, type, match_retry); + total_cancelled += this->mark_matching_items_removed_locked_(this->to_add_, component, name_cstr, type, match_retry); return total_cancelled > 0; } diff --git a/esphome/core/scheduler.h b/esphome/core/scheduler.h index fd16840240..bea1503df0 100644 --- a/esphome/core/scheduler.h +++ b/esphome/core/scheduler.h @@ -243,8 +243,18 @@ class Scheduler { } // Helper function to check if item matches criteria for cancellation - inline bool HOT matches_item_(const std::unique_ptr &item, Component *component, const char *name_cstr, - SchedulerItem::Type type, bool match_retry, bool skip_removed = true) const { + // IMPORTANT: Must be called with scheduler lock held + inline bool HOT matches_item_locked_(const std::unique_ptr &item, Component *component, + const char *name_cstr, SchedulerItem::Type type, bool match_retry, + bool skip_removed = true) const { + // THREAD SAFETY: Check for nullptr first to prevent LoadProhibited crashes. On multi-threaded + // platforms, items can be moved out of defer_queue_ during processing, leaving nullptr entries. + // PR #11305 added nullptr checks in callers (mark_matching_items_removed_locked_() and + // has_cancelled_timeout_in_container_locked_()), but this check provides defense-in-depth: helper + // functions should be safe regardless of caller behavior. + // Fixes: https://github.com/esphome/esphome/issues/11940 + if (!item) + return false; if (item->component != component || item->type != type || (skip_removed && item->remove) || (match_retry && !item->is_retry)) { return false; @@ -304,8 +314,8 @@ class Scheduler { // SAFETY: Moving out the unique_ptr leaves a nullptr in the vector at defer_queue_front_. // This is intentional and safe because: // 1. The vector is only cleaned up by cleanup_defer_queue_locked_() at the end of this function - // 2. Any code iterating defer_queue_ MUST check for nullptr items (see mark_matching_items_removed_ - // and has_cancelled_timeout_in_container_ in scheduler.h) + // 2. Any code iterating defer_queue_ MUST check for nullptr items (see mark_matching_items_removed_locked_ + // and has_cancelled_timeout_in_container_locked_ in scheduler.h) // 3. The lock protects concurrent access, but the nullptr remains until cleanup item = std::move(this->defer_queue_[this->defer_queue_front_]); this->defer_queue_front_++; @@ -393,10 +403,10 @@ class Scheduler { // Helper to mark matching items in a container as removed // Returns the number of items marked for removal - // IMPORTANT: Caller must hold the scheduler lock before calling this function. + // IMPORTANT: Must be called with scheduler lock held template - size_t mark_matching_items_removed_(Container &container, Component *component, const char *name_cstr, - SchedulerItem::Type type, bool match_retry) { + size_t mark_matching_items_removed_locked_(Container &container, Component *component, const char *name_cstr, + SchedulerItem::Type type, bool match_retry) { size_t count = 0; for (auto &item : container) { // Skip nullptr items (can happen in defer_queue_ when items are being processed) @@ -405,7 +415,7 @@ class Scheduler { // the vector can still contain nullptr items from the processing loop. This check prevents crashes. if (!item) continue; - if (this->matches_item_(item, component, name_cstr, type, match_retry)) { + if (this->matches_item_locked_(item, component, name_cstr, type, match_retry)) { // Mark item for removal (platform-specific) this->set_item_removed_(item.get(), true); count++; @@ -415,9 +425,10 @@ class Scheduler { } // Template helper to check if any item in a container matches our criteria + // IMPORTANT: Must be called with scheduler lock held template - bool has_cancelled_timeout_in_container_(const Container &container, Component *component, const char *name_cstr, - bool match_retry) const { + bool has_cancelled_timeout_in_container_locked_(const Container &container, Component *component, + const char *name_cstr, bool match_retry) const { for (const auto &item : container) { // Skip nullptr items (can happen in defer_queue_ when items are being processed) // The defer_queue_ uses index-based processing: items are std::moved out but left in the @@ -426,8 +437,8 @@ class Scheduler { if (!item) continue; if (is_item_removed_(item.get()) && - this->matches_item_(item, component, name_cstr, SchedulerItem::TIMEOUT, match_retry, - /* skip_removed= */ false)) { + this->matches_item_locked_(item, component, name_cstr, SchedulerItem::TIMEOUT, match_retry, + /* skip_removed= */ false)) { return true; } } From c59af222170518a7b85154dba9b26c53a8db0d33 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Tue, 18 Nov 2025 12:40:31 -0500 Subject: [PATCH 116/415] [esp32] Fix Arduino build on some ESP32 S2 boards (#11972) --- esphome/components/esp32/__init__.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index 0f85e585f7..6f577d2926 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -931,6 +931,12 @@ async def to_code(config): add_idf_sdkconfig_option("CONFIG_MBEDTLS_CERTIFICATE_BUNDLE", True) add_idf_sdkconfig_option("CONFIG_ESP_PHY_REDUCE_TX_POWER", True) + # ESP32-S2 Arduino: Disable USB Serial on boot to avoid TinyUSB dependency + if get_esp32_variant() == VARIANT_ESP32S2: + cg.add_build_unflag("-DARDUINO_USB_CDC_ON_BOOT=1") + cg.add_build_unflag("-DARDUINO_USB_CDC_ON_BOOT=0") + cg.add_build_flag("-DARDUINO_USB_CDC_ON_BOOT=0") + cg.add_build_flag("-Wno-nonnull-compare") add_idf_sdkconfig_option(f"CONFIG_IDF_TARGET_{variant}", True) From 1888f5ffd582e3afdbfdff8faf9efbac8202ada4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 18 Nov 2025 12:16:18 -0600 Subject: [PATCH 117/415] [scheduler] Add defensive nullptr checks and explicit locking requirements (#11974) --- esphome/core/scheduler.cpp | 14 ++++++++------ esphome/core/scheduler.h | 35 +++++++++++++++++++++++------------ 2 files changed, 31 insertions(+), 18 deletions(-) diff --git a/esphome/core/scheduler.cpp b/esphome/core/scheduler.cpp index d2e0f0dab4..09d50ee7c8 100644 --- a/esphome/core/scheduler.cpp +++ b/esphome/core/scheduler.cpp @@ -154,8 +154,8 @@ void HOT Scheduler::set_timer_common_(Component *component, SchedulerItem::Type // For retries, check if there's a cancelled timeout first if (is_retry && name_cstr != nullptr && type == SchedulerItem::TIMEOUT && - (has_cancelled_timeout_in_container_(this->items_, component, name_cstr, /* match_retry= */ true) || - has_cancelled_timeout_in_container_(this->to_add_, component, name_cstr, /* match_retry= */ true))) { + (has_cancelled_timeout_in_container_locked_(this->items_, component, name_cstr, /* match_retry= */ true) || + has_cancelled_timeout_in_container_locked_(this->to_add_, component, name_cstr, /* match_retry= */ true))) { // Skip scheduling - the retry was cancelled #ifdef ESPHOME_DEBUG_SCHEDULER ESP_LOGD(TAG, "Skipping retry '%s' - found cancelled item", name_cstr); @@ -556,7 +556,8 @@ bool HOT Scheduler::cancel_item_locked_(Component *component, const char *name_c #ifndef ESPHOME_THREAD_SINGLE // Mark items in defer queue as cancelled (they'll be skipped when processed) if (type == SchedulerItem::TIMEOUT) { - total_cancelled += this->mark_matching_items_removed_(this->defer_queue_, component, name_cstr, type, match_retry); + total_cancelled += + this->mark_matching_items_removed_locked_(this->defer_queue_, component, name_cstr, type, match_retry); } #endif /* not ESPHOME_THREAD_SINGLE */ @@ -565,19 +566,20 @@ bool HOT Scheduler::cancel_item_locked_(Component *component, const char *name_c // (removing the last element doesn't break heap structure) if (!this->items_.empty()) { auto &last_item = this->items_.back(); - if (this->matches_item_(last_item, component, name_cstr, type, match_retry)) { + if (this->matches_item_locked_(last_item, component, name_cstr, type, match_retry)) { this->recycle_item_(std::move(this->items_.back())); this->items_.pop_back(); total_cancelled++; } // For other items in heap, we can only mark for removal (can't remove from middle of heap) - size_t heap_cancelled = this->mark_matching_items_removed_(this->items_, component, name_cstr, type, match_retry); + size_t heap_cancelled = + this->mark_matching_items_removed_locked_(this->items_, component, name_cstr, type, match_retry); total_cancelled += heap_cancelled; this->to_remove_ += heap_cancelled; // Track removals for heap items } // Cancel items in to_add_ - total_cancelled += this->mark_matching_items_removed_(this->to_add_, component, name_cstr, type, match_retry); + total_cancelled += this->mark_matching_items_removed_locked_(this->to_add_, component, name_cstr, type, match_retry); return total_cancelled > 0; } diff --git a/esphome/core/scheduler.h b/esphome/core/scheduler.h index fd16840240..bea1503df0 100644 --- a/esphome/core/scheduler.h +++ b/esphome/core/scheduler.h @@ -243,8 +243,18 @@ class Scheduler { } // Helper function to check if item matches criteria for cancellation - inline bool HOT matches_item_(const std::unique_ptr &item, Component *component, const char *name_cstr, - SchedulerItem::Type type, bool match_retry, bool skip_removed = true) const { + // IMPORTANT: Must be called with scheduler lock held + inline bool HOT matches_item_locked_(const std::unique_ptr &item, Component *component, + const char *name_cstr, SchedulerItem::Type type, bool match_retry, + bool skip_removed = true) const { + // THREAD SAFETY: Check for nullptr first to prevent LoadProhibited crashes. On multi-threaded + // platforms, items can be moved out of defer_queue_ during processing, leaving nullptr entries. + // PR #11305 added nullptr checks in callers (mark_matching_items_removed_locked_() and + // has_cancelled_timeout_in_container_locked_()), but this check provides defense-in-depth: helper + // functions should be safe regardless of caller behavior. + // Fixes: https://github.com/esphome/esphome/issues/11940 + if (!item) + return false; if (item->component != component || item->type != type || (skip_removed && item->remove) || (match_retry && !item->is_retry)) { return false; @@ -304,8 +314,8 @@ class Scheduler { // SAFETY: Moving out the unique_ptr leaves a nullptr in the vector at defer_queue_front_. // This is intentional and safe because: // 1. The vector is only cleaned up by cleanup_defer_queue_locked_() at the end of this function - // 2. Any code iterating defer_queue_ MUST check for nullptr items (see mark_matching_items_removed_ - // and has_cancelled_timeout_in_container_ in scheduler.h) + // 2. Any code iterating defer_queue_ MUST check for nullptr items (see mark_matching_items_removed_locked_ + // and has_cancelled_timeout_in_container_locked_ in scheduler.h) // 3. The lock protects concurrent access, but the nullptr remains until cleanup item = std::move(this->defer_queue_[this->defer_queue_front_]); this->defer_queue_front_++; @@ -393,10 +403,10 @@ class Scheduler { // Helper to mark matching items in a container as removed // Returns the number of items marked for removal - // IMPORTANT: Caller must hold the scheduler lock before calling this function. + // IMPORTANT: Must be called with scheduler lock held template - size_t mark_matching_items_removed_(Container &container, Component *component, const char *name_cstr, - SchedulerItem::Type type, bool match_retry) { + size_t mark_matching_items_removed_locked_(Container &container, Component *component, const char *name_cstr, + SchedulerItem::Type type, bool match_retry) { size_t count = 0; for (auto &item : container) { // Skip nullptr items (can happen in defer_queue_ when items are being processed) @@ -405,7 +415,7 @@ class Scheduler { // the vector can still contain nullptr items from the processing loop. This check prevents crashes. if (!item) continue; - if (this->matches_item_(item, component, name_cstr, type, match_retry)) { + if (this->matches_item_locked_(item, component, name_cstr, type, match_retry)) { // Mark item for removal (platform-specific) this->set_item_removed_(item.get(), true); count++; @@ -415,9 +425,10 @@ class Scheduler { } // Template helper to check if any item in a container matches our criteria + // IMPORTANT: Must be called with scheduler lock held template - bool has_cancelled_timeout_in_container_(const Container &container, Component *component, const char *name_cstr, - bool match_retry) const { + bool has_cancelled_timeout_in_container_locked_(const Container &container, Component *component, + const char *name_cstr, bool match_retry) const { for (const auto &item : container) { // Skip nullptr items (can happen in defer_queue_ when items are being processed) // The defer_queue_ uses index-based processing: items are std::moved out but left in the @@ -426,8 +437,8 @@ class Scheduler { if (!item) continue; if (is_item_removed_(item.get()) && - this->matches_item_(item, component, name_cstr, SchedulerItem::TIMEOUT, match_retry, - /* skip_removed= */ false)) { + this->matches_item_locked_(item, component, name_cstr, SchedulerItem::TIMEOUT, match_retry, + /* skip_removed= */ false)) { return true; } } From fe2befcec2c50704afb816f5434da1871393522d Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Tue, 18 Nov 2025 13:18:09 -0500 Subject: [PATCH 118/415] [bme68x] Print error when no sensors are configured (#11976) --- esphome/components/bme68x_bsec2/bme68x_bsec2.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/esphome/components/bme68x_bsec2/bme68x_bsec2.cpp b/esphome/components/bme68x_bsec2/bme68x_bsec2.cpp index f5dcfd65a1..91383c8d45 100644 --- a/esphome/components/bme68x_bsec2/bme68x_bsec2.cpp +++ b/esphome/components/bme68x_bsec2/bme68x_bsec2.cpp @@ -70,6 +70,9 @@ void BME68xBSEC2Component::dump_config() { if (this->is_failed()) { ESP_LOGE(TAG, "Communication failed (BSEC2 status: %d, BME68X status: %d)", this->bsec_status_, this->bme68x_status_); + if (this->bsec_status_ == BSEC_I_SU_SUBSCRIBEDOUTPUTGATES) { + ESP_LOGE(TAG, "No sensors, add at least one sensor to the config"); + } } if (this->algorithm_output_ != ALGORITHM_OUTPUT_IAQ) { From 72e4b16a5b58d0a2f542d619daad19873f2e5020 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Tue, 18 Nov 2025 13:29:40 -0500 Subject: [PATCH 119/415] [sfa30] Fix negative temperature values (#11973) --- esphome/components/sfa30/sfa30.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/esphome/components/sfa30/sfa30.cpp b/esphome/components/sfa30/sfa30.cpp index 99709d5fbb..bbe3bcd7d2 100644 --- a/esphome/components/sfa30/sfa30.cpp +++ b/esphome/components/sfa30/sfa30.cpp @@ -73,17 +73,17 @@ void SFA30Component::update() { } if (this->formaldehyde_sensor_ != nullptr) { - const float formaldehyde = raw_data[0] / 5.0f; + const float formaldehyde = static_cast(raw_data[0]) / 5.0f; this->formaldehyde_sensor_->publish_state(formaldehyde); } if (this->humidity_sensor_ != nullptr) { - const float humidity = raw_data[1] / 100.0f; + const float humidity = static_cast(raw_data[1]) / 100.0f; this->humidity_sensor_->publish_state(humidity); } if (this->temperature_sensor_ != nullptr) { - const float temperature = raw_data[2] / 200.0f; + const float temperature = static_cast(raw_data[2]) / 200.0f; this->temperature_sensor_->publish_state(temperature); } From 81fe5deaa9f376764facd3cd5f26c04e9d4e8aac Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 19 Nov 2025 08:12:42 +1300 Subject: [PATCH 120/415] Bump github/codeql-action from 4.31.3 to 4.31.4 (#11977) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/codeql.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index adb2a9d79f..21fff10c95 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -58,7 +58,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@014f16e7ab1402f30e7c3329d33797e7948572db # v4.31.3 + uses: github/codeql-action/init@e12f0178983d466f2f6028f5cc7a6d786fd97f4b # v4.31.4 with: languages: ${{ matrix.language }} build-mode: ${{ matrix.build-mode }} @@ -86,6 +86,6 @@ jobs: exit 1 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@014f16e7ab1402f30e7c3329d33797e7948572db # v4.31.3 + uses: github/codeql-action/analyze@e12f0178983d466f2f6028f5cc7a6d786fd97f4b # v4.31.4 with: category: "/language:${{matrix.language}}" From 70ed9c7c4db30952faabdd8b7c6373cc28e41419 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 18 Nov 2025 13:17:21 -0600 Subject: [PATCH 121/415] [wifi] Fix captive portal unusable when WiFi credentials are wrong (#11965) --- esphome/components/captive_portal/__init__.py | 10 +++ .../captive_portal/captive_portal.cpp | 10 ++- .../captive_portal/captive_portal.h | 4 ++ esphome/components/esp32_improv/__init__.py | 6 +- .../esp32_improv/esp32_improv_component.cpp | 1 + .../esp32_improv/esp32_improv_component.h | 1 + .../web_server_idf/web_server_idf.cpp | 15 +++++ .../web_server_idf/web_server_idf.h | 4 ++ esphome/components/wifi/__init__.py | 8 ++- esphome/components/wifi/wifi_component.cpp | 62 +++++++++++++++---- esphome/components/wifi/wifi_component.h | 5 ++ 11 files changed, 111 insertions(+), 15 deletions(-) diff --git a/esphome/components/captive_portal/__init__.py b/esphome/components/captive_portal/__init__.py index 9bd3ef8a05..25d0a22083 100644 --- a/esphome/components/captive_portal/__init__.py +++ b/esphome/components/captive_portal/__init__.py @@ -72,6 +72,16 @@ def _final_validate(config: ConfigType) -> ConfigType: "Add 'ap:' to your WiFi configuration to enable the captive portal." ) + # Register socket needs for DNS server and additional HTTP connections + # - 1 UDP socket for DNS server + # - 3 additional TCP sockets for captive portal detection probes + configuration requests + # OS captive portal detection makes multiple probe requests that stay in TIME_WAIT. + # Need headroom for actual user configuration requests. + # LRU purging will reclaim idle sockets to prevent exhaustion from repeated attempts. + from esphome.components import socket + + socket.consume_sockets(4, "captive_portal")(config) + return config diff --git a/esphome/components/captive_portal/captive_portal.cpp b/esphome/components/captive_portal/captive_portal.cpp index 30438747f2..459ac557c8 100644 --- a/esphome/components/captive_portal/captive_portal.cpp +++ b/esphome/components/captive_portal/captive_portal.cpp @@ -50,8 +50,8 @@ void CaptivePortal::handle_wifisave(AsyncWebServerRequest *request) { ESP_LOGI(TAG, "Requested WiFi Settings Change:"); ESP_LOGI(TAG, " SSID='%s'", ssid.c_str()); ESP_LOGI(TAG, " Password=" LOG_SECRET("'%s'"), psk.c_str()); - wifi::global_wifi_component->save_wifi_sta(ssid, psk); - wifi::global_wifi_component->start_scanning(); + // Defer save to main loop thread to avoid NVS operations from HTTP thread + this->defer([ssid, psk]() { wifi::global_wifi_component->save_wifi_sta(ssid, psk); }); request->redirect(ESPHOME_F("/?save")); } @@ -63,6 +63,12 @@ void CaptivePortal::start() { this->base_->init(); if (!this->initialized_) { this->base_->add_handler(this); +#ifdef USE_ESP32 + // Enable LRU socket purging to handle captive portal detection probe bursts + // OS captive portal detection makes many simultaneous HTTP requests which can + // exhaust sockets. LRU purging automatically closes oldest idle connections. + this->base_->get_server()->set_lru_purge_enable(true); +#endif } network::IPAddress ip = wifi::global_wifi_component->wifi_soft_ap_ip(); diff --git a/esphome/components/captive_portal/captive_portal.h b/esphome/components/captive_portal/captive_portal.h index f48c286f0c..ae9b9dfba0 100644 --- a/esphome/components/captive_portal/captive_portal.h +++ b/esphome/components/captive_portal/captive_portal.h @@ -40,6 +40,10 @@ class CaptivePortal : public AsyncWebHandler, public Component { void end() { this->active_ = false; this->disable_loop(); // Stop processing DNS requests +#ifdef USE_ESP32 + // Disable LRU socket purging now that captive portal is done + this->base_->get_server()->set_lru_purge_enable(false); +#endif this->base_->deinit(); if (this->dns_server_ != nullptr) { this->dns_server_->stop(); diff --git a/esphome/components/esp32_improv/__init__.py b/esphome/components/esp32_improv/__init__.py index 1a7194da81..2e69d400ca 100644 --- a/esphome/components/esp32_improv/__init__.py +++ b/esphome/components/esp32_improv/__init__.py @@ -20,6 +20,10 @@ CONF_ON_STOP = "on_stop" CONF_STATUS_INDICATOR = "status_indicator" CONF_WIFI_TIMEOUT = "wifi_timeout" +# Default WiFi timeout - aligned with WiFi component ap_timeout +# Allows sufficient time to try all BSSIDs before starting provisioning mode +DEFAULT_WIFI_TIMEOUT = "90s" + improv_ns = cg.esphome_ns.namespace("improv") Error = improv_ns.enum("Error") @@ -59,7 +63,7 @@ CONFIG_SCHEMA = ( CONF_AUTHORIZED_DURATION, default="1min" ): cv.positive_time_period_milliseconds, cv.Optional( - CONF_WIFI_TIMEOUT, default="1min" + CONF_WIFI_TIMEOUT, default=DEFAULT_WIFI_TIMEOUT ): cv.positive_time_period_milliseconds, cv.Optional(CONF_ON_PROVISIONED): automation.validate_automation( { diff --git a/esphome/components/esp32_improv/esp32_improv_component.cpp b/esphome/components/esp32_improv/esp32_improv_component.cpp index 398b1d4251..0ad54bbb15 100644 --- a/esphome/components/esp32_improv/esp32_improv_component.cpp +++ b/esphome/components/esp32_improv/esp32_improv_component.cpp @@ -127,6 +127,7 @@ void ESP32ImprovComponent::loop() { // Set initial state based on whether we have an authorizer this->set_state_(this->get_initial_state_(), false); this->set_error_(improv::ERROR_NONE); + this->should_start_ = false; // Clear flag after starting ESP_LOGD(TAG, "Service started!"); } } diff --git a/esphome/components/esp32_improv/esp32_improv_component.h b/esphome/components/esp32_improv/esp32_improv_component.h index 989552ea56..8f4cfd7958 100644 --- a/esphome/components/esp32_improv/esp32_improv_component.h +++ b/esphome/components/esp32_improv/esp32_improv_component.h @@ -45,6 +45,7 @@ class ESP32ImprovComponent : public Component, public improv_base::ImprovBase { void start(); void stop(); bool is_active() const { return this->state_ != improv::STATE_STOPPED; } + bool should_start() const { return this->should_start_; } #ifdef USE_ESP32_IMPROV_STATE_CALLBACK void add_on_state_callback(std::function &&callback) { diff --git a/esphome/components/web_server_idf/web_server_idf.cpp b/esphome/components/web_server_idf/web_server_idf.cpp index ce91569de2..f5a66f6bd9 100644 --- a/esphome/components/web_server_idf/web_server_idf.cpp +++ b/esphome/components/web_server_idf/web_server_idf.cpp @@ -94,6 +94,18 @@ void AsyncWebServer::end() { } } +void AsyncWebServer::set_lru_purge_enable(bool enable) { + if (this->lru_purge_enable_ == enable) { + return; // No change needed + } + this->lru_purge_enable_ = enable; + // If server is already running, restart it with new config + if (this->server_) { + this->end(); + this->begin(); + } +} + void AsyncWebServer::begin() { if (this->server_) { this->end(); @@ -101,6 +113,8 @@ void AsyncWebServer::begin() { httpd_config_t config = HTTPD_DEFAULT_CONFIG(); config.server_port = this->port_; config.uri_match_fn = [](const char * /*unused*/, const char * /*unused*/, size_t /*unused*/) { return true; }; + // Enable LRU purging if requested (e.g., by captive portal to handle probe bursts) + config.lru_purge_enable = this->lru_purge_enable_; if (httpd_start(&this->server_, &config) == ESP_OK) { const httpd_uri_t handler_get = { .uri = "", @@ -242,6 +256,7 @@ void AsyncWebServerRequest::send(int code, const char *content_type, const char void AsyncWebServerRequest::redirect(const std::string &url) { httpd_resp_set_status(*this, "302 Found"); httpd_resp_set_hdr(*this, "Location", url.c_str()); + httpd_resp_set_hdr(*this, "Connection", "close"); httpd_resp_send(*this, nullptr, 0); } diff --git a/esphome/components/web_server_idf/web_server_idf.h b/esphome/components/web_server_idf/web_server_idf.h index 5ec6fec009..b9f690b462 100644 --- a/esphome/components/web_server_idf/web_server_idf.h +++ b/esphome/components/web_server_idf/web_server_idf.h @@ -199,9 +199,13 @@ class AsyncWebServer { return *handler; } + void set_lru_purge_enable(bool enable); + httpd_handle_t get_server() { return this->server_; } + protected: uint16_t port_{}; httpd_handle_t server_{}; + bool lru_purge_enable_{false}; static esp_err_t request_handler(httpd_req_t *r); static esp_err_t request_post_handler(httpd_req_t *r); esp_err_t request_handler_(AsyncWebServerRequest *request) const; diff --git a/esphome/components/wifi/__init__.py b/esphome/components/wifi/__init__.py index f543d972c9..2b21478f30 100644 --- a/esphome/components/wifi/__init__.py +++ b/esphome/components/wifi/__init__.py @@ -69,6 +69,12 @@ CONF_MIN_AUTH_MODE = "min_auth_mode" # Limited to 127 because selected_sta_index_ is int8_t in C++ MAX_WIFI_NETWORKS = 127 +# Default AP timeout - allows sufficient time to try all BSSIDs during initial connection +# After AP starts, WiFi scanning is skipped to avoid disrupting the AP, so we only +# get best-effort connection attempts. Longer timeout ensures we exhaust all options +# before falling back to AP mode. Aligned with improv wifi_timeout default. +DEFAULT_AP_TIMEOUT = "90s" + wifi_ns = cg.esphome_ns.namespace("wifi") EAPAuth = wifi_ns.struct("EAPAuth") ManualIP = wifi_ns.struct("ManualIP") @@ -177,7 +183,7 @@ CONF_AP_TIMEOUT = "ap_timeout" WIFI_NETWORK_AP = WIFI_NETWORK_BASE.extend( { cv.Optional( - CONF_AP_TIMEOUT, default="1min" + CONF_AP_TIMEOUT, default=DEFAULT_AP_TIMEOUT ): cv.positive_time_period_milliseconds, } ) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index 51a5a47323..30340601fb 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -199,7 +199,12 @@ 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 uint32_t WIFI_COOLDOWN_DURATION_MS = 500; + +/// Cooldown duration when fallback AP is active and captive portal may be running +/// Longer interval gives users time to configure WiFi without constant connection attempts +/// While connecting, WiFi can't beacon the AP properly, so needs longer cooldown +static constexpr uint32_t WIFI_COOLDOWN_WITH_AP_ACTIVE_MS = 30000; static constexpr uint8_t get_max_retries_for_phase(WiFiRetryPhase phase) { switch (phase) { @@ -275,7 +280,9 @@ int8_t WiFiComponent::find_next_hidden_sta_(int8_t start_index) { } } - if (!this->ssid_was_seen_in_scan_(sta.get_ssid())) { + // If we didn't scan this cycle, treat all networks as potentially hidden + // Otherwise, only retry networks that weren't seen in the scan + if (!this->did_scan_this_cycle_ || !this->ssid_was_seen_in_scan_(sta.get_ssid())) { ESP_LOGD(TAG, "Hidden candidate " LOG_SECRET("'%s'") " at index %d", sta.get_ssid().c_str(), static_cast(i)); return static_cast(i); } @@ -417,10 +424,6 @@ void WiFiComponent::start() { void WiFiComponent::restart_adapter() { ESP_LOGW(TAG, "Restarting adapter"); this->wifi_mode_(false, {}); - // 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; } @@ -441,7 +444,16 @@ void WiFiComponent::loop() { switch (this->state_) { case WIFI_COMPONENT_STATE_COOLDOWN: { this->status_set_warning(LOG_STR("waiting to reconnect")); - if (now - this->action_started_ > WIFI_COOLDOWN_DURATION_MS) { + // Skip cooldown if new credentials were provided while connecting + if (this->skip_cooldown_next_cycle_) { + this->skip_cooldown_next_cycle_ = false; + this->check_connecting_finished(); + break; + } + // Use longer cooldown when captive portal/improv is active to avoid disrupting user config + bool portal_active = this->is_captive_portal_active_() || this->is_esp32_improv_active_(); + uint32_t cooldown_duration = portal_active ? WIFI_COOLDOWN_WITH_AP_ACTIVE_MS : WIFI_COOLDOWN_DURATION_MS; + if (now - this->action_started_ > cooldown_duration) { // 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 @@ -495,7 +507,8 @@ void WiFiComponent::loop() { #endif // USE_WIFI_AP #ifdef USE_IMPROV - if (esp32_improv::global_improv_component != nullptr && !esp32_improv::global_improv_component->is_active()) { + if (esp32_improv::global_improv_component != nullptr && !esp32_improv::global_improv_component->is_active() && + !esp32_improv::global_improv_component->should_start()) { if (now - this->last_connected_ > esp32_improv::global_improv_component->get_wifi_timeout()) { if (this->wifi_mode_(true, {})) esp32_improv::global_improv_component->start(); @@ -605,6 +618,8 @@ void WiFiComponent::set_sta(const WiFiAP &ap) { this->init_sta(1); this->add_sta(ap); this->selected_sta_index_ = 0; + // When new credentials are set (e.g., from improv), skip cooldown to retry immediately + this->skip_cooldown_next_cycle_ = true; } WiFiAP WiFiComponent::build_params_for_current_phase_() { @@ -666,6 +681,17 @@ void WiFiComponent::save_wifi_sta(const std::string &ssid, const std::string &pa sta.set_ssid(ssid); sta.set_password(password); this->set_sta(sta); + + // Trigger connection attempt (exits cooldown if needed, no-op if already connecting/connected) + this->connect_soon_(); +} + +void WiFiComponent::connect_soon_() { + // Only trigger retry if we're in cooldown - if already connecting/connected, do nothing + if (this->state_ == WIFI_COMPONENT_STATE_COOLDOWN) { + ESP_LOGD(TAG, "Exiting cooldown early due to new WiFi credentials"); + this->retry_connect(); + } } void WiFiComponent::start_connecting(const WiFiAP &ap) { @@ -963,6 +989,7 @@ void WiFiComponent::check_scanning_finished() { return; } this->scan_done_ = false; + this->did_scan_this_cycle_ = true; if (this->scan_result_.empty()) { ESP_LOGW(TAG, "No networks found"); @@ -1229,9 +1256,16 @@ WiFiRetryPhase WiFiComponent::determine_next_phase_() { return WiFiRetryPhase::RESTARTING_ADAPTER; case WiFiRetryPhase::RESTARTING_ADAPTER: - // After restart, 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; + // After restart, go back to explicit hidden if we went through it initially + if (this->went_through_explicit_hidden_phase_()) { + return WiFiRetryPhase::EXPLICIT_HIDDEN; + } + // Skip scanning when captive portal/improv is active to avoid disrupting AP + // Even passive scans can cause brief AP disconnections on ESP32 + if (this->is_captive_portal_active_() || this->is_esp32_improv_active_()) { + return WiFiRetryPhase::RETRY_HIDDEN; + } + return WiFiRetryPhase::SCAN_CONNECTING; } // Should never reach here @@ -1319,6 +1353,12 @@ bool WiFiComponent::transition_to_phase_(WiFiRetryPhase new_phase) { if (!this->is_captive_portal_active_() && !this->is_esp32_improv_active_()) { this->restart_adapter(); } + // Clear scan flag - we're starting a new retry cycle + this->did_scan_this_cycle_ = false; + // Always enter cooldown after restart (or skip-restart) to allow stabilization + // Use extended cooldown when AP is active to avoid constant scanning that blocks DNS + this->state_ = WIFI_COMPONENT_STATE_COOLDOWN; + this->action_started_ = millis(); // Return true to indicate we should wait (go to COOLDOWN) instead of immediately connecting return true; diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index 66e2ccf1cb..b3548078bc 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -291,6 +291,7 @@ class WiFiComponent : public Component { void set_passive_scan(bool passive); void save_wifi_sta(const std::string &ssid, const std::string &password); + // ========== INTERNAL METHODS ========== // (In most use cases you won't need these) /// Setup WiFi interface. @@ -424,6 +425,8 @@ class WiFiComponent : public Component { return true; } + void connect_soon_(); + void wifi_loop_(); bool wifi_mode_(optional sta, optional ap); bool wifi_sta_pre_setup_(); @@ -529,6 +532,8 @@ class WiFiComponent : public Component { bool enable_on_boot_{true}; bool got_ipv4_address_{false}; bool keep_scan_results_{false}; + bool did_scan_this_cycle_{false}; + bool skip_cooldown_next_cycle_{false}; // Pointers at the end (naturally aligned) Trigger<> *connect_trigger_{new Trigger<>()}; From 6c8577678c63c01c12734f9c0cbc0966268bbffa Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 12 Nov 2025 14:01:07 -0600 Subject: [PATCH 122/415] [captive_portal] Warn when enabled without WiFi AP configured (#11856) --- esphome/components/captive_portal/__init__.py | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/esphome/components/captive_portal/__init__.py b/esphome/components/captive_portal/__init__.py index 99acb76bcf..9bd3ef8a05 100644 --- a/esphome/components/captive_portal/__init__.py +++ b/esphome/components/captive_portal/__init__.py @@ -1,9 +1,12 @@ +import logging + import esphome.codegen as cg from esphome.components import web_server_base from esphome.components.web_server_base import CONF_WEB_SERVER_BASE_ID from esphome.config_helpers import filter_source_files_from_platform import esphome.config_validation as cv from esphome.const import ( + CONF_AP, CONF_ID, PLATFORM_BK72XX, PLATFORM_ESP32, @@ -14,6 +17,10 @@ from esphome.const import ( ) from esphome.core import CORE, coroutine_with_priority from esphome.coroutine import CoroPriority +import esphome.final_validate as fv +from esphome.types import ConfigType + +_LOGGER = logging.getLogger(__name__) def AUTO_LOAD() -> list[str]: @@ -50,6 +57,27 @@ CONFIG_SCHEMA = cv.All( ) +def _final_validate(config: ConfigType) -> ConfigType: + full_config = fv.full_config.get() + wifi_conf = full_config.get("wifi") + + if wifi_conf is None: + # This shouldn't happen due to DEPENDENCIES = ["wifi"], but check anyway + raise cv.Invalid("Captive portal requires the wifi component to be configured") + + if CONF_AP not in wifi_conf: + _LOGGER.warning( + "Captive portal is enabled but no WiFi AP is configured. " + "The captive portal will not be accessible. " + "Add 'ap:' to your WiFi configuration to enable the captive portal." + ) + + return config + + +FINAL_VALIDATE_SCHEMA = _final_validate + + @coroutine_with_priority(CoroPriority.CAPTIVE_PORTAL) async def to_code(config): paren = await cg.get_variable(config[CONF_WEB_SERVER_BASE_ID]) From 3b25fdbc5f458067494a87ddb8f59e4669ae85ad Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Mon, 17 Nov 2025 12:32:08 -0500 Subject: [PATCH 123/415] [core] Add support for setting environment variables (#11953) --- esphome/const.py | 1 + esphome/core/config.py | 15 +++++++++++++++ tests/components/esphome/common.yaml | 3 +++ 3 files changed, 19 insertions(+) diff --git a/esphome/const.py b/esphome/const.py index 2ca0018fdb..8360531bff 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -336,6 +336,7 @@ CONF_ENERGY = "energy" CONF_ENTITY_CATEGORY = "entity_category" CONF_ENTITY_ID = "entity_id" CONF_ENUM_DATAPOINT = "enum_datapoint" +CONF_ENVIRONMENT_VARIABLES = "environment_variables" CONF_EQUATION = "equation" CONF_ESP8266_DISABLE_SSL_SUPPORT = "esp8266_disable_ssl_support" CONF_ESPHOME = "esphome" diff --git a/esphome/core/config.py b/esphome/core/config.py index 763f9ebd9f..0a239c5f5e 100644 --- a/esphome/core/config.py +++ b/esphome/core/config.py @@ -17,6 +17,7 @@ from esphome.const import ( CONF_COMPILE_PROCESS_LIMIT, CONF_DEBUG_SCHEDULER, CONF_DEVICES, + CONF_ENVIRONMENT_VARIABLES, CONF_ESPHOME, CONF_FRIENDLY_NAME, CONF_ID, @@ -215,6 +216,11 @@ CONFIG_SCHEMA = cv.All( cv.string_strict: cv.Any([cv.string], cv.string), } ), + cv.Optional(CONF_ENVIRONMENT_VARIABLES, default={}): cv.Schema( + { + cv.string_strict: cv.string, + } + ), cv.Optional(CONF_ON_BOOT): automation.validate_automation( { cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(StartupTrigger), @@ -426,6 +432,12 @@ async def _add_platformio_options(pio_options): cg.add_platformio_option(key, val) +@coroutine_with_priority(CoroPriority.FINAL) +async def _add_environment_variables(env_vars: dict[str, str]) -> None: + # Set environment variables for the build process + os.environ.update(env_vars) + + @coroutine_with_priority(CoroPriority.AUTOMATION) async def _add_automations(config): for conf in config.get(CONF_ON_BOOT, []): @@ -563,6 +575,9 @@ async def to_code(config: ConfigType) -> None: if config[CONF_PLATFORMIO_OPTIONS]: CORE.add_job(_add_platformio_options, config[CONF_PLATFORMIO_OPTIONS]) + if config[CONF_ENVIRONMENT_VARIABLES]: + CORE.add_job(_add_environment_variables, config[CONF_ENVIRONMENT_VARIABLES]) + # Process areas all_areas: list[dict[str, str | core.ID]] = [] if CONF_AREA in config: diff --git a/tests/components/esphome/common.yaml b/tests/components/esphome/common.yaml index b2d7bccaa5..db75b08b38 100644 --- a/tests/components/esphome/common.yaml +++ b/tests/components/esphome/common.yaml @@ -2,6 +2,9 @@ esphome: debug_scheduler: true platformio_options: board_build.flash_mode: dio + environment_variables: + TEST_ENV_VAR: "test_value" + BUILD_NUMBER: "12345" area: id: testing_area name: Testing Area From e8998a79c71309164c0d4f4ab8cb0cce6c1b25c0 Mon Sep 17 00:00:00 2001 From: strange_v Date: Tue, 18 Nov 2025 02:32:17 +0100 Subject: [PATCH 124/415] [mipi_rgb] Fix GUITION-4848S040 colors (#11709) --- esphome/components/mipi_rgb/mipi_rgb.cpp | 15 ++++++++------- esphome/components/mipi_rgb/models/guition.py | 1 + 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/esphome/components/mipi_rgb/mipi_rgb.cpp b/esphome/components/mipi_rgb/mipi_rgb.cpp index 00c9c8cbff..080fb08c09 100644 --- a/esphome/components/mipi_rgb/mipi_rgb.cpp +++ b/esphome/components/mipi_rgb/mipi_rgb.cpp @@ -350,6 +350,7 @@ void MipiRgb::dump_config() { "\n Width: %u" "\n Height: %u" "\n Rotation: %d degrees" + "\n PCLK Inverted: %s" "\n HSync Pulse Width: %u" "\n HSync Back Porch: %u" "\n HSync Front Porch: %u" @@ -357,18 +358,18 @@ void MipiRgb::dump_config() { "\n VSync Back Porch: %u" "\n VSync Front Porch: %u" "\n Invert Colors: %s" - "\n Pixel Clock: %dMHz" + "\n Pixel Clock: %uMHz" "\n Reset Pin: %s" "\n DE Pin: %s" "\n PCLK Pin: %s" "\n HSYNC Pin: %s" "\n VSYNC Pin: %s", - this->model_, this->width_, this->height_, this->rotation_, this->hsync_pulse_width_, - this->hsync_back_porch_, this->hsync_front_porch_, this->vsync_pulse_width_, this->vsync_back_porch_, - this->vsync_front_porch_, YESNO(this->invert_colors_), this->pclk_frequency_ / 1000000, - get_pin_name(this->reset_pin_).c_str(), get_pin_name(this->de_pin_).c_str(), - get_pin_name(this->pclk_pin_).c_str(), get_pin_name(this->hsync_pin_).c_str(), - get_pin_name(this->vsync_pin_).c_str()); + this->model_, this->width_, this->height_, this->rotation_, YESNO(this->pclk_inverted_), + this->hsync_pulse_width_, this->hsync_back_porch_, this->hsync_front_porch_, this->vsync_pulse_width_, + this->vsync_back_porch_, this->vsync_front_porch_, YESNO(this->invert_colors_), + (unsigned) (this->pclk_frequency_ / 1000000), get_pin_name(this->reset_pin_).c_str(), + get_pin_name(this->de_pin_).c_str(), get_pin_name(this->pclk_pin_).c_str(), + get_pin_name(this->hsync_pin_).c_str(), get_pin_name(this->vsync_pin_).c_str()); if (this->madctl_ & MADCTL_BGR) { this->dump_pins_(8, 13, "Blue", 0); diff --git a/esphome/components/mipi_rgb/models/guition.py b/esphome/components/mipi_rgb/models/guition.py index da433e686e..915b8beda0 100644 --- a/esphome/components/mipi_rgb/models/guition.py +++ b/esphome/components/mipi_rgb/models/guition.py @@ -11,6 +11,7 @@ st7701s.extend( vsync_pin=17, pclk_pin=21, pclk_frequency="12MHz", + pclk_inverted=False, pixel_mode="18bit", mirror_x=True, mirror_y=True, From 70aa94b8a40845b23f573d425845e11376f24fe3 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Tue, 18 Nov 2025 14:27:50 +1000 Subject: [PATCH 125/415] [lvgl] Apply scale to spinbox value (#11946) --- esphome/components/lvgl/widgets/spinbox.py | 5 ++++- tests/components/lvgl/lvgl-package.yaml | 4 +++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/esphome/components/lvgl/widgets/spinbox.py b/esphome/components/lvgl/widgets/spinbox.py index ac23ded723..c6f25e9587 100644 --- a/esphome/components/lvgl/widgets/spinbox.py +++ b/esphome/components/lvgl/widgets/spinbox.py @@ -1,6 +1,7 @@ from esphome import automation import esphome.config_validation as cv from esphome.const import CONF_ID, CONF_RANGE_FROM, CONF_RANGE_TO, CONF_STEP, CONF_VALUE +from esphome.cpp_generator import MockObj from ..automation import action_to_code from ..defines import ( @@ -114,7 +115,9 @@ class SpinboxType(WidgetType): w.obj, digits, digits - config[CONF_DECIMAL_PLACES] ) if (value := config.get(CONF_VALUE)) is not None: - lv.spinbox_set_value(w.obj, await lv_float.process(value)) + lv.spinbox_set_value( + w.obj, MockObj(await lv_float.process(value)) * w.get_scale() + ) def get_scale(self, config): return 10 ** config[CONF_DECIMAL_PLACES] diff --git a/tests/components/lvgl/lvgl-package.yaml b/tests/components/lvgl/lvgl-package.yaml index d7c342b16e..fba860a407 100644 --- a/tests/components/lvgl/lvgl-package.yaml +++ b/tests/components/lvgl/lvgl-package.yaml @@ -703,7 +703,9 @@ lvgl: on_value: - lvgl.spinbox.update: id: spinbox_id - value: !lambda return x; + value: !lambda |- + static float yyy = 83.0; + return yyy + .8; - button: styles: spin_button id: spin_up From 93215f17375679b1c847b2a359bfe19533a9dff0 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Tue, 18 Nov 2025 12:40:31 -0500 Subject: [PATCH 126/415] [esp32] Fix Arduino build on some ESP32 S2 boards (#11972) --- esphome/components/esp32/__init__.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index 0f85e585f7..6f577d2926 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -931,6 +931,12 @@ async def to_code(config): add_idf_sdkconfig_option("CONFIG_MBEDTLS_CERTIFICATE_BUNDLE", True) add_idf_sdkconfig_option("CONFIG_ESP_PHY_REDUCE_TX_POWER", True) + # ESP32-S2 Arduino: Disable USB Serial on boot to avoid TinyUSB dependency + if get_esp32_variant() == VARIANT_ESP32S2: + cg.add_build_unflag("-DARDUINO_USB_CDC_ON_BOOT=1") + cg.add_build_unflag("-DARDUINO_USB_CDC_ON_BOOT=0") + cg.add_build_flag("-DARDUINO_USB_CDC_ON_BOOT=0") + cg.add_build_flag("-Wno-nonnull-compare") add_idf_sdkconfig_option(f"CONFIG_IDF_TARGET_{variant}", True) From 6db73df649ce1d63c66fd1ead771a31bd5c5f912 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 18 Nov 2025 12:16:18 -0600 Subject: [PATCH 127/415] [scheduler] Add defensive nullptr checks and explicit locking requirements (#11974) --- esphome/core/scheduler.cpp | 14 ++++++++------ esphome/core/scheduler.h | 35 +++++++++++++++++++++++------------ 2 files changed, 31 insertions(+), 18 deletions(-) diff --git a/esphome/core/scheduler.cpp b/esphome/core/scheduler.cpp index d2e0f0dab4..09d50ee7c8 100644 --- a/esphome/core/scheduler.cpp +++ b/esphome/core/scheduler.cpp @@ -154,8 +154,8 @@ void HOT Scheduler::set_timer_common_(Component *component, SchedulerItem::Type // For retries, check if there's a cancelled timeout first if (is_retry && name_cstr != nullptr && type == SchedulerItem::TIMEOUT && - (has_cancelled_timeout_in_container_(this->items_, component, name_cstr, /* match_retry= */ true) || - has_cancelled_timeout_in_container_(this->to_add_, component, name_cstr, /* match_retry= */ true))) { + (has_cancelled_timeout_in_container_locked_(this->items_, component, name_cstr, /* match_retry= */ true) || + has_cancelled_timeout_in_container_locked_(this->to_add_, component, name_cstr, /* match_retry= */ true))) { // Skip scheduling - the retry was cancelled #ifdef ESPHOME_DEBUG_SCHEDULER ESP_LOGD(TAG, "Skipping retry '%s' - found cancelled item", name_cstr); @@ -556,7 +556,8 @@ bool HOT Scheduler::cancel_item_locked_(Component *component, const char *name_c #ifndef ESPHOME_THREAD_SINGLE // Mark items in defer queue as cancelled (they'll be skipped when processed) if (type == SchedulerItem::TIMEOUT) { - total_cancelled += this->mark_matching_items_removed_(this->defer_queue_, component, name_cstr, type, match_retry); + total_cancelled += + this->mark_matching_items_removed_locked_(this->defer_queue_, component, name_cstr, type, match_retry); } #endif /* not ESPHOME_THREAD_SINGLE */ @@ -565,19 +566,20 @@ bool HOT Scheduler::cancel_item_locked_(Component *component, const char *name_c // (removing the last element doesn't break heap structure) if (!this->items_.empty()) { auto &last_item = this->items_.back(); - if (this->matches_item_(last_item, component, name_cstr, type, match_retry)) { + if (this->matches_item_locked_(last_item, component, name_cstr, type, match_retry)) { this->recycle_item_(std::move(this->items_.back())); this->items_.pop_back(); total_cancelled++; } // For other items in heap, we can only mark for removal (can't remove from middle of heap) - size_t heap_cancelled = this->mark_matching_items_removed_(this->items_, component, name_cstr, type, match_retry); + size_t heap_cancelled = + this->mark_matching_items_removed_locked_(this->items_, component, name_cstr, type, match_retry); total_cancelled += heap_cancelled; this->to_remove_ += heap_cancelled; // Track removals for heap items } // Cancel items in to_add_ - total_cancelled += this->mark_matching_items_removed_(this->to_add_, component, name_cstr, type, match_retry); + total_cancelled += this->mark_matching_items_removed_locked_(this->to_add_, component, name_cstr, type, match_retry); return total_cancelled > 0; } diff --git a/esphome/core/scheduler.h b/esphome/core/scheduler.h index fd16840240..bea1503df0 100644 --- a/esphome/core/scheduler.h +++ b/esphome/core/scheduler.h @@ -243,8 +243,18 @@ class Scheduler { } // Helper function to check if item matches criteria for cancellation - inline bool HOT matches_item_(const std::unique_ptr &item, Component *component, const char *name_cstr, - SchedulerItem::Type type, bool match_retry, bool skip_removed = true) const { + // IMPORTANT: Must be called with scheduler lock held + inline bool HOT matches_item_locked_(const std::unique_ptr &item, Component *component, + const char *name_cstr, SchedulerItem::Type type, bool match_retry, + bool skip_removed = true) const { + // THREAD SAFETY: Check for nullptr first to prevent LoadProhibited crashes. On multi-threaded + // platforms, items can be moved out of defer_queue_ during processing, leaving nullptr entries. + // PR #11305 added nullptr checks in callers (mark_matching_items_removed_locked_() and + // has_cancelled_timeout_in_container_locked_()), but this check provides defense-in-depth: helper + // functions should be safe regardless of caller behavior. + // Fixes: https://github.com/esphome/esphome/issues/11940 + if (!item) + return false; if (item->component != component || item->type != type || (skip_removed && item->remove) || (match_retry && !item->is_retry)) { return false; @@ -304,8 +314,8 @@ class Scheduler { // SAFETY: Moving out the unique_ptr leaves a nullptr in the vector at defer_queue_front_. // This is intentional and safe because: // 1. The vector is only cleaned up by cleanup_defer_queue_locked_() at the end of this function - // 2. Any code iterating defer_queue_ MUST check for nullptr items (see mark_matching_items_removed_ - // and has_cancelled_timeout_in_container_ in scheduler.h) + // 2. Any code iterating defer_queue_ MUST check for nullptr items (see mark_matching_items_removed_locked_ + // and has_cancelled_timeout_in_container_locked_ in scheduler.h) // 3. The lock protects concurrent access, but the nullptr remains until cleanup item = std::move(this->defer_queue_[this->defer_queue_front_]); this->defer_queue_front_++; @@ -393,10 +403,10 @@ class Scheduler { // Helper to mark matching items in a container as removed // Returns the number of items marked for removal - // IMPORTANT: Caller must hold the scheduler lock before calling this function. + // IMPORTANT: Must be called with scheduler lock held template - size_t mark_matching_items_removed_(Container &container, Component *component, const char *name_cstr, - SchedulerItem::Type type, bool match_retry) { + size_t mark_matching_items_removed_locked_(Container &container, Component *component, const char *name_cstr, + SchedulerItem::Type type, bool match_retry) { size_t count = 0; for (auto &item : container) { // Skip nullptr items (can happen in defer_queue_ when items are being processed) @@ -405,7 +415,7 @@ class Scheduler { // the vector can still contain nullptr items from the processing loop. This check prevents crashes. if (!item) continue; - if (this->matches_item_(item, component, name_cstr, type, match_retry)) { + if (this->matches_item_locked_(item, component, name_cstr, type, match_retry)) { // Mark item for removal (platform-specific) this->set_item_removed_(item.get(), true); count++; @@ -415,9 +425,10 @@ class Scheduler { } // Template helper to check if any item in a container matches our criteria + // IMPORTANT: Must be called with scheduler lock held template - bool has_cancelled_timeout_in_container_(const Container &container, Component *component, const char *name_cstr, - bool match_retry) const { + bool has_cancelled_timeout_in_container_locked_(const Container &container, Component *component, + const char *name_cstr, bool match_retry) const { for (const auto &item : container) { // Skip nullptr items (can happen in defer_queue_ when items are being processed) // The defer_queue_ uses index-based processing: items are std::moved out but left in the @@ -426,8 +437,8 @@ class Scheduler { if (!item) continue; if (is_item_removed_(item.get()) && - this->matches_item_(item, component, name_cstr, SchedulerItem::TIMEOUT, match_retry, - /* skip_removed= */ false)) { + this->matches_item_locked_(item, component, name_cstr, SchedulerItem::TIMEOUT, match_retry, + /* skip_removed= */ false)) { return true; } } From f18bc626909d68efde5156f59f85a817bc866971 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Tue, 18 Nov 2025 13:29:40 -0500 Subject: [PATCH 128/415] [sfa30] Fix negative temperature values (#11973) --- esphome/components/sfa30/sfa30.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/esphome/components/sfa30/sfa30.cpp b/esphome/components/sfa30/sfa30.cpp index 99709d5fbb..bbe3bcd7d2 100644 --- a/esphome/components/sfa30/sfa30.cpp +++ b/esphome/components/sfa30/sfa30.cpp @@ -73,17 +73,17 @@ void SFA30Component::update() { } if (this->formaldehyde_sensor_ != nullptr) { - const float formaldehyde = raw_data[0] / 5.0f; + const float formaldehyde = static_cast(raw_data[0]) / 5.0f; this->formaldehyde_sensor_->publish_state(formaldehyde); } if (this->humidity_sensor_ != nullptr) { - const float humidity = raw_data[1] / 100.0f; + const float humidity = static_cast(raw_data[1]) / 100.0f; this->humidity_sensor_->publish_state(humidity); } if (this->temperature_sensor_ != nullptr) { - const float temperature = raw_data[2] / 200.0f; + const float temperature = static_cast(raw_data[2]) / 200.0f; this->temperature_sensor_->publish_state(temperature); } From f436f6ee2e3de361cdb5441aa0accf60986abb69 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 18 Nov 2025 13:17:21 -0600 Subject: [PATCH 129/415] [wifi] Fix captive portal unusable when WiFi credentials are wrong (#11965) --- esphome/components/captive_portal/__init__.py | 10 +++ .../captive_portal/captive_portal.cpp | 10 ++- .../captive_portal/captive_portal.h | 4 ++ esphome/components/esp32_improv/__init__.py | 6 +- .../esp32_improv/esp32_improv_component.cpp | 1 + .../esp32_improv/esp32_improv_component.h | 1 + .../web_server_idf/web_server_idf.cpp | 15 +++++ .../web_server_idf/web_server_idf.h | 4 ++ esphome/components/wifi/__init__.py | 8 ++- esphome/components/wifi/wifi_component.cpp | 62 +++++++++++++++---- esphome/components/wifi/wifi_component.h | 5 ++ 11 files changed, 111 insertions(+), 15 deletions(-) diff --git a/esphome/components/captive_portal/__init__.py b/esphome/components/captive_portal/__init__.py index 9bd3ef8a05..25d0a22083 100644 --- a/esphome/components/captive_portal/__init__.py +++ b/esphome/components/captive_portal/__init__.py @@ -72,6 +72,16 @@ def _final_validate(config: ConfigType) -> ConfigType: "Add 'ap:' to your WiFi configuration to enable the captive portal." ) + # Register socket needs for DNS server and additional HTTP connections + # - 1 UDP socket for DNS server + # - 3 additional TCP sockets for captive portal detection probes + configuration requests + # OS captive portal detection makes multiple probe requests that stay in TIME_WAIT. + # Need headroom for actual user configuration requests. + # LRU purging will reclaim idle sockets to prevent exhaustion from repeated attempts. + from esphome.components import socket + + socket.consume_sockets(4, "captive_portal")(config) + return config diff --git a/esphome/components/captive_portal/captive_portal.cpp b/esphome/components/captive_portal/captive_portal.cpp index 30438747f2..459ac557c8 100644 --- a/esphome/components/captive_portal/captive_portal.cpp +++ b/esphome/components/captive_portal/captive_portal.cpp @@ -50,8 +50,8 @@ void CaptivePortal::handle_wifisave(AsyncWebServerRequest *request) { ESP_LOGI(TAG, "Requested WiFi Settings Change:"); ESP_LOGI(TAG, " SSID='%s'", ssid.c_str()); ESP_LOGI(TAG, " Password=" LOG_SECRET("'%s'"), psk.c_str()); - wifi::global_wifi_component->save_wifi_sta(ssid, psk); - wifi::global_wifi_component->start_scanning(); + // Defer save to main loop thread to avoid NVS operations from HTTP thread + this->defer([ssid, psk]() { wifi::global_wifi_component->save_wifi_sta(ssid, psk); }); request->redirect(ESPHOME_F("/?save")); } @@ -63,6 +63,12 @@ void CaptivePortal::start() { this->base_->init(); if (!this->initialized_) { this->base_->add_handler(this); +#ifdef USE_ESP32 + // Enable LRU socket purging to handle captive portal detection probe bursts + // OS captive portal detection makes many simultaneous HTTP requests which can + // exhaust sockets. LRU purging automatically closes oldest idle connections. + this->base_->get_server()->set_lru_purge_enable(true); +#endif } network::IPAddress ip = wifi::global_wifi_component->wifi_soft_ap_ip(); diff --git a/esphome/components/captive_portal/captive_portal.h b/esphome/components/captive_portal/captive_portal.h index f48c286f0c..ae9b9dfba0 100644 --- a/esphome/components/captive_portal/captive_portal.h +++ b/esphome/components/captive_portal/captive_portal.h @@ -40,6 +40,10 @@ class CaptivePortal : public AsyncWebHandler, public Component { void end() { this->active_ = false; this->disable_loop(); // Stop processing DNS requests +#ifdef USE_ESP32 + // Disable LRU socket purging now that captive portal is done + this->base_->get_server()->set_lru_purge_enable(false); +#endif this->base_->deinit(); if (this->dns_server_ != nullptr) { this->dns_server_->stop(); diff --git a/esphome/components/esp32_improv/__init__.py b/esphome/components/esp32_improv/__init__.py index 1a7194da81..2e69d400ca 100644 --- a/esphome/components/esp32_improv/__init__.py +++ b/esphome/components/esp32_improv/__init__.py @@ -20,6 +20,10 @@ CONF_ON_STOP = "on_stop" CONF_STATUS_INDICATOR = "status_indicator" CONF_WIFI_TIMEOUT = "wifi_timeout" +# Default WiFi timeout - aligned with WiFi component ap_timeout +# Allows sufficient time to try all BSSIDs before starting provisioning mode +DEFAULT_WIFI_TIMEOUT = "90s" + improv_ns = cg.esphome_ns.namespace("improv") Error = improv_ns.enum("Error") @@ -59,7 +63,7 @@ CONFIG_SCHEMA = ( CONF_AUTHORIZED_DURATION, default="1min" ): cv.positive_time_period_milliseconds, cv.Optional( - CONF_WIFI_TIMEOUT, default="1min" + CONF_WIFI_TIMEOUT, default=DEFAULT_WIFI_TIMEOUT ): cv.positive_time_period_milliseconds, cv.Optional(CONF_ON_PROVISIONED): automation.validate_automation( { diff --git a/esphome/components/esp32_improv/esp32_improv_component.cpp b/esphome/components/esp32_improv/esp32_improv_component.cpp index 398b1d4251..0ad54bbb15 100644 --- a/esphome/components/esp32_improv/esp32_improv_component.cpp +++ b/esphome/components/esp32_improv/esp32_improv_component.cpp @@ -127,6 +127,7 @@ void ESP32ImprovComponent::loop() { // Set initial state based on whether we have an authorizer this->set_state_(this->get_initial_state_(), false); this->set_error_(improv::ERROR_NONE); + this->should_start_ = false; // Clear flag after starting ESP_LOGD(TAG, "Service started!"); } } diff --git a/esphome/components/esp32_improv/esp32_improv_component.h b/esphome/components/esp32_improv/esp32_improv_component.h index 989552ea56..8f4cfd7958 100644 --- a/esphome/components/esp32_improv/esp32_improv_component.h +++ b/esphome/components/esp32_improv/esp32_improv_component.h @@ -45,6 +45,7 @@ class ESP32ImprovComponent : public Component, public improv_base::ImprovBase { void start(); void stop(); bool is_active() const { return this->state_ != improv::STATE_STOPPED; } + bool should_start() const { return this->should_start_; } #ifdef USE_ESP32_IMPROV_STATE_CALLBACK void add_on_state_callback(std::function &&callback) { diff --git a/esphome/components/web_server_idf/web_server_idf.cpp b/esphome/components/web_server_idf/web_server_idf.cpp index ce91569de2..f5a66f6bd9 100644 --- a/esphome/components/web_server_idf/web_server_idf.cpp +++ b/esphome/components/web_server_idf/web_server_idf.cpp @@ -94,6 +94,18 @@ void AsyncWebServer::end() { } } +void AsyncWebServer::set_lru_purge_enable(bool enable) { + if (this->lru_purge_enable_ == enable) { + return; // No change needed + } + this->lru_purge_enable_ = enable; + // If server is already running, restart it with new config + if (this->server_) { + this->end(); + this->begin(); + } +} + void AsyncWebServer::begin() { if (this->server_) { this->end(); @@ -101,6 +113,8 @@ void AsyncWebServer::begin() { httpd_config_t config = HTTPD_DEFAULT_CONFIG(); config.server_port = this->port_; config.uri_match_fn = [](const char * /*unused*/, const char * /*unused*/, size_t /*unused*/) { return true; }; + // Enable LRU purging if requested (e.g., by captive portal to handle probe bursts) + config.lru_purge_enable = this->lru_purge_enable_; if (httpd_start(&this->server_, &config) == ESP_OK) { const httpd_uri_t handler_get = { .uri = "", @@ -242,6 +256,7 @@ void AsyncWebServerRequest::send(int code, const char *content_type, const char void AsyncWebServerRequest::redirect(const std::string &url) { httpd_resp_set_status(*this, "302 Found"); httpd_resp_set_hdr(*this, "Location", url.c_str()); + httpd_resp_set_hdr(*this, "Connection", "close"); httpd_resp_send(*this, nullptr, 0); } diff --git a/esphome/components/web_server_idf/web_server_idf.h b/esphome/components/web_server_idf/web_server_idf.h index 5ec6fec009..b9f690b462 100644 --- a/esphome/components/web_server_idf/web_server_idf.h +++ b/esphome/components/web_server_idf/web_server_idf.h @@ -199,9 +199,13 @@ class AsyncWebServer { return *handler; } + void set_lru_purge_enable(bool enable); + httpd_handle_t get_server() { return this->server_; } + protected: uint16_t port_{}; httpd_handle_t server_{}; + bool lru_purge_enable_{false}; static esp_err_t request_handler(httpd_req_t *r); static esp_err_t request_post_handler(httpd_req_t *r); esp_err_t request_handler_(AsyncWebServerRequest *request) const; diff --git a/esphome/components/wifi/__init__.py b/esphome/components/wifi/__init__.py index 11bd7798e2..5b3b30e0e9 100644 --- a/esphome/components/wifi/__init__.py +++ b/esphome/components/wifi/__init__.py @@ -69,6 +69,12 @@ CONF_MIN_AUTH_MODE = "min_auth_mode" # Limited to 127 because selected_sta_index_ is int8_t in C++ MAX_WIFI_NETWORKS = 127 +# Default AP timeout - allows sufficient time to try all BSSIDs during initial connection +# After AP starts, WiFi scanning is skipped to avoid disrupting the AP, so we only +# get best-effort connection attempts. Longer timeout ensures we exhaust all options +# before falling back to AP mode. Aligned with improv wifi_timeout default. +DEFAULT_AP_TIMEOUT = "90s" + wifi_ns = cg.esphome_ns.namespace("wifi") EAPAuth = wifi_ns.struct("EAPAuth") ManualIP = wifi_ns.struct("ManualIP") @@ -177,7 +183,7 @@ CONF_AP_TIMEOUT = "ap_timeout" WIFI_NETWORK_AP = WIFI_NETWORK_BASE.extend( { cv.Optional( - CONF_AP_TIMEOUT, default="1min" + CONF_AP_TIMEOUT, default=DEFAULT_AP_TIMEOUT ): cv.positive_time_period_milliseconds, } ) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index 880928c3e3..e31d7bbf32 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -199,7 +199,12 @@ 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 uint32_t WIFI_COOLDOWN_DURATION_MS = 500; + +/// Cooldown duration when fallback AP is active and captive portal may be running +/// Longer interval gives users time to configure WiFi without constant connection attempts +/// While connecting, WiFi can't beacon the AP properly, so needs longer cooldown +static constexpr uint32_t WIFI_COOLDOWN_WITH_AP_ACTIVE_MS = 30000; static constexpr uint8_t get_max_retries_for_phase(WiFiRetryPhase phase) { switch (phase) { @@ -275,7 +280,9 @@ int8_t WiFiComponent::find_next_hidden_sta_(int8_t start_index) { } } - if (!this->ssid_was_seen_in_scan_(sta.get_ssid())) { + // If we didn't scan this cycle, treat all networks as potentially hidden + // Otherwise, only retry networks that weren't seen in the scan + if (!this->did_scan_this_cycle_ || !this->ssid_was_seen_in_scan_(sta.get_ssid())) { ESP_LOGD(TAG, "Hidden candidate " LOG_SECRET("'%s'") " at index %d", sta.get_ssid().c_str(), static_cast(i)); return static_cast(i); } @@ -417,10 +424,6 @@ void WiFiComponent::start() { void WiFiComponent::restart_adapter() { ESP_LOGW(TAG, "Restarting adapter"); this->wifi_mode_(false, {}); - // 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; } @@ -441,7 +444,16 @@ void WiFiComponent::loop() { switch (this->state_) { case WIFI_COMPONENT_STATE_COOLDOWN: { this->status_set_warning(LOG_STR("waiting to reconnect")); - if (now - this->action_started_ > WIFI_COOLDOWN_DURATION_MS) { + // Skip cooldown if new credentials were provided while connecting + if (this->skip_cooldown_next_cycle_) { + this->skip_cooldown_next_cycle_ = false; + this->check_connecting_finished(); + break; + } + // Use longer cooldown when captive portal/improv is active to avoid disrupting user config + bool portal_active = this->is_captive_portal_active_() || this->is_esp32_improv_active_(); + uint32_t cooldown_duration = portal_active ? WIFI_COOLDOWN_WITH_AP_ACTIVE_MS : WIFI_COOLDOWN_DURATION_MS; + if (now - this->action_started_ > cooldown_duration) { // 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 @@ -495,7 +507,8 @@ void WiFiComponent::loop() { #endif // USE_WIFI_AP #ifdef USE_IMPROV - if (esp32_improv::global_improv_component != nullptr && !esp32_improv::global_improv_component->is_active()) { + if (esp32_improv::global_improv_component != nullptr && !esp32_improv::global_improv_component->is_active() && + !esp32_improv::global_improv_component->should_start()) { if (now - this->last_connected_ > esp32_improv::global_improv_component->get_wifi_timeout()) { if (this->wifi_mode_(true, {})) esp32_improv::global_improv_component->start(); @@ -605,6 +618,8 @@ void WiFiComponent::set_sta(const WiFiAP &ap) { this->init_sta(1); this->add_sta(ap); this->selected_sta_index_ = 0; + // When new credentials are set (e.g., from improv), skip cooldown to retry immediately + this->skip_cooldown_next_cycle_ = true; } WiFiAP WiFiComponent::build_params_for_current_phase_() { @@ -666,6 +681,17 @@ void WiFiComponent::save_wifi_sta(const std::string &ssid, const std::string &pa sta.set_ssid(ssid); sta.set_password(password); this->set_sta(sta); + + // Trigger connection attempt (exits cooldown if needed, no-op if already connecting/connected) + this->connect_soon_(); +} + +void WiFiComponent::connect_soon_() { + // Only trigger retry if we're in cooldown - if already connecting/connected, do nothing + if (this->state_ == WIFI_COMPONENT_STATE_COOLDOWN) { + ESP_LOGD(TAG, "Exiting cooldown early due to new WiFi credentials"); + this->retry_connect(); + } } void WiFiComponent::start_connecting(const WiFiAP &ap) { @@ -961,6 +987,7 @@ void WiFiComponent::check_scanning_finished() { return; } this->scan_done_ = false; + this->did_scan_this_cycle_ = true; if (this->scan_result_.empty()) { ESP_LOGW(TAG, "No networks found"); @@ -1227,9 +1254,16 @@ WiFiRetryPhase WiFiComponent::determine_next_phase_() { return WiFiRetryPhase::RESTARTING_ADAPTER; case WiFiRetryPhase::RESTARTING_ADAPTER: - // After restart, 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; + // After restart, go back to explicit hidden if we went through it initially + if (this->went_through_explicit_hidden_phase_()) { + return WiFiRetryPhase::EXPLICIT_HIDDEN; + } + // Skip scanning when captive portal/improv is active to avoid disrupting AP + // Even passive scans can cause brief AP disconnections on ESP32 + if (this->is_captive_portal_active_() || this->is_esp32_improv_active_()) { + return WiFiRetryPhase::RETRY_HIDDEN; + } + return WiFiRetryPhase::SCAN_CONNECTING; } // Should never reach here @@ -1317,6 +1351,12 @@ bool WiFiComponent::transition_to_phase_(WiFiRetryPhase new_phase) { if (!this->is_captive_portal_active_() && !this->is_esp32_improv_active_()) { this->restart_adapter(); } + // Clear scan flag - we're starting a new retry cycle + this->did_scan_this_cycle_ = false; + // Always enter cooldown after restart (or skip-restart) to allow stabilization + // Use extended cooldown when AP is active to avoid constant scanning that blocks DNS + this->state_ = WIFI_COMPONENT_STATE_COOLDOWN; + this->action_started_ = millis(); // Return true to indicate we should wait (go to COOLDOWN) instead of immediately connecting return true; diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index 5023cf3428..2e0a9816c6 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -291,6 +291,7 @@ class WiFiComponent : public Component { void set_passive_scan(bool passive); void save_wifi_sta(const std::string &ssid, const std::string &password); + // ========== INTERNAL METHODS ========== // (In most use cases you won't need these) /// Setup WiFi interface. @@ -424,6 +425,8 @@ class WiFiComponent : public Component { return true; } + void connect_soon_(); + void wifi_loop_(); bool wifi_mode_(optional sta, optional ap); bool wifi_sta_pre_setup_(); @@ -529,6 +532,8 @@ class WiFiComponent : public Component { bool enable_on_boot_; bool got_ipv4_address_{false}; bool keep_scan_results_{false}; + bool did_scan_this_cycle_{false}; + bool skip_cooldown_next_cycle_{false}; // Pointers at the end (naturally aligned) Trigger<> *connect_trigger_{new Trigger<>()}; From 2681a14d05cbc1df1f434b2d5270b5b326e54140 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 19 Nov 2025 09:17:33 +1300 Subject: [PATCH 130/415] Bump version to 2025.11.0b4 --- Doxyfile | 2 +- esphome/const.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Doxyfile b/Doxyfile index 04046d9ce6..c7b2187964 100644 --- a/Doxyfile +++ b/Doxyfile @@ -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.0b3 +PROJECT_NUMBER = 2025.11.0b4 # 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 diff --git a/esphome/const.py b/esphome/const.py index 8360531bff..7a3a79f270 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -4,7 +4,7 @@ from enum import Enum from esphome.enum import StrEnum -__version__ = "2025.11.0b3" +__version__ = "2025.11.0b4" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" VALID_SUBSTITUTIONS_CHARACTERS = ( From 66e471cf2ac3dcecf060695f77b28ba78163d066 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 18 Nov 2025 14:33:07 -0600 Subject: [PATCH 131/415] [mdns] Modernize to C++17 nested namespace syntax --- esphome/components/mdns/mdns_component.cpp | 6 ++---- esphome/components/mdns/mdns_component.h | 6 ++---- esphome/components/mdns/mdns_esp32.cpp | 6 ++---- esphome/components/mdns/mdns_esp8266.cpp | 6 ++---- esphome/components/mdns/mdns_host.cpp | 6 ++---- esphome/components/mdns/mdns_libretiny.cpp | 6 ++---- esphome/components/mdns/mdns_rp2040.cpp | 6 ++---- 7 files changed, 14 insertions(+), 28 deletions(-) diff --git a/esphome/components/mdns/mdns_component.cpp b/esphome/components/mdns/mdns_component.cpp index b66129404e..c81defd19f 100644 --- a/esphome/components/mdns/mdns_component.cpp +++ b/esphome/components/mdns/mdns_component.cpp @@ -21,8 +21,7 @@ #include "esphome/components/dashboard_import/dashboard_import.h" #endif -namespace esphome { -namespace mdns { +namespace esphome::mdns { static const char *const TAG = "mdns"; @@ -189,6 +188,5 @@ void MDNSComponent::dump_config() { #endif } -} // namespace mdns -} // namespace esphome +} // namespace esphome::mdns #endif diff --git a/esphome/components/mdns/mdns_component.h b/esphome/components/mdns/mdns_component.h index f4237d5a69..691c45b7df 100644 --- a/esphome/components/mdns/mdns_component.h +++ b/esphome/components/mdns/mdns_component.h @@ -6,8 +6,7 @@ #include "esphome/core/component.h" #include "esphome/core/helpers.h" -namespace esphome { -namespace mdns { +namespace esphome::mdns { // Helper struct that identifies strings that may be stored in flash storage (similar to LogString) struct MDNSString; @@ -79,6 +78,5 @@ class MDNSComponent : public Component { void compile_records_(StaticVector &services); }; -} // namespace mdns -} // namespace esphome +} // namespace esphome::mdns #endif diff --git a/esphome/components/mdns/mdns_esp32.cpp b/esphome/components/mdns/mdns_esp32.cpp index ecdc926cc9..5547a2524b 100644 --- a/esphome/components/mdns/mdns_esp32.cpp +++ b/esphome/components/mdns/mdns_esp32.cpp @@ -7,8 +7,7 @@ #include "esphome/core/log.h" #include "mdns_component.h" -namespace esphome { -namespace mdns { +namespace esphome::mdns { static const char *const TAG = "mdns"; @@ -56,7 +55,6 @@ void MDNSComponent::on_shutdown() { delay(40); // Allow the mdns packets announcing service removal to be sent } -} // namespace mdns -} // namespace esphome +} // namespace esphome::mdns #endif // USE_ESP32 diff --git a/esphome/components/mdns/mdns_esp8266.cpp b/esphome/components/mdns/mdns_esp8266.cpp index 9bbb406070..06f905884c 100644 --- a/esphome/components/mdns/mdns_esp8266.cpp +++ b/esphome/components/mdns/mdns_esp8266.cpp @@ -9,8 +9,7 @@ #include "esphome/core/log.h" #include "mdns_component.h" -namespace esphome { -namespace mdns { +namespace esphome::mdns { void MDNSComponent::setup() { #ifdef USE_MDNS_STORE_SERVICES @@ -52,7 +51,6 @@ void MDNSComponent::on_shutdown() { delay(10); } -} // namespace mdns -} // namespace esphome +} // namespace esphome::mdns #endif diff --git a/esphome/components/mdns/mdns_host.cpp b/esphome/components/mdns/mdns_host.cpp index f645d8d068..64b8c8f54b 100644 --- a/esphome/components/mdns/mdns_host.cpp +++ b/esphome/components/mdns/mdns_host.cpp @@ -6,8 +6,7 @@ #include "esphome/core/log.h" #include "mdns_component.h" -namespace esphome { -namespace mdns { +namespace esphome::mdns { void MDNSComponent::setup() { // Host platform doesn't have actual mDNS implementation @@ -15,7 +14,6 @@ void MDNSComponent::setup() { void MDNSComponent::on_shutdown() {} -} // namespace mdns -} // namespace esphome +} // namespace esphome::mdns #endif diff --git a/esphome/components/mdns/mdns_libretiny.cpp b/esphome/components/mdns/mdns_libretiny.cpp index fb2088f719..a049fe2109 100644 --- a/esphome/components/mdns/mdns_libretiny.cpp +++ b/esphome/components/mdns/mdns_libretiny.cpp @@ -9,8 +9,7 @@ #include -namespace esphome { -namespace mdns { +namespace esphome::mdns { void MDNSComponent::setup() { #ifdef USE_MDNS_STORE_SERVICES @@ -46,7 +45,6 @@ void MDNSComponent::setup() { void MDNSComponent::on_shutdown() {} -} // namespace mdns -} // namespace esphome +} // namespace esphome::mdns #endif diff --git a/esphome/components/mdns/mdns_rp2040.cpp b/esphome/components/mdns/mdns_rp2040.cpp index a9f5349f14..a102e0b6c3 100644 --- a/esphome/components/mdns/mdns_rp2040.cpp +++ b/esphome/components/mdns/mdns_rp2040.cpp @@ -9,8 +9,7 @@ #include -namespace esphome { -namespace mdns { +namespace esphome::mdns { void MDNSComponent::setup() { #ifdef USE_MDNS_STORE_SERVICES @@ -51,7 +50,6 @@ void MDNSComponent::on_shutdown() { delay(40); } -} // namespace mdns -} // namespace esphome +} // namespace esphome::mdns #endif From 99acc62c3bbe1fda788cf321df098c28969d4fce Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 18 Nov 2025 14:50:28 -0600 Subject: [PATCH 132/415] [lock] Modernize to C++17 nested namespaces --- esphome/components/lock/automation.h | 6 ++---- esphome/components/lock/lock.cpp | 6 ++---- esphome/components/lock/lock.h | 6 ++---- 3 files changed, 6 insertions(+), 12 deletions(-) diff --git a/esphome/components/lock/automation.h b/esphome/components/lock/automation.h index 0f596ef5e6..cba2c3fdda 100644 --- a/esphome/components/lock/automation.h +++ b/esphome/components/lock/automation.h @@ -4,8 +4,7 @@ #include "esphome/core/automation.h" #include "esphome/core/component.h" -namespace esphome { -namespace lock { +namespace esphome::lock { template class LockAction : public Action { public: @@ -72,5 +71,4 @@ class LockUnlockTrigger : public Trigger<> { } }; -} // namespace lock -} // namespace esphome +} // namespace esphome::lock diff --git a/esphome/components/lock/lock.cpp b/esphome/components/lock/lock.cpp index 54fefe8745..b8f0fbe011 100644 --- a/esphome/components/lock/lock.cpp +++ b/esphome/components/lock/lock.cpp @@ -3,8 +3,7 @@ #include "esphome/core/controller_registry.h" #include "esphome/core/log.h" -namespace esphome { -namespace lock { +namespace esphome::lock { static const char *const TAG = "lock"; @@ -108,5 +107,4 @@ LockCall &LockCall::set_state(const std::string &state) { } const optional &LockCall::get_state() const { return this->state_; } -} // namespace lock -} // namespace esphome +} // namespace esphome::lock diff --git a/esphome/components/lock/lock.h b/esphome/components/lock/lock.h index 9737569921..8a906ef9fc 100644 --- a/esphome/components/lock/lock.h +++ b/esphome/components/lock/lock.h @@ -7,8 +7,7 @@ #include "esphome/core/preferences.h" #include -namespace esphome { -namespace lock { +namespace esphome::lock { class Lock; @@ -177,5 +176,4 @@ class Lock : public EntityBase { ESPPreferenceObject rtc_; }; -} // namespace lock -} // namespace esphome +} // namespace esphome::lock From b5ebe911504a9955aef9bcacebf676b044026690 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 18 Nov 2025 15:24:50 -0600 Subject: [PATCH 133/415] [api] Optimize APINoiseContext memory usage by removing shared_ptr overhead --- esphome/components/api/api_connection.cpp | 4 ++-- esphome/components/api/api_frame_helper_noise.cpp | 2 +- esphome/components/api/api_frame_helper_noise.h | 9 ++++----- esphome/components/api/api_server.cpp | 6 +++--- esphome/components/api/api_server.h | 6 +++--- esphome/components/mdns/mdns_component.cpp | 2 +- esphome/components/mqtt/mqtt_client.cpp | 2 +- 7 files changed, 15 insertions(+), 16 deletions(-) diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 4acd2fc15c..892cb2f9ce 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -90,8 +90,8 @@ static const int CAMERA_STOP_STREAM = 5000; APIConnection::APIConnection(std::unique_ptr sock, APIServer *parent) : parent_(parent), initial_state_iterator_(this), list_entities_iterator_(this) { #if defined(USE_API_PLAINTEXT) && defined(USE_API_NOISE) - auto noise_ctx = parent->get_noise_ctx(); - if (noise_ctx->has_psk()) { + auto &noise_ctx = parent->get_noise_ctx(); + if (noise_ctx.has_psk()) { this->helper_ = std::unique_ptr{new APINoiseFrameHelper(std::move(sock), noise_ctx, &this->client_info_)}; } else { diff --git a/esphome/components/api/api_frame_helper_noise.cpp b/esphome/components/api/api_frame_helper_noise.cpp index 633b07a7fa..4fff2f7a8c 100644 --- a/esphome/components/api/api_frame_helper_noise.cpp +++ b/esphome/components/api/api_frame_helper_noise.cpp @@ -527,7 +527,7 @@ APIError APINoiseFrameHelper::init_handshake_() { if (aerr != APIError::OK) return aerr; - const auto &psk = ctx_->get_psk(); + const auto &psk = this->ctx_.get_psk(); err = noise_handshakestate_set_pre_shared_key(handshake_, psk.data(), psk.size()); aerr = handle_noise_error_(err, LOG_STR("noise_handshakestate_set_pre_shared_key"), APIError::HANDSHAKESTATE_SETUP_FAILED); diff --git a/esphome/components/api/api_frame_helper_noise.h b/esphome/components/api/api_frame_helper_noise.h index e3243e4fa5..7eb01058db 100644 --- a/esphome/components/api/api_frame_helper_noise.h +++ b/esphome/components/api/api_frame_helper_noise.h @@ -9,9 +9,8 @@ namespace esphome::api { class APINoiseFrameHelper final : public APIFrameHelper { public: - APINoiseFrameHelper(std::unique_ptr socket, std::shared_ptr ctx, - const ClientInfo *client_info) - : APIFrameHelper(std::move(socket), client_info), ctx_(std::move(ctx)) { + APINoiseFrameHelper(std::unique_ptr socket, APINoiseContext &ctx, const ClientInfo *client_info) + : APIFrameHelper(std::move(socket), client_info), ctx_(ctx) { // Noise header structure: // Pos 0: indicator (0x01) // Pos 1-2: encrypted payload size (16-bit big-endian) @@ -41,8 +40,8 @@ class APINoiseFrameHelper final : public APIFrameHelper { NoiseCipherState *send_cipher_{nullptr}; NoiseCipherState *recv_cipher_{nullptr}; - // Shared pointer (8 bytes on 32-bit = 4 bytes control block pointer + 4 bytes object pointer) - std::shared_ptr ctx_; + // Reference to noise context (4 bytes on 32-bit) + APINoiseContext &ctx_; // Vector (12 bytes on 32-bit) std::vector prologue_; diff --git a/esphome/components/api/api_server.cpp b/esphome/components/api/api_server.cpp index 18601d74ff..ebb6387427 100644 --- a/esphome/components/api/api_server.cpp +++ b/esphome/components/api/api_server.cpp @@ -227,8 +227,8 @@ void APIServer::dump_config() { " Max connections: %u", network::get_use_address(), this->port_, this->listen_backlog_, this->max_connections_); #ifdef USE_API_NOISE - ESP_LOGCONFIG(TAG, " Noise encryption: %s", YESNO(this->noise_ctx_->has_psk())); - if (!this->noise_ctx_->has_psk()) { + ESP_LOGCONFIG(TAG, " Noise encryption: %s", YESNO(this->noise_ctx_.has_psk())); + if (!this->noise_ctx_.has_psk()) { ESP_LOGCONFIG(TAG, " Supports encryption: YES"); } #else @@ -493,7 +493,7 @@ bool APIServer::save_noise_psk(psk_t psk, bool make_active) { ESP_LOGW(TAG, "Key set in YAML"); return false; #else - auto &old_psk = this->noise_ctx_->get_psk(); + auto &old_psk = this->noise_ctx_.get_psk(); if (std::equal(old_psk.begin(), old_psk.end(), psk.begin())) { ESP_LOGW(TAG, "New PSK matches old"); return true; diff --git a/esphome/components/api/api_server.h b/esphome/components/api/api_server.h index 2d58063d6c..02ae0c5fe1 100644 --- a/esphome/components/api/api_server.h +++ b/esphome/components/api/api_server.h @@ -54,8 +54,8 @@ class APIServer : public Component, public Controller { #ifdef USE_API_NOISE bool save_noise_psk(psk_t psk, bool make_active = true); bool clear_noise_psk(bool make_active = true); - void set_noise_psk(psk_t psk) { noise_ctx_->set_psk(psk); } - std::shared_ptr get_noise_ctx() { return noise_ctx_; } + void set_noise_psk(psk_t psk) { this->noise_ctx_.set_psk(psk); } + APINoiseContext &get_noise_ctx() { return this->noise_ctx_; } #endif // USE_API_NOISE void handle_disconnect(APIConnection *conn); @@ -228,7 +228,7 @@ class APIServer : public Component, public Controller { // 7 bytes used, 1 byte padding #ifdef USE_API_NOISE - std::shared_ptr noise_ctx_ = std::make_shared(); + APINoiseContext noise_ctx_; ESPPreferenceObject noise_pref_; #endif // USE_API_NOISE }; diff --git a/esphome/components/mdns/mdns_component.cpp b/esphome/components/mdns/mdns_component.cpp index b66129404e..19bc2cb4e2 100644 --- a/esphome/components/mdns/mdns_component.cpp +++ b/esphome/components/mdns/mdns_component.cpp @@ -119,7 +119,7 @@ void MDNSComponent::compile_records_(StaticVectorget_noise_ctx()->has_psk(); + bool has_psk = api::global_api_server->get_noise_ctx().has_psk(); const char *encryption_key = has_psk ? TXT_API_ENCRYPTION : TXT_API_ENCRYPTION_SUPPORTED; txt_records.push_back({MDNS_STR(encryption_key), MDNS_STR(NOISE_ENCRYPTION)}); #endif diff --git a/esphome/components/mqtt/mqtt_client.cpp b/esphome/components/mqtt/mqtt_client.cpp index 9055b4421e..a810d98adf 100644 --- a/esphome/components/mqtt/mqtt_client.cpp +++ b/esphome/components/mqtt/mqtt_client.cpp @@ -140,7 +140,7 @@ void MQTTClientComponent::send_device_info_() { #endif #ifdef USE_API_NOISE - root[api::global_api_server->get_noise_ctx()->has_psk() ? "api_encryption" : "api_encryption_supported"] = + root[api::global_api_server->get_noise_ctx().has_psk() ? "api_encryption" : "api_encryption_supported"] = "Noise_NNpsk0_25519_ChaChaPoly_SHA256"; #endif }, From 5c2cf9f37c85ce46a8a3d09d328a4641e6ebbf17 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 18 Nov 2025 16:10:07 -0600 Subject: [PATCH 134/415] [web_server_base] Replace shared_ptr with unique_ptr for AsyncWebServer --- esphome/components/web_server_base/web_server_base.h | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/esphome/components/web_server_base/web_server_base.h b/esphome/components/web_server_base/web_server_base.h index 039a452d64..fbf0d00c06 100644 --- a/esphome/components/web_server_base/web_server_base.h +++ b/esphome/components/web_server_base/web_server_base.h @@ -111,7 +111,7 @@ class WebServerBase : public Component { this->initialized_++; return; } - this->server_ = std::make_shared(this->port_); + this->server_ = std::make_unique(this->port_); // All content is controlled and created by user - so allowing all origins is fine here. DefaultHeaders::Instance().addHeader("Access-Control-Allow-Origin", "*"); this->server_->begin(); @@ -127,7 +127,7 @@ class WebServerBase : public Component { this->server_ = nullptr; } } - std::shared_ptr get_server() const { return server_; } + AsyncWebServer *get_server() const { return this->server_.get(); } float get_setup_priority() const override; #ifdef USE_WEBSERVER_AUTH @@ -143,7 +143,7 @@ class WebServerBase : public Component { protected: int initialized_{0}; uint16_t port_{80}; - std::shared_ptr server_{nullptr}; + std::unique_ptr server_{nullptr}; std::vector handlers_; #ifdef USE_WEBSERVER_AUTH internal::Credentials credentials_; From 29374837c68d8643c61b35803130b829eca84268 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 18 Nov 2025 17:06:34 -0600 Subject: [PATCH 135/415] [wifi, captive_portal, web_server, wifi_info] Use stack allocation for MAC address formatting (#11963) --- esphome/components/captive_portal/captive_portal.cpp | 6 ++++-- esphome/components/web_server/web_server.cpp | 4 ++-- esphome/components/wifi/wifi_component.cpp | 6 ++++-- esphome/components/wifi_info/wifi_info_text_sensor.h | 5 ++++- esphome/core/helpers.cpp | 12 +++++++++--- esphome/core/helpers.h | 5 +++++ 6 files changed, 28 insertions(+), 10 deletions(-) diff --git a/esphome/components/captive_portal/captive_portal.cpp b/esphome/components/captive_portal/captive_portal.cpp index 459ac557c8..4eb00835b1 100644 --- a/esphome/components/captive_portal/captive_portal.cpp +++ b/esphome/components/captive_portal/captive_portal.cpp @@ -13,14 +13,16 @@ static const char *const TAG = "captive_portal"; void CaptivePortal::handle_config(AsyncWebServerRequest *request) { AsyncResponseStream *stream = request->beginResponseStream(ESPHOME_F("application/json")); stream->addHeader(ESPHOME_F("cache-control"), ESPHOME_F("public, max-age=0, must-revalidate")); + char mac_s[18]; + const char *mac_str = get_mac_address_pretty_into_buffer(mac_s); #ifdef USE_ESP8266 stream->print(ESPHOME_F("{\"mac\":\"")); - stream->print(get_mac_address_pretty().c_str()); + stream->print(mac_str); stream->print(ESPHOME_F("\",\"name\":\"")); stream->print(App.get_name().c_str()); stream->print(ESPHOME_F("\",\"aps\":[{}")); #else - stream->printf(R"({"mac":"%s","name":"%s","aps":[{})", get_mac_address_pretty().c_str(), App.get_name().c_str()); + stream->printf(R"({"mac":"%s","name":"%s","aps":[{})", mac_str, App.get_name().c_str()); #endif for (auto &scan : wifi::global_wifi_component->get_scan_result()) { diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index 5a8128ba43..cc51463fe7 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -359,8 +359,8 @@ void WebServer::handle_pna_cors_request(AsyncWebServerRequest *request) { AsyncWebServerResponse *response = request->beginResponse(200, ""); response->addHeader(HEADER_CORS_ALLOW_PNA, "true"); response->addHeader(HEADER_PNA_NAME, App.get_name().c_str()); - std::string mac = get_mac_address_pretty(); - response->addHeader(HEADER_PNA_ID, mac.c_str()); + char mac_s[18]; + response->addHeader(HEADER_PNA_ID, get_mac_address_pretty_into_buffer(mac_s)); request->send(response); } #endif diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index 30340601fb..6f698bc2a8 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -341,10 +341,11 @@ void WiFiComponent::setup() { } void WiFiComponent::start() { + char mac_s[18]; ESP_LOGCONFIG(TAG, "Starting\n" " Local MAC: %s", - get_mac_address_pretty().c_str()); + get_mac_address_pretty_into_buffer(mac_s)); this->last_connected_ = millis(); uint32_t hash = this->has_sta() ? fnv1_hash(App.get_compilation_time()) : 88491487UL; @@ -826,7 +827,8 @@ void WiFiComponent::print_connect_params_() { char bssid_s[18]; format_mac_addr_upper(bssid.data(), bssid_s); - ESP_LOGCONFIG(TAG, " Local MAC: %s", get_mac_address_pretty().c_str()); + char mac_s[18]; + ESP_LOGCONFIG(TAG, " Local MAC: %s", get_mac_address_pretty_into_buffer(mac_s)); if (this->is_disabled()) { ESP_LOGCONFIG(TAG, " Disabled"); return; diff --git a/esphome/components/wifi_info/wifi_info_text_sensor.h b/esphome/components/wifi_info/wifi_info_text_sensor.h index 04889d6bb3..0814336c43 100644 --- a/esphome/components/wifi_info/wifi_info_text_sensor.h +++ b/esphome/components/wifi_info/wifi_info_text_sensor.h @@ -126,7 +126,10 @@ class BSSIDWiFiInfo : public PollingComponent, public text_sensor::TextSensor { class MacAddressWifiInfo : public Component, public text_sensor::TextSensor { public: - void setup() override { this->publish_state(get_mac_address_pretty()); } + void setup() override { + char mac_s[18]; + this->publish_state(get_mac_address_pretty_into_buffer(mac_s)); + } void dump_config() override; }; diff --git a/esphome/core/helpers.cpp b/esphome/core/helpers.cpp index 568acb9f1b..50af71649c 100644 --- a/esphome/core/helpers.cpp +++ b/esphome/core/helpers.cpp @@ -638,9 +638,8 @@ std::string get_mac_address() { } std::string get_mac_address_pretty() { - uint8_t mac[6]; - get_mac_address_raw(mac); - return format_mac_address_pretty(mac); + char buf[18]; + return std::string(get_mac_address_pretty_into_buffer(buf)); } void get_mac_address_into_buffer(std::span buf) { @@ -649,6 +648,13 @@ void get_mac_address_into_buffer(std::span buf) { format_mac_addr_lower_no_sep(mac, buf.data()); } +const char *get_mac_address_pretty_into_buffer(std::span buf) { + uint8_t mac[6]; + get_mac_address_raw(mac); + format_mac_addr_upper(mac, buf.data()); + return buf.data(); +} + #ifndef USE_ESP32 bool has_custom_mac_address() { return false; } #endif diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index 16eab8b8f6..d8c1f4647e 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -1052,6 +1052,11 @@ std::string get_mac_address_pretty(); /// Assumes buffer length is 13 (12 digits for hexadecimal representation followed by null terminator). void get_mac_address_into_buffer(std::span buf); +/// Get the device MAC address into the given buffer, in colon-separated uppercase hex notation. +/// Buffer must be exactly 18 bytes (17 for "XX:XX:XX:XX:XX:XX" + null terminator). +/// Returns pointer to the buffer for convenience. +const char *get_mac_address_pretty_into_buffer(std::span buf); + #ifdef USE_ESP32 /// Set the MAC address to use from the provided byte array (6 bytes). void set_mac_address(uint8_t *mac); From 88717ac1f57ae773dc77d71010cebc8f542c70e0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 18 Nov 2025 18:53:50 -0600 Subject: [PATCH 136/415] [api] Remove redundant socket pointer from APIFrameHelper --- esphome/components/api/api_frame_helper.h | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/esphome/components/api/api_frame_helper.h b/esphome/components/api/api_frame_helper.h index 9aaada3cf7..d931a6e3a9 100644 --- a/esphome/components/api/api_frame_helper.h +++ b/esphome/components/api/api_frame_helper.h @@ -84,9 +84,7 @@ class APIFrameHelper { public: APIFrameHelper() = default; explicit APIFrameHelper(std::unique_ptr socket, const ClientInfo *client_info) - : socket_owned_(std::move(socket)), client_info_(client_info) { - socket_ = socket_owned_.get(); - } + : socket_(std::move(socket)), client_info_(client_info) {} virtual ~APIFrameHelper() = default; virtual APIError init() = 0; virtual APIError loop(); @@ -149,9 +147,8 @@ class APIFrameHelper { APIError write_raw_(const struct iovec *iov, int iovcnt, socket::Socket *socket, std::vector &tx_buf, const std::string &info, StateEnum &state, StateEnum failed_state); - // Pointers first (4 bytes each) - socket::Socket *socket_{nullptr}; - std::unique_ptr socket_owned_; + // Socket ownership (4 bytes on 32-bit, 8 bytes on 64-bit) + std::unique_ptr socket_; // Common state enum for all frame helpers // Note: Not all states are used by all implementations From ecaa3f9f71a2bbcaa0879d04f02df25e9de471ee Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 18 Nov 2025 21:07:13 -0600 Subject: [PATCH 137/415] [light] Modernize namespace declarations to C++17 syntax --- esphome/components/light/addressable_light.cpp | 6 ++---- esphome/components/light/addressable_light.h | 6 ++---- esphome/components/light/addressable_light_effect.h | 6 ++---- esphome/components/light/addressable_light_wrapper.h | 6 ++---- esphome/components/light/automation.cpp | 6 ++---- esphome/components/light/automation.h | 6 ++---- esphome/components/light/base_light_effects.h | 6 ++---- esphome/components/light/color_mode.h | 6 ++---- esphome/components/light/esp_color_correction.cpp | 6 ++---- esphome/components/light/esp_color_correction.h | 6 ++---- esphome/components/light/esp_color_view.h | 6 ++---- esphome/components/light/esp_hsv_color.cpp | 6 ++---- esphome/components/light/esp_hsv_color.h | 6 ++---- esphome/components/light/esp_range_view.cpp | 6 ++---- esphome/components/light/esp_range_view.h | 6 ++---- esphome/components/light/light_call.cpp | 6 ++---- esphome/components/light/light_color_values.h | 6 ++---- esphome/components/light/light_effect.cpp | 6 ++---- esphome/components/light/light_effect.h | 6 ++---- esphome/components/light/light_json_schema.cpp | 6 ++---- esphome/components/light/light_json_schema.h | 6 ++---- esphome/components/light/light_output.cpp | 6 ++---- esphome/components/light/light_output.h | 6 ++---- esphome/components/light/light_state.cpp | 6 ++---- esphome/components/light/light_state.h | 6 ++---- esphome/components/light/light_transformer.h | 6 ++---- esphome/components/light/transformers.h | 6 ++---- 27 files changed, 54 insertions(+), 108 deletions(-) diff --git a/esphome/components/light/addressable_light.cpp b/esphome/components/light/addressable_light.cpp index 5cbdcb0e86..2f6ffc9a38 100644 --- a/esphome/components/light/addressable_light.cpp +++ b/esphome/components/light/addressable_light.cpp @@ -1,8 +1,7 @@ #include "addressable_light.h" #include "esphome/core/log.h" -namespace esphome { -namespace light { +namespace esphome::light { static const char *const TAG = "light.addressable"; @@ -112,5 +111,4 @@ optional AddressableLightTransformer::apply() { return {}; } -} // namespace light -} // namespace esphome +} // namespace esphome::light diff --git a/esphome/components/light/addressable_light.h b/esphome/components/light/addressable_light.h index 393cc679bc..2e4b984ce4 100644 --- a/esphome/components/light/addressable_light.h +++ b/esphome/components/light/addressable_light.h @@ -14,8 +14,7 @@ #include "esphome/components/power_supply/power_supply.h" #endif -namespace esphome { -namespace light { +namespace esphome::light { /// Convert the color information from a `LightColorValues` object to a `Color` object (does not apply brightness). Color color_from_light_color_values(LightColorValues val); @@ -116,5 +115,4 @@ class AddressableLightTransformer : public LightTransformer { Color target_color_{}; }; -} // namespace light -} // namespace esphome +} // namespace esphome::light diff --git a/esphome/components/light/addressable_light_effect.h b/esphome/components/light/addressable_light_effect.h index 0847db3770..a85ea4661d 100644 --- a/esphome/components/light/addressable_light_effect.h +++ b/esphome/components/light/addressable_light_effect.h @@ -7,8 +7,7 @@ #include "esphome/components/light/light_state.h" #include "esphome/components/light/addressable_light.h" -namespace esphome { -namespace light { +namespace esphome::light { inline static int16_t sin16_c(uint16_t theta) { static const uint16_t BASE[] = {0, 6393, 12539, 18204, 23170, 27245, 30273, 32137}; @@ -371,5 +370,4 @@ class AddressableFlickerEffect : public AddressableLightEffect { uint8_t intensity_{13}; }; -} // namespace light -} // namespace esphome +} // namespace esphome::light diff --git a/esphome/components/light/addressable_light_wrapper.h b/esphome/components/light/addressable_light_wrapper.h index d358502430..8665e62a79 100644 --- a/esphome/components/light/addressable_light_wrapper.h +++ b/esphome/components/light/addressable_light_wrapper.h @@ -3,8 +3,7 @@ #include "esphome/core/component.h" #include "addressable_light.h" -namespace esphome { -namespace light { +namespace esphome::light { class AddressableLightWrapper : public light::AddressableLight { public: @@ -123,5 +122,4 @@ class AddressableLightWrapper : public light::AddressableLight { ColorMode color_mode_{ColorMode::UNKNOWN}; }; -} // namespace light -} // namespace esphome +} // namespace esphome::light diff --git a/esphome/components/light/automation.cpp b/esphome/components/light/automation.cpp index 8c1785f061..ddac2f9341 100644 --- a/esphome/components/light/automation.cpp +++ b/esphome/components/light/automation.cpp @@ -1,8 +1,7 @@ #include "automation.h" #include "esphome/core/log.h" -namespace esphome { -namespace light { +namespace esphome::light { static const char *const TAG = "light.automation"; @@ -11,5 +10,4 @@ void addressableset_warn_about_scale(const char *field) { field); } -} // namespace light -} // namespace esphome +} // namespace esphome::light diff --git a/esphome/components/light/automation.h b/esphome/components/light/automation.h index 8899db8bba..9893c15e0c 100644 --- a/esphome/components/light/automation.h +++ b/esphome/components/light/automation.h @@ -4,8 +4,7 @@ #include "light_state.h" #include "addressable_light.h" -namespace esphome { -namespace light { +namespace esphome::light { enum class LimitMode { CLAMP, DO_NOTHING }; @@ -216,5 +215,4 @@ template class AddressableSet : public Action { } }; -} // namespace light -} // namespace esphome +} // namespace esphome::light diff --git a/esphome/components/light/base_light_effects.h b/esphome/components/light/base_light_effects.h index 515afc5c59..2eeae574e7 100644 --- a/esphome/components/light/base_light_effects.h +++ b/esphome/components/light/base_light_effects.h @@ -6,8 +6,7 @@ #include "esphome/core/helpers.h" #include "light_effect.h" -namespace esphome { -namespace light { +namespace esphome::light { inline static float random_cubic_float() { const float r = random_float() * 2.0f - 1.0f; @@ -235,5 +234,4 @@ class FlickerLightEffect : public LightEffect { float alpha_{}; }; -} // namespace light -} // namespace esphome +} // namespace esphome::light diff --git a/esphome/components/light/color_mode.h b/esphome/components/light/color_mode.h index aa3448c145..0750ae250d 100644 --- a/esphome/components/light/color_mode.h +++ b/esphome/components/light/color_mode.h @@ -3,8 +3,7 @@ #include #include "esphome/core/finite_set_mask.h" -namespace esphome { -namespace light { +namespace esphome::light { /// Color capabilities are the various outputs that a light has and that can be independently controlled by the user. enum class ColorCapability : uint8_t { @@ -210,5 +209,4 @@ inline bool has_capability(const ColorModeMask &mask, ColorCapability capability return (mask.get_mask() & CAPABILITY_BITMASKS[capability_to_index(capability)]) != 0; } -} // namespace light -} // namespace esphome +} // namespace esphome::light diff --git a/esphome/components/light/esp_color_correction.cpp b/esphome/components/light/esp_color_correction.cpp index e5e68264cc..1b511a94b2 100644 --- a/esphome/components/light/esp_color_correction.cpp +++ b/esphome/components/light/esp_color_correction.cpp @@ -2,8 +2,7 @@ #include "light_color_values.h" #include "esphome/core/log.h" -namespace esphome { -namespace light { +namespace esphome::light { void ESPColorCorrection::calculate_gamma_table(float gamma) { for (uint16_t i = 0; i < 256; i++) { @@ -23,5 +22,4 @@ void ESPColorCorrection::calculate_gamma_table(float gamma) { } } -} // namespace light -} // namespace esphome +} // namespace esphome::light diff --git a/esphome/components/light/esp_color_correction.h b/esphome/components/light/esp_color_correction.h index 14c065058c..d275e045b7 100644 --- a/esphome/components/light/esp_color_correction.h +++ b/esphome/components/light/esp_color_correction.h @@ -2,8 +2,7 @@ #include "esphome/core/color.h" -namespace esphome { -namespace light { +namespace esphome::light { class ESPColorCorrection { public: @@ -73,5 +72,4 @@ class ESPColorCorrection { uint8_t local_brightness_{255}; }; -} // namespace light -} // namespace esphome +} // namespace esphome::light diff --git a/esphome/components/light/esp_color_view.h b/esphome/components/light/esp_color_view.h index 35117e7dd8..440a23e9c9 100644 --- a/esphome/components/light/esp_color_view.h +++ b/esphome/components/light/esp_color_view.h @@ -4,8 +4,7 @@ #include "esp_hsv_color.h" #include "esp_color_correction.h" -namespace esphome { -namespace light { +namespace esphome::light { class ESPColorSettable { public: @@ -106,5 +105,4 @@ class ESPColorView : public ESPColorSettable { const ESPColorCorrection *color_correction_; }; -} // namespace light -} // namespace esphome +} // namespace esphome::light diff --git a/esphome/components/light/esp_hsv_color.cpp b/esphome/components/light/esp_hsv_color.cpp index 450c2e11ce..07205ea6d0 100644 --- a/esphome/components/light/esp_hsv_color.cpp +++ b/esphome/components/light/esp_hsv_color.cpp @@ -1,7 +1,6 @@ #include "esp_hsv_color.h" -namespace esphome { -namespace light { +namespace esphome::light { Color ESPHSVColor::to_rgb() const { // based on FastLED's hsv rainbow to rgb @@ -70,5 +69,4 @@ Color ESPHSVColor::to_rgb() const { return rgb; } -} // namespace light -} // namespace esphome +} // namespace esphome::light diff --git a/esphome/components/light/esp_hsv_color.h b/esphome/components/light/esp_hsv_color.h index cdde91c71c..4b54039258 100644 --- a/esphome/components/light/esp_hsv_color.h +++ b/esphome/components/light/esp_hsv_color.h @@ -3,8 +3,7 @@ #include "esphome/core/color.h" #include "esphome/core/helpers.h" -namespace esphome { -namespace light { +namespace esphome::light { struct ESPHSVColor { union { @@ -32,5 +31,4 @@ struct ESPHSVColor { Color to_rgb() const; }; -} // namespace light -} // namespace esphome +} // namespace esphome::light diff --git a/esphome/components/light/esp_range_view.cpp b/esphome/components/light/esp_range_view.cpp index e1f0a507bd..58d552031a 100644 --- a/esphome/components/light/esp_range_view.cpp +++ b/esphome/components/light/esp_range_view.cpp @@ -1,8 +1,7 @@ #include "esp_range_view.h" #include "addressable_light.h" -namespace esphome { -namespace light { +namespace esphome::light { int32_t HOT interpret_index(int32_t index, int32_t size) { if (index < 0) @@ -92,5 +91,4 @@ ESPRangeView &ESPRangeView::operator=(const ESPRangeView &rhs) { // NOLINT ESPColorView ESPRangeIterator::operator*() const { return this->range_.parent_->get(this->i_); } -} // namespace light -} // namespace esphome +} // namespace esphome::light diff --git a/esphome/components/light/esp_range_view.h b/esphome/components/light/esp_range_view.h index 07d18af79f..f5e4ebb83f 100644 --- a/esphome/components/light/esp_range_view.h +++ b/esphome/components/light/esp_range_view.h @@ -3,8 +3,7 @@ #include "esp_color_view.h" #include "esp_hsv_color.h" -namespace esphome { -namespace light { +namespace esphome::light { int32_t interpret_index(int32_t index, int32_t size); @@ -76,5 +75,4 @@ class ESPRangeIterator { int32_t i_; }; -} // namespace light -} // namespace esphome +} // namespace esphome::light diff --git a/esphome/components/light/light_call.cpp b/esphome/components/light/light_call.cpp index b15ff84b97..b3bdb16c73 100644 --- a/esphome/components/light/light_call.cpp +++ b/esphome/components/light/light_call.cpp @@ -4,8 +4,7 @@ #include "esphome/core/log.h" #include "esphome/core/optional.h" -namespace esphome { -namespace light { +namespace esphome::light { static const char *const TAG = "light"; @@ -647,5 +646,4 @@ LightCall &LightCall::set_rgbw(float red, float green, float blue, float white) return *this; } -} // namespace light -} // namespace esphome +} // namespace esphome::light diff --git a/esphome/components/light/light_color_values.h b/esphome/components/light/light_color_values.h index 04d7d1e7d8..bedfad2c35 100644 --- a/esphome/components/light/light_color_values.h +++ b/esphome/components/light/light_color_values.h @@ -4,8 +4,7 @@ #include "color_mode.h" #include -namespace esphome { -namespace light { +namespace esphome::light { inline static uint8_t to_uint8_scale(float x) { return static_cast(roundf(x * 255.0f)); } @@ -310,5 +309,4 @@ class LightColorValues { ColorMode color_mode_; }; -} // namespace light -} // namespace esphome +} // namespace esphome::light diff --git a/esphome/components/light/light_effect.cpp b/esphome/components/light/light_effect.cpp index a210b48e5b..81b923f7f9 100644 --- a/esphome/components/light/light_effect.cpp +++ b/esphome/components/light/light_effect.cpp @@ -1,8 +1,7 @@ #include "light_effect.h" #include "light_state.h" -namespace esphome { -namespace light { +namespace esphome::light { uint32_t LightEffect::get_index() const { if (this->state_ == nullptr) { @@ -32,5 +31,4 @@ uint32_t LightEffect::get_index_in_parent_() const { return 0; // Not found } -} // namespace light -} // namespace esphome +} // namespace esphome::light diff --git a/esphome/components/light/light_effect.h b/esphome/components/light/light_effect.h index d4c2dc3582..aa1f6f7899 100644 --- a/esphome/components/light/light_effect.h +++ b/esphome/components/light/light_effect.h @@ -2,8 +2,7 @@ #include "esphome/core/component.h" -namespace esphome { -namespace light { +namespace esphome::light { class LightState; @@ -55,5 +54,4 @@ class LightEffect { uint32_t get_index_in_parent_() const; }; -} // namespace light -} // namespace esphome +} // namespace esphome::light diff --git a/esphome/components/light/light_json_schema.cpp b/esphome/components/light/light_json_schema.cpp index e754c453b5..1c9b92f504 100644 --- a/esphome/components/light/light_json_schema.cpp +++ b/esphome/components/light/light_json_schema.cpp @@ -3,8 +3,7 @@ #ifdef USE_JSON -namespace esphome { -namespace light { +namespace esphome::light { // See https://www.home-assistant.io/integrations/light.mqtt/#json-schema for documentation on the schema @@ -169,7 +168,6 @@ void LightJSONSchema::parse_json(LightState &state, LightCall &call, JsonObject } } -} // namespace light -} // namespace esphome +} // namespace esphome::light #endif diff --git a/esphome/components/light/light_json_schema.h b/esphome/components/light/light_json_schema.h index c92dd7b655..dac81e32e3 100644 --- a/esphome/components/light/light_json_schema.h +++ b/esphome/components/light/light_json_schema.h @@ -8,8 +8,7 @@ #include "light_call.h" #include "light_state.h" -namespace esphome { -namespace light { +namespace esphome::light { class LightJSONSchema { public: @@ -22,7 +21,6 @@ class LightJSONSchema { static void parse_color_json(LightState &state, LightCall &call, JsonObject root); }; -} // namespace light -} // namespace esphome +} // namespace esphome::light #endif diff --git a/esphome/components/light/light_output.cpp b/esphome/components/light/light_output.cpp index e805a0b694..a86e8e5bf1 100644 --- a/esphome/components/light/light_output.cpp +++ b/esphome/components/light/light_output.cpp @@ -1,12 +1,10 @@ #include "light_output.h" #include "transformers.h" -namespace esphome { -namespace light { +namespace esphome::light { std::unique_ptr LightOutput::create_default_transition() { return make_unique(); } -} // namespace light -} // namespace esphome +} // namespace esphome::light diff --git a/esphome/components/light/light_output.h b/esphome/components/light/light_output.h index 73ba0371cd..c82d270be8 100644 --- a/esphome/components/light/light_output.h +++ b/esphome/components/light/light_output.h @@ -5,8 +5,7 @@ #include "light_state.h" #include "light_transformer.h" -namespace esphome { -namespace light { +namespace esphome::light { /// Interface to write LightStates to hardware. class LightOutput { @@ -29,5 +28,4 @@ class LightOutput { virtual void write_state(LightState *state) = 0; }; -} // namespace light -} // namespace esphome +} // namespace esphome::light diff --git a/esphome/components/light/light_state.cpp b/esphome/components/light/light_state.cpp index 4c253ec5a8..36b2af03a5 100644 --- a/esphome/components/light/light_state.cpp +++ b/esphome/components/light/light_state.cpp @@ -5,8 +5,7 @@ #include "light_output.h" #include "transformers.h" -namespace esphome { -namespace light { +namespace esphome::light { static const char *const TAG = "light"; @@ -304,5 +303,4 @@ void LightState::save_remote_values_() { this->rtc_.save(&saved); } -} // namespace light -} // namespace esphome +} // namespace esphome::light diff --git a/esphome/components/light/light_state.h b/esphome/components/light/light_state.h index bf63c0ec27..06519cdc14 100644 --- a/esphome/components/light/light_state.h +++ b/esphome/components/light/light_state.h @@ -15,8 +15,7 @@ #include #include -namespace esphome { -namespace light { +namespace esphome::light { class LightOutput; @@ -298,5 +297,4 @@ class LightState : public EntityBase, public Component { LightRestoreMode restore_mode_; }; -} // namespace light -} // namespace esphome +} // namespace esphome::light diff --git a/esphome/components/light/light_transformer.h b/esphome/components/light/light_transformer.h index a84183c03c..079c2d2ae0 100644 --- a/esphome/components/light/light_transformer.h +++ b/esphome/components/light/light_transformer.h @@ -4,8 +4,7 @@ #include "esphome/core/helpers.h" #include "light_color_values.h" -namespace esphome { -namespace light { +namespace esphome::light { /// Base class for all light color transformers, such as transitions or flashes. class LightTransformer { @@ -59,5 +58,4 @@ class LightTransformer { LightColorValues target_values_; }; -} // namespace light -} // namespace esphome +} // namespace esphome::light diff --git a/esphome/components/light/transformers.h b/esphome/components/light/transformers.h index 71d41a66d3..a26713b723 100644 --- a/esphome/components/light/transformers.h +++ b/esphome/components/light/transformers.h @@ -6,8 +6,7 @@ #include "light_state.h" #include "light_transformer.h" -namespace esphome { -namespace light { +namespace esphome::light { class LightTransitionTransformer : public LightTransformer { public: @@ -118,5 +117,4 @@ class LightFlashTransformer : public LightTransformer { bool begun_lightstate_restore_; }; -} // namespace light -} // namespace esphome +} // namespace esphome::light From 2d7942e788dc36e5d8de504d58764dc03646e481 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 18 Nov 2025 21:46:47 -0600 Subject: [PATCH 138/415] [ld24xx] Modernize namespace declarations to C++17 syntax --- esphome/components/ld2410/automation.h | 6 ++---- esphome/components/ld2410/button/factory_reset_button.cpp | 6 ++---- esphome/components/ld2410/button/factory_reset_button.h | 6 ++---- esphome/components/ld2410/button/query_button.cpp | 6 ++---- esphome/components/ld2410/button/query_button.h | 6 ++---- esphome/components/ld2410/button/restart_button.cpp | 6 ++---- esphome/components/ld2410/button/restart_button.h | 6 ++---- esphome/components/ld2410/ld2410.cpp | 6 ++---- esphome/components/ld2410/ld2410.h | 6 ++---- esphome/components/ld2410/number/gate_threshold_number.cpp | 6 ++---- esphome/components/ld2410/number/gate_threshold_number.h | 6 ++---- esphome/components/ld2410/number/light_threshold_number.cpp | 6 ++---- esphome/components/ld2410/number/light_threshold_number.h | 6 ++---- .../ld2410/number/max_distance_timeout_number.cpp | 6 ++---- .../components/ld2410/number/max_distance_timeout_number.h | 6 ++---- esphome/components/ld2410/select/baud_rate_select.cpp | 6 ++---- esphome/components/ld2410/select/baud_rate_select.h | 6 ++---- .../components/ld2410/select/distance_resolution_select.cpp | 6 ++---- .../components/ld2410/select/distance_resolution_select.h | 6 ++---- .../components/ld2410/select/light_out_control_select.cpp | 6 ++---- esphome/components/ld2410/select/light_out_control_select.h | 6 ++---- esphome/components/ld2410/switch/bluetooth_switch.cpp | 6 ++---- esphome/components/ld2410/switch/bluetooth_switch.h | 6 ++---- .../components/ld2410/switch/engineering_mode_switch.cpp | 6 ++---- esphome/components/ld2410/switch/engineering_mode_switch.h | 6 ++---- esphome/components/ld2412/button/factory_reset_button.cpp | 6 ++---- esphome/components/ld2412/button/factory_reset_button.h | 6 ++---- esphome/components/ld2412/button/query_button.cpp | 6 ++---- esphome/components/ld2412/button/query_button.h | 6 ++---- esphome/components/ld2412/button/restart_button.cpp | 6 ++---- esphome/components/ld2412/button/restart_button.h | 6 ++---- .../button/start_dynamic_background_correction_button.cpp | 6 ++---- .../button/start_dynamic_background_correction_button.h | 6 ++---- esphome/components/ld2412/ld2412.cpp | 6 ++---- esphome/components/ld2412/ld2412.h | 6 ++---- esphome/components/ld2412/number/gate_threshold_number.cpp | 6 ++---- esphome/components/ld2412/number/gate_threshold_number.h | 6 ++---- esphome/components/ld2412/number/light_threshold_number.cpp | 6 ++---- esphome/components/ld2412/number/light_threshold_number.h | 6 ++---- .../ld2412/number/max_distance_timeout_number.cpp | 6 ++---- .../components/ld2412/number/max_distance_timeout_number.h | 6 ++---- esphome/components/ld2412/select/baud_rate_select.cpp | 6 ++---- esphome/components/ld2412/select/baud_rate_select.h | 6 ++---- .../components/ld2412/select/distance_resolution_select.cpp | 6 ++---- .../components/ld2412/select/distance_resolution_select.h | 6 ++---- .../components/ld2412/select/light_out_control_select.cpp | 6 ++---- esphome/components/ld2412/select/light_out_control_select.h | 6 ++---- esphome/components/ld2412/switch/bluetooth_switch.cpp | 6 ++---- esphome/components/ld2412/switch/bluetooth_switch.h | 6 ++---- .../components/ld2412/switch/engineering_mode_switch.cpp | 6 ++---- esphome/components/ld2412/switch/engineering_mode_switch.h | 6 ++---- .../ld2420/binary_sensor/ld2420_binary_sensor.cpp | 6 ++---- .../components/ld2420/binary_sensor/ld2420_binary_sensor.h | 6 ++---- esphome/components/ld2420/button/reconfig_buttons.cpp | 6 ++---- esphome/components/ld2420/button/reconfig_buttons.h | 6 ++---- esphome/components/ld2420/ld2420.cpp | 6 ++---- esphome/components/ld2420/ld2420.h | 6 ++---- esphome/components/ld2420/number/gate_config_number.cpp | 6 ++---- esphome/components/ld2420/number/gate_config_number.h | 6 ++---- esphome/components/ld2420/select/operating_mode_select.cpp | 6 ++---- esphome/components/ld2420/select/operating_mode_select.h | 6 ++---- esphome/components/ld2420/sensor/ld2420_sensor.cpp | 6 ++---- esphome/components/ld2420/sensor/ld2420_sensor.h | 6 ++---- .../components/ld2420/text_sensor/ld2420_text_sensor.cpp | 6 ++---- esphome/components/ld2420/text_sensor/ld2420_text_sensor.h | 6 ++---- esphome/components/ld2450/button/factory_reset_button.cpp | 6 ++---- esphome/components/ld2450/button/factory_reset_button.h | 6 ++---- esphome/components/ld2450/button/restart_button.cpp | 6 ++---- esphome/components/ld2450/button/restart_button.h | 6 ++---- esphome/components/ld2450/ld2450.cpp | 6 ++---- esphome/components/ld2450/ld2450.h | 6 ++---- .../components/ld2450/number/presence_timeout_number.cpp | 6 ++---- esphome/components/ld2450/number/presence_timeout_number.h | 6 ++---- esphome/components/ld2450/number/zone_coordinate_number.cpp | 6 ++---- esphome/components/ld2450/number/zone_coordinate_number.h | 6 ++---- esphome/components/ld2450/select/baud_rate_select.cpp | 6 ++---- esphome/components/ld2450/select/baud_rate_select.h | 6 ++---- esphome/components/ld2450/select/zone_type_select.cpp | 6 ++---- esphome/components/ld2450/select/zone_type_select.h | 6 ++---- esphome/components/ld2450/switch/bluetooth_switch.cpp | 6 ++---- esphome/components/ld2450/switch/bluetooth_switch.h | 6 ++---- esphome/components/ld2450/switch/multi_target_switch.cpp | 6 ++---- esphome/components/ld2450/switch/multi_target_switch.h | 6 ++---- esphome/components/ld24xx/ld24xx.h | 6 ++---- 84 files changed, 168 insertions(+), 336 deletions(-) diff --git a/esphome/components/ld2410/automation.h b/esphome/components/ld2410/automation.h index f4f1c197b2..614453b575 100644 --- a/esphome/components/ld2410/automation.h +++ b/esphome/components/ld2410/automation.h @@ -4,8 +4,7 @@ #include "esphome/core/component.h" #include "ld2410.h" -namespace esphome { -namespace ld2410 { +namespace esphome::ld2410 { template class BluetoothPasswordSetAction : public Action { public: @@ -18,5 +17,4 @@ template class BluetoothPasswordSetAction : public Action LD2410Component *ld2410_comp_; }; -} // namespace ld2410 -} // namespace esphome +} // namespace esphome::ld2410 diff --git a/esphome/components/ld2410/button/factory_reset_button.cpp b/esphome/components/ld2410/button/factory_reset_button.cpp index a848b02a9d..0223df7086 100644 --- a/esphome/components/ld2410/button/factory_reset_button.cpp +++ b/esphome/components/ld2410/button/factory_reset_button.cpp @@ -1,9 +1,7 @@ #include "factory_reset_button.h" -namespace esphome { -namespace ld2410 { +namespace esphome::ld2410 { void FactoryResetButton::press_action() { this->parent_->factory_reset(); } -} // namespace ld2410 -} // namespace esphome +} // namespace esphome::ld2410 diff --git a/esphome/components/ld2410/button/factory_reset_button.h b/esphome/components/ld2410/button/factory_reset_button.h index 45bf979033..715a8c4056 100644 --- a/esphome/components/ld2410/button/factory_reset_button.h +++ b/esphome/components/ld2410/button/factory_reset_button.h @@ -3,8 +3,7 @@ #include "esphome/components/button/button.h" #include "../ld2410.h" -namespace esphome { -namespace ld2410 { +namespace esphome::ld2410 { class FactoryResetButton : public button::Button, public Parented { public: @@ -14,5 +13,4 @@ class FactoryResetButton : public button::Button, public Parentedparent_->read_all_info(); } -} // namespace ld2410 -} // namespace esphome +} // namespace esphome::ld2410 diff --git a/esphome/components/ld2410/button/query_button.h b/esphome/components/ld2410/button/query_button.h index c7a47e32d8..7a786901ae 100644 --- a/esphome/components/ld2410/button/query_button.h +++ b/esphome/components/ld2410/button/query_button.h @@ -3,8 +3,7 @@ #include "esphome/components/button/button.h" #include "../ld2410.h" -namespace esphome { -namespace ld2410 { +namespace esphome::ld2410 { class QueryButton : public button::Button, public Parented { public: @@ -14,5 +13,4 @@ class QueryButton : public button::Button, public Parented { void press_action() override; }; -} // namespace ld2410 -} // namespace esphome +} // namespace esphome::ld2410 diff --git a/esphome/components/ld2410/button/restart_button.cpp b/esphome/components/ld2410/button/restart_button.cpp index de0d36c1ef..0d5002d3c6 100644 --- a/esphome/components/ld2410/button/restart_button.cpp +++ b/esphome/components/ld2410/button/restart_button.cpp @@ -1,9 +1,7 @@ #include "restart_button.h" -namespace esphome { -namespace ld2410 { +namespace esphome::ld2410 { void RestartButton::press_action() { this->parent_->restart_and_read_all_info(); } -} // namespace ld2410 -} // namespace esphome +} // namespace esphome::ld2410 diff --git a/esphome/components/ld2410/button/restart_button.h b/esphome/components/ld2410/button/restart_button.h index d00dc05a53..9bf8639a8c 100644 --- a/esphome/components/ld2410/button/restart_button.h +++ b/esphome/components/ld2410/button/restart_button.h @@ -3,8 +3,7 @@ #include "esphome/components/button/button.h" #include "../ld2410.h" -namespace esphome { -namespace ld2410 { +namespace esphome::ld2410 { class RestartButton : public button::Button, public Parented { public: @@ -14,5 +13,4 @@ class RestartButton : public button::Button, public Parented { void press_action() override; }; -} // namespace ld2410 -} // namespace esphome +} // namespace esphome::ld2410 diff --git a/esphome/components/ld2410/ld2410.cpp b/esphome/components/ld2410/ld2410.cpp index 391f2024cd..bb2e4e2f4c 100644 --- a/esphome/components/ld2410/ld2410.cpp +++ b/esphome/components/ld2410/ld2410.cpp @@ -9,8 +9,7 @@ #include "esphome/core/application.h" -namespace esphome { -namespace ld2410 { +namespace esphome::ld2410 { static const char *const TAG = "ld2410"; @@ -782,5 +781,4 @@ void LD2410Component::set_gate_still_sensor(uint8_t gate, sensor::Sensor *s) { } #endif -} // namespace ld2410 -} // namespace esphome +} // namespace esphome::ld2410 diff --git a/esphome/components/ld2410/ld2410.h b/esphome/components/ld2410/ld2410.h index 52cf76b5b6..efe585fb76 100644 --- a/esphome/components/ld2410/ld2410.h +++ b/esphome/components/ld2410/ld2410.h @@ -29,8 +29,7 @@ #include -namespace esphome { -namespace ld2410 { +namespace esphome::ld2410 { using namespace ld24xx; @@ -133,5 +132,4 @@ class LD2410Component : public Component, public uart::UARTDevice { #endif }; -} // namespace ld2410 -} // namespace esphome +} // namespace esphome::ld2410 diff --git a/esphome/components/ld2410/number/gate_threshold_number.cpp b/esphome/components/ld2410/number/gate_threshold_number.cpp index 5d040554d7..65e864a4d7 100644 --- a/esphome/components/ld2410/number/gate_threshold_number.cpp +++ b/esphome/components/ld2410/number/gate_threshold_number.cpp @@ -1,7 +1,6 @@ #include "gate_threshold_number.h" -namespace esphome { -namespace ld2410 { +namespace esphome::ld2410 { GateThresholdNumber::GateThresholdNumber(uint8_t gate) : gate_(gate) {} @@ -10,5 +9,4 @@ void GateThresholdNumber::control(float value) { this->parent_->set_gate_threshold(this->gate_); } -} // namespace ld2410 -} // namespace esphome +} // namespace esphome::ld2410 diff --git a/esphome/components/ld2410/number/gate_threshold_number.h b/esphome/components/ld2410/number/gate_threshold_number.h index 2806ecce63..63491f18d3 100644 --- a/esphome/components/ld2410/number/gate_threshold_number.h +++ b/esphome/components/ld2410/number/gate_threshold_number.h @@ -3,8 +3,7 @@ #include "esphome/components/number/number.h" #include "../ld2410.h" -namespace esphome { -namespace ld2410 { +namespace esphome::ld2410 { class GateThresholdNumber : public number::Number, public Parented { public: @@ -15,5 +14,4 @@ class GateThresholdNumber : public number::Number, public Parentedpublish_state(value); this->parent_->set_light_out_control(); } -} // namespace ld2410 -} // namespace esphome +} // namespace esphome::ld2410 diff --git a/esphome/components/ld2410/number/light_threshold_number.h b/esphome/components/ld2410/number/light_threshold_number.h index 8f014373c0..3c5e433416 100644 --- a/esphome/components/ld2410/number/light_threshold_number.h +++ b/esphome/components/ld2410/number/light_threshold_number.h @@ -3,8 +3,7 @@ #include "esphome/components/number/number.h" #include "../ld2410.h" -namespace esphome { -namespace ld2410 { +namespace esphome::ld2410 { class LightThresholdNumber : public number::Number, public Parented { public: @@ -14,5 +13,4 @@ class LightThresholdNumber : public number::Number, public Parentedpublish_state(value); this->parent_->set_max_distances_timeout(); } -} // namespace ld2410 -} // namespace esphome +} // namespace esphome::ld2410 diff --git a/esphome/components/ld2410/number/max_distance_timeout_number.h b/esphome/components/ld2410/number/max_distance_timeout_number.h index 7d91b4b5fe..35f4cbbfae 100644 --- a/esphome/components/ld2410/number/max_distance_timeout_number.h +++ b/esphome/components/ld2410/number/max_distance_timeout_number.h @@ -3,8 +3,7 @@ #include "esphome/components/number/number.h" #include "../ld2410.h" -namespace esphome { -namespace ld2410 { +namespace esphome::ld2410 { class MaxDistanceTimeoutNumber : public number::Number, public Parented { public: @@ -14,5 +13,4 @@ class MaxDistanceTimeoutNumber : public number::Number, public Parentedpublish_state(index); this->parent_->set_baud_rate(this->option_at(index)); } -} // namespace ld2410 -} // namespace esphome +} // namespace esphome::ld2410 diff --git a/esphome/components/ld2410/select/baud_rate_select.h b/esphome/components/ld2410/select/baud_rate_select.h index 9385c8cf7e..fb1d016b1f 100644 --- a/esphome/components/ld2410/select/baud_rate_select.h +++ b/esphome/components/ld2410/select/baud_rate_select.h @@ -3,8 +3,7 @@ #include "esphome/components/select/select.h" #include "../ld2410.h" -namespace esphome { -namespace ld2410 { +namespace esphome::ld2410 { class BaudRateSelect : public select::Select, public Parented { public: @@ -14,5 +13,4 @@ class BaudRateSelect : public select::Select, public Parented { void control(size_t index) override; }; -} // namespace ld2410 -} // namespace esphome +} // namespace esphome::ld2410 diff --git a/esphome/components/ld2410/select/distance_resolution_select.cpp b/esphome/components/ld2410/select/distance_resolution_select.cpp index 4fc4c5af02..635bf206d3 100644 --- a/esphome/components/ld2410/select/distance_resolution_select.cpp +++ b/esphome/components/ld2410/select/distance_resolution_select.cpp @@ -1,12 +1,10 @@ #include "distance_resolution_select.h" -namespace esphome { -namespace ld2410 { +namespace esphome::ld2410 { void DistanceResolutionSelect::control(size_t index) { this->publish_state(index); this->parent_->set_distance_resolution(this->option_at(index)); } -} // namespace ld2410 -} // namespace esphome +} // namespace esphome::ld2410 diff --git a/esphome/components/ld2410/select/distance_resolution_select.h b/esphome/components/ld2410/select/distance_resolution_select.h index 1a04f843a6..be2389d36e 100644 --- a/esphome/components/ld2410/select/distance_resolution_select.h +++ b/esphome/components/ld2410/select/distance_resolution_select.h @@ -3,8 +3,7 @@ #include "esphome/components/select/select.h" #include "../ld2410.h" -namespace esphome { -namespace ld2410 { +namespace esphome::ld2410 { class DistanceResolutionSelect : public select::Select, public Parented { public: @@ -14,5 +13,4 @@ class DistanceResolutionSelect : public select::Select, public Parentedpublish_state(index); this->parent_->set_light_out_control(); } -} // namespace ld2410 -} // namespace esphome +} // namespace esphome::ld2410 diff --git a/esphome/components/ld2410/select/light_out_control_select.h b/esphome/components/ld2410/select/light_out_control_select.h index e8cd8f1d6a..608c311af4 100644 --- a/esphome/components/ld2410/select/light_out_control_select.h +++ b/esphome/components/ld2410/select/light_out_control_select.h @@ -3,8 +3,7 @@ #include "esphome/components/select/select.h" #include "../ld2410.h" -namespace esphome { -namespace ld2410 { +namespace esphome::ld2410 { class LightOutControlSelect : public select::Select, public Parented { public: @@ -14,5 +13,4 @@ class LightOutControlSelect : public select::Select, public Parentedpublish_state(state); this->parent_->set_bluetooth(state); } -} // namespace ld2410 -} // namespace esphome +} // namespace esphome::ld2410 diff --git a/esphome/components/ld2410/switch/bluetooth_switch.h b/esphome/components/ld2410/switch/bluetooth_switch.h index 35ae1ec0c9..07804e2292 100644 --- a/esphome/components/ld2410/switch/bluetooth_switch.h +++ b/esphome/components/ld2410/switch/bluetooth_switch.h @@ -3,8 +3,7 @@ #include "esphome/components/switch/switch.h" #include "../ld2410.h" -namespace esphome { -namespace ld2410 { +namespace esphome::ld2410 { class BluetoothSwitch : public switch_::Switch, public Parented { public: @@ -14,5 +13,4 @@ class BluetoothSwitch : public switch_::Switch, public Parented void write_state(bool state) override; }; -} // namespace ld2410 -} // namespace esphome +} // namespace esphome::ld2410 diff --git a/esphome/components/ld2410/switch/engineering_mode_switch.cpp b/esphome/components/ld2410/switch/engineering_mode_switch.cpp index 967c87c887..4f2f08b03e 100644 --- a/esphome/components/ld2410/switch/engineering_mode_switch.cpp +++ b/esphome/components/ld2410/switch/engineering_mode_switch.cpp @@ -1,12 +1,10 @@ #include "engineering_mode_switch.h" -namespace esphome { -namespace ld2410 { +namespace esphome::ld2410 { void EngineeringModeSwitch::write_state(bool state) { this->publish_state(state); this->parent_->set_engineering_mode(state); } -} // namespace ld2410 -} // namespace esphome +} // namespace esphome::ld2410 diff --git a/esphome/components/ld2410/switch/engineering_mode_switch.h b/esphome/components/ld2410/switch/engineering_mode_switch.h index e521200cd6..4dd8e16653 100644 --- a/esphome/components/ld2410/switch/engineering_mode_switch.h +++ b/esphome/components/ld2410/switch/engineering_mode_switch.h @@ -3,8 +3,7 @@ #include "esphome/components/switch/switch.h" #include "../ld2410.h" -namespace esphome { -namespace ld2410 { +namespace esphome::ld2410 { class EngineeringModeSwitch : public switch_::Switch, public Parented { public: @@ -14,5 +13,4 @@ class EngineeringModeSwitch : public switch_::Switch, public Parentedparent_->factory_reset(); } -} // namespace ld2412 -} // namespace esphome +} // namespace esphome::ld2412 diff --git a/esphome/components/ld2412/button/factory_reset_button.h b/esphome/components/ld2412/button/factory_reset_button.h index 36a3fffcd5..1ef6b23b80 100644 --- a/esphome/components/ld2412/button/factory_reset_button.h +++ b/esphome/components/ld2412/button/factory_reset_button.h @@ -3,8 +3,7 @@ #include "esphome/components/button/button.h" #include "../ld2412.h" -namespace esphome { -namespace ld2412 { +namespace esphome::ld2412 { class FactoryResetButton : public button::Button, public Parented { public: @@ -14,5 +13,4 @@ class FactoryResetButton : public button::Button, public Parentedparent_->read_all_info(); } -} // namespace ld2412 -} // namespace esphome +} // namespace esphome::ld2412 diff --git a/esphome/components/ld2412/button/query_button.h b/esphome/components/ld2412/button/query_button.h index 595ef6d1e9..373e135802 100644 --- a/esphome/components/ld2412/button/query_button.h +++ b/esphome/components/ld2412/button/query_button.h @@ -3,8 +3,7 @@ #include "esphome/components/button/button.h" #include "../ld2412.h" -namespace esphome { -namespace ld2412 { +namespace esphome::ld2412 { class QueryButton : public button::Button, public Parented { public: @@ -14,5 +13,4 @@ class QueryButton : public button::Button, public Parented { void press_action() override; }; -} // namespace ld2412 -} // namespace esphome +} // namespace esphome::ld2412 diff --git a/esphome/components/ld2412/button/restart_button.cpp b/esphome/components/ld2412/button/restart_button.cpp index aca0d17841..430f6c998f 100644 --- a/esphome/components/ld2412/button/restart_button.cpp +++ b/esphome/components/ld2412/button/restart_button.cpp @@ -1,9 +1,7 @@ #include "restart_button.h" -namespace esphome { -namespace ld2412 { +namespace esphome::ld2412 { void RestartButton::press_action() { this->parent_->restart_and_read_all_info(); } -} // namespace ld2412 -} // namespace esphome +} // namespace esphome::ld2412 diff --git a/esphome/components/ld2412/button/restart_button.h b/esphome/components/ld2412/button/restart_button.h index 5cd582e2a3..80c79f5e7d 100644 --- a/esphome/components/ld2412/button/restart_button.h +++ b/esphome/components/ld2412/button/restart_button.h @@ -3,8 +3,7 @@ #include "esphome/components/button/button.h" #include "../ld2412.h" -namespace esphome { -namespace ld2412 { +namespace esphome::ld2412 { class RestartButton : public button::Button, public Parented { public: @@ -14,5 +13,4 @@ class RestartButton : public button::Button, public Parented { void press_action() override; }; -} // namespace ld2412 -} // namespace esphome +} // namespace esphome::ld2412 diff --git a/esphome/components/ld2412/button/start_dynamic_background_correction_button.cpp b/esphome/components/ld2412/button/start_dynamic_background_correction_button.cpp index 9b37243b82..8ba41a03fb 100644 --- a/esphome/components/ld2412/button/start_dynamic_background_correction_button.cpp +++ b/esphome/components/ld2412/button/start_dynamic_background_correction_button.cpp @@ -2,10 +2,8 @@ #include "restart_button.h" -namespace esphome { -namespace ld2412 { +namespace esphome::ld2412 { void StartDynamicBackgroundCorrectionButton::press_action() { this->parent_->start_dynamic_background_correction(); } -} // namespace ld2412 -} // namespace esphome +} // namespace esphome::ld2412 diff --git a/esphome/components/ld2412/button/start_dynamic_background_correction_button.h b/esphome/components/ld2412/button/start_dynamic_background_correction_button.h index 3af0a8a149..b1f2127896 100644 --- a/esphome/components/ld2412/button/start_dynamic_background_correction_button.h +++ b/esphome/components/ld2412/button/start_dynamic_background_correction_button.h @@ -3,8 +3,7 @@ #include "esphome/components/button/button.h" #include "../ld2412.h" -namespace esphome { -namespace ld2412 { +namespace esphome::ld2412 { class StartDynamicBackgroundCorrectionButton : public button::Button, public Parented { public: @@ -14,5 +13,4 @@ class StartDynamicBackgroundCorrectionButton : public button::Button, public Par void press_action() override; }; -} // namespace ld2412 -} // namespace esphome +} // namespace esphome::ld2412 diff --git a/esphome/components/ld2412/ld2412.cpp b/esphome/components/ld2412/ld2412.cpp index 4f2fd7c2bd..0f6fe62d30 100644 --- a/esphome/components/ld2412/ld2412.cpp +++ b/esphome/components/ld2412/ld2412.cpp @@ -10,8 +10,7 @@ #include "esphome/core/application.h" #include "esphome/core/helpers.h" -namespace esphome { -namespace ld2412 { +namespace esphome::ld2412 { static const char *const TAG = "ld2412"; @@ -855,5 +854,4 @@ void LD2412Component::set_gate_still_sensor(uint8_t gate, sensor::Sensor *s) { } #endif -} // namespace ld2412 -} // namespace esphome +} // namespace esphome::ld2412 diff --git a/esphome/components/ld2412/ld2412.h b/esphome/components/ld2412/ld2412.h index 2bed34bdd8..5dd5e7bcde 100644 --- a/esphome/components/ld2412/ld2412.h +++ b/esphome/components/ld2412/ld2412.h @@ -29,8 +29,7 @@ #include -namespace esphome { -namespace ld2412 { +namespace esphome::ld2412 { using namespace ld24xx; @@ -137,5 +136,4 @@ class LD2412Component : public Component, public uart::UARTDevice { #endif }; -} // namespace ld2412 -} // namespace esphome +} // namespace esphome::ld2412 diff --git a/esphome/components/ld2412/number/gate_threshold_number.cpp b/esphome/components/ld2412/number/gate_threshold_number.cpp index 47f8cd9107..8d12bad115 100644 --- a/esphome/components/ld2412/number/gate_threshold_number.cpp +++ b/esphome/components/ld2412/number/gate_threshold_number.cpp @@ -1,7 +1,6 @@ #include "gate_threshold_number.h" -namespace esphome { -namespace ld2412 { +namespace esphome::ld2412 { GateThresholdNumber::GateThresholdNumber(uint8_t gate) : gate_(gate) {} @@ -10,5 +9,4 @@ void GateThresholdNumber::control(float value) { this->parent_->set_gate_threshold(); } -} // namespace ld2412 -} // namespace esphome +} // namespace esphome::ld2412 diff --git a/esphome/components/ld2412/number/gate_threshold_number.h b/esphome/components/ld2412/number/gate_threshold_number.h index 61d9945a0a..78c2e54d82 100644 --- a/esphome/components/ld2412/number/gate_threshold_number.h +++ b/esphome/components/ld2412/number/gate_threshold_number.h @@ -3,8 +3,7 @@ #include "esphome/components/number/number.h" #include "../ld2412.h" -namespace esphome { -namespace ld2412 { +namespace esphome::ld2412 { class GateThresholdNumber : public number::Number, public Parented { public: @@ -15,5 +14,4 @@ class GateThresholdNumber : public number::Number, public Parentedpublish_state(value); this->parent_->set_light_out_control(); } -} // namespace ld2412 -} // namespace esphome +} // namespace esphome::ld2412 diff --git a/esphome/components/ld2412/number/light_threshold_number.h b/esphome/components/ld2412/number/light_threshold_number.h index d8727d3c98..81fd73111c 100644 --- a/esphome/components/ld2412/number/light_threshold_number.h +++ b/esphome/components/ld2412/number/light_threshold_number.h @@ -3,8 +3,7 @@ #include "esphome/components/number/number.h" #include "../ld2412.h" -namespace esphome { -namespace ld2412 { +namespace esphome::ld2412 { class LightThresholdNumber : public number::Number, public Parented { public: @@ -14,5 +13,4 @@ class LightThresholdNumber : public number::Number, public Parentedpublish_state(value); this->parent_->set_basic_config(); } -} // namespace ld2412 -} // namespace esphome +} // namespace esphome::ld2412 diff --git a/esphome/components/ld2412/number/max_distance_timeout_number.h b/esphome/components/ld2412/number/max_distance_timeout_number.h index af0dcf68c5..c1e947fa19 100644 --- a/esphome/components/ld2412/number/max_distance_timeout_number.h +++ b/esphome/components/ld2412/number/max_distance_timeout_number.h @@ -3,8 +3,7 @@ #include "esphome/components/number/number.h" #include "../ld2412.h" -namespace esphome { -namespace ld2412 { +namespace esphome::ld2412 { class MaxDistanceTimeoutNumber : public number::Number, public Parented { public: @@ -14,5 +13,4 @@ class MaxDistanceTimeoutNumber : public number::Number, public Parentedpublish_state(index); this->parent_->set_baud_rate(this->option_at(index)); } -} // namespace ld2412 -} // namespace esphome +} // namespace esphome::ld2412 diff --git a/esphome/components/ld2412/select/baud_rate_select.h b/esphome/components/ld2412/select/baud_rate_select.h index ffe0329341..4666dd2fa0 100644 --- a/esphome/components/ld2412/select/baud_rate_select.h +++ b/esphome/components/ld2412/select/baud_rate_select.h @@ -3,8 +3,7 @@ #include "esphome/components/select/select.h" #include "../ld2412.h" -namespace esphome { -namespace ld2412 { +namespace esphome::ld2412 { class BaudRateSelect : public select::Select, public Parented { public: @@ -14,5 +13,4 @@ class BaudRateSelect : public select::Select, public Parented { void control(size_t index) override; }; -} // namespace ld2412 -} // namespace esphome +} // namespace esphome::ld2412 diff --git a/esphome/components/ld2412/select/distance_resolution_select.cpp b/esphome/components/ld2412/select/distance_resolution_select.cpp index 5a6f46a071..95b80f87fb 100644 --- a/esphome/components/ld2412/select/distance_resolution_select.cpp +++ b/esphome/components/ld2412/select/distance_resolution_select.cpp @@ -1,12 +1,10 @@ #include "distance_resolution_select.h" -namespace esphome { -namespace ld2412 { +namespace esphome::ld2412 { void DistanceResolutionSelect::control(size_t index) { this->publish_state(index); this->parent_->set_distance_resolution(this->option_at(index)); } -} // namespace ld2412 -} // namespace esphome +} // namespace esphome::ld2412 diff --git a/esphome/components/ld2412/select/distance_resolution_select.h b/esphome/components/ld2412/select/distance_resolution_select.h index 842f63b7b1..d3b7fad2f9 100644 --- a/esphome/components/ld2412/select/distance_resolution_select.h +++ b/esphome/components/ld2412/select/distance_resolution_select.h @@ -3,8 +3,7 @@ #include "esphome/components/select/select.h" #include "../ld2412.h" -namespace esphome { -namespace ld2412 { +namespace esphome::ld2412 { class DistanceResolutionSelect : public select::Select, public Parented { public: @@ -14,5 +13,4 @@ class DistanceResolutionSelect : public select::Select, public Parentedpublish_state(index); this->parent_->set_light_out_control(); } -} // namespace ld2412 -} // namespace esphome +} // namespace esphome::ld2412 diff --git a/esphome/components/ld2412/select/light_out_control_select.h b/esphome/components/ld2412/select/light_out_control_select.h index 7a50970d0d..9f86189878 100644 --- a/esphome/components/ld2412/select/light_out_control_select.h +++ b/esphome/components/ld2412/select/light_out_control_select.h @@ -3,8 +3,7 @@ #include "esphome/components/select/select.h" #include "../ld2412.h" -namespace esphome { -namespace ld2412 { +namespace esphome::ld2412 { class LightOutControlSelect : public select::Select, public Parented { public: @@ -14,5 +13,4 @@ class LightOutControlSelect : public select::Select, public Parentedpublish_state(state); this->parent_->set_bluetooth(state); } -} // namespace ld2412 -} // namespace esphome +} // namespace esphome::ld2412 diff --git a/esphome/components/ld2412/switch/bluetooth_switch.h b/esphome/components/ld2412/switch/bluetooth_switch.h index 730d338d87..0c0d1fa550 100644 --- a/esphome/components/ld2412/switch/bluetooth_switch.h +++ b/esphome/components/ld2412/switch/bluetooth_switch.h @@ -3,8 +3,7 @@ #include "esphome/components/switch/switch.h" #include "../ld2412.h" -namespace esphome { -namespace ld2412 { +namespace esphome::ld2412 { class BluetoothSwitch : public switch_::Switch, public Parented { public: @@ -14,5 +13,4 @@ class BluetoothSwitch : public switch_::Switch, public Parented void write_state(bool state) override; }; -} // namespace ld2412 -} // namespace esphome +} // namespace esphome::ld2412 diff --git a/esphome/components/ld2412/switch/engineering_mode_switch.cpp b/esphome/components/ld2412/switch/engineering_mode_switch.cpp index 29ca0c22a8..28b4e5d9e6 100644 --- a/esphome/components/ld2412/switch/engineering_mode_switch.cpp +++ b/esphome/components/ld2412/switch/engineering_mode_switch.cpp @@ -1,12 +1,10 @@ #include "engineering_mode_switch.h" -namespace esphome { -namespace ld2412 { +namespace esphome::ld2412 { void EngineeringModeSwitch::write_state(bool state) { this->publish_state(state); this->parent_->set_engineering_mode(state); } -} // namespace ld2412 -} // namespace esphome +} // namespace esphome::ld2412 diff --git a/esphome/components/ld2412/switch/engineering_mode_switch.h b/esphome/components/ld2412/switch/engineering_mode_switch.h index aaa404c673..4e75a8a185 100644 --- a/esphome/components/ld2412/switch/engineering_mode_switch.h +++ b/esphome/components/ld2412/switch/engineering_mode_switch.h @@ -3,8 +3,7 @@ #include "esphome/components/switch/switch.h" #include "../ld2412.h" -namespace esphome { -namespace ld2412 { +namespace esphome::ld2412 { class EngineeringModeSwitch : public switch_::Switch, public Parented { public: @@ -14,5 +13,4 @@ class EngineeringModeSwitch : public switch_::Switch, public Parentedpresence_bsensor_); } -} // namespace ld2420 -} // namespace esphome +} // namespace esphome::ld2420 diff --git a/esphome/components/ld2420/binary_sensor/ld2420_binary_sensor.h b/esphome/components/ld2420/binary_sensor/ld2420_binary_sensor.h index ee06439090..ec52312f92 100644 --- a/esphome/components/ld2420/binary_sensor/ld2420_binary_sensor.h +++ b/esphome/components/ld2420/binary_sensor/ld2420_binary_sensor.h @@ -3,8 +3,7 @@ #include "../ld2420.h" #include "esphome/components/binary_sensor/binary_sensor.h" -namespace esphome { -namespace ld2420 { +namespace esphome::ld2420 { class LD2420BinarySensor : public LD2420Listener, public Component, binary_sensor::BinarySensor { public: @@ -21,5 +20,4 @@ class LD2420BinarySensor : public LD2420Listener, public Component, binary_senso binary_sensor::BinarySensor *presence_bsensor_{nullptr}; }; -} // namespace ld2420 -} // namespace esphome +} // namespace esphome::ld2420 diff --git a/esphome/components/ld2420/button/reconfig_buttons.cpp b/esphome/components/ld2420/button/reconfig_buttons.cpp index fb8ec2b5a6..1e748e59b8 100644 --- a/esphome/components/ld2420/button/reconfig_buttons.cpp +++ b/esphome/components/ld2420/button/reconfig_buttons.cpp @@ -4,13 +4,11 @@ static const char *const TAG = "ld2420.button"; -namespace esphome { -namespace ld2420 { +namespace esphome::ld2420 { void LD2420ApplyConfigButton::press_action() { this->parent_->apply_config_action(); } void LD2420RevertConfigButton::press_action() { this->parent_->revert_config_action(); } void LD2420RestartModuleButton::press_action() { this->parent_->restart_module_action(); } void LD2420FactoryResetButton::press_action() { this->parent_->factory_reset_action(); } -} // namespace ld2420 -} // namespace esphome +} // namespace esphome::ld2420 diff --git a/esphome/components/ld2420/button/reconfig_buttons.h b/esphome/components/ld2420/button/reconfig_buttons.h index 4e9e7a3692..72171ef386 100644 --- a/esphome/components/ld2420/button/reconfig_buttons.h +++ b/esphome/components/ld2420/button/reconfig_buttons.h @@ -3,8 +3,7 @@ #include "esphome/components/button/button.h" #include "../ld2420.h" -namespace esphome { -namespace ld2420 { +namespace esphome::ld2420 { class LD2420ApplyConfigButton : public button::Button, public Parented { public: @@ -38,5 +37,4 @@ class LD2420FactoryResetButton : public button::Button, public Parented listeners_{}; }; -} // namespace ld2420 -} // namespace esphome +} // namespace esphome::ld2420 diff --git a/esphome/components/ld2420/number/gate_config_number.cpp b/esphome/components/ld2420/number/gate_config_number.cpp index a373753770..998eed2188 100644 --- a/esphome/components/ld2420/number/gate_config_number.cpp +++ b/esphome/components/ld2420/number/gate_config_number.cpp @@ -4,8 +4,7 @@ static const char *const TAG = "ld2420.number"; -namespace esphome { -namespace ld2420 { +namespace esphome::ld2420 { void LD2420TimeoutNumber::control(float timeout) { this->publish_state(timeout); @@ -69,5 +68,4 @@ void LD2420StillThresholdNumbers::control(float still_threshold) { } } -} // namespace ld2420 -} // namespace esphome +} // namespace esphome::ld2420 diff --git a/esphome/components/ld2420/number/gate_config_number.h b/esphome/components/ld2420/number/gate_config_number.h index 459a8026e3..8a8b9c61b1 100644 --- a/esphome/components/ld2420/number/gate_config_number.h +++ b/esphome/components/ld2420/number/gate_config_number.h @@ -3,8 +3,7 @@ #include "esphome/components/number/number.h" #include "../ld2420.h" -namespace esphome { -namespace ld2420 { +namespace esphome::ld2420 { class LD2420TimeoutNumber : public number::Number, public Parented { public: @@ -74,5 +73,4 @@ class LD2420MoveThresholdNumbers : public number::Number, public Parentedparent_->set_operating_mode(this->option_at(index)); } -} // namespace ld2420 -} // namespace esphome +} // namespace esphome::ld2420 diff --git a/esphome/components/ld2420/select/operating_mode_select.h b/esphome/components/ld2420/select/operating_mode_select.h index f59eb33432..c1b8e0b11b 100644 --- a/esphome/components/ld2420/select/operating_mode_select.h +++ b/esphome/components/ld2420/select/operating_mode_select.h @@ -3,8 +3,7 @@ #include "../ld2420.h" #include "esphome/components/select/select.h" -namespace esphome { -namespace ld2420 { +namespace esphome::ld2420 { class LD2420Select : public Component, public select::Select, public Parented { public: @@ -14,5 +13,4 @@ class LD2420Select : public Component, public select::Select, public Parenteddistance_sensor_); } -} // namespace ld2420 -} // namespace esphome +} // namespace esphome::ld2420 diff --git a/esphome/components/ld2420/sensor/ld2420_sensor.h b/esphome/components/ld2420/sensor/ld2420_sensor.h index 82730d60e3..4849cfa047 100644 --- a/esphome/components/ld2420/sensor/ld2420_sensor.h +++ b/esphome/components/ld2420/sensor/ld2420_sensor.h @@ -3,8 +3,7 @@ #include "../ld2420.h" #include "esphome/components/sensor/sensor.h" -namespace esphome { -namespace ld2420 { +namespace esphome::ld2420 { class LD2420Sensor : public LD2420Listener, public Component, sensor::Sensor { public: @@ -30,5 +29,4 @@ class LD2420Sensor : public LD2420Listener, public Component, sensor::Sensor { std::vector energy_sensors_ = std::vector(TOTAL_GATES); }; -} // namespace ld2420 -} // namespace esphome +} // namespace esphome::ld2420 diff --git a/esphome/components/ld2420/text_sensor/ld2420_text_sensor.cpp b/esphome/components/ld2420/text_sensor/ld2420_text_sensor.cpp index f647a36936..f7b016c9d9 100644 --- a/esphome/components/ld2420/text_sensor/ld2420_text_sensor.cpp +++ b/esphome/components/ld2420/text_sensor/ld2420_text_sensor.cpp @@ -2,8 +2,7 @@ #include "esphome/core/helpers.h" #include "esphome/core/log.h" -namespace esphome { -namespace ld2420 { +namespace esphome::ld2420 { static const char *const TAG = "ld2420.text_sensor"; @@ -12,5 +11,4 @@ void LD2420TextSensor::dump_config() { LOG_TEXT_SENSOR(" ", "Firmware", this->fw_version_text_sensor_); } -} // namespace ld2420 -} // namespace esphome +} // namespace esphome::ld2420 diff --git a/esphome/components/ld2420/text_sensor/ld2420_text_sensor.h b/esphome/components/ld2420/text_sensor/ld2420_text_sensor.h index 073ddd5d0f..1932eaaf69 100644 --- a/esphome/components/ld2420/text_sensor/ld2420_text_sensor.h +++ b/esphome/components/ld2420/text_sensor/ld2420_text_sensor.h @@ -3,8 +3,7 @@ #include "../ld2420.h" #include "esphome/components/text_sensor/text_sensor.h" -namespace esphome { -namespace ld2420 { +namespace esphome::ld2420 { class LD2420TextSensor : public LD2420Listener, public Component, text_sensor::TextSensor { public: @@ -20,5 +19,4 @@ class LD2420TextSensor : public LD2420Listener, public Component, text_sensor::T text_sensor::TextSensor *fw_version_text_sensor_{nullptr}; }; -} // namespace ld2420 -} // namespace esphome +} // namespace esphome::ld2420 diff --git a/esphome/components/ld2450/button/factory_reset_button.cpp b/esphome/components/ld2450/button/factory_reset_button.cpp index bcac7ada2f..7a8eb5b0dd 100644 --- a/esphome/components/ld2450/button/factory_reset_button.cpp +++ b/esphome/components/ld2450/button/factory_reset_button.cpp @@ -1,9 +1,7 @@ #include "factory_reset_button.h" -namespace esphome { -namespace ld2450 { +namespace esphome::ld2450 { void FactoryResetButton::press_action() { this->parent_->factory_reset(); } -} // namespace ld2450 -} // namespace esphome +} // namespace esphome::ld2450 diff --git a/esphome/components/ld2450/button/factory_reset_button.h b/esphome/components/ld2450/button/factory_reset_button.h index 8e80347119..392fc67ffd 100644 --- a/esphome/components/ld2450/button/factory_reset_button.h +++ b/esphome/components/ld2450/button/factory_reset_button.h @@ -3,8 +3,7 @@ #include "esphome/components/button/button.h" #include "../ld2450.h" -namespace esphome { -namespace ld2450 { +namespace esphome::ld2450 { class FactoryResetButton : public button::Button, public Parented { public: @@ -14,5 +13,4 @@ class FactoryResetButton : public button::Button, public Parentedparent_->restart_and_read_all_info(); } -} // namespace ld2450 -} // namespace esphome +} // namespace esphome::ld2450 diff --git a/esphome/components/ld2450/button/restart_button.h b/esphome/components/ld2450/button/restart_button.h index a44ae5a4d2..9219011f8b 100644 --- a/esphome/components/ld2450/button/restart_button.h +++ b/esphome/components/ld2450/button/restart_button.h @@ -3,8 +3,7 @@ #include "esphome/components/button/button.h" #include "../ld2450.h" -namespace esphome { -namespace ld2450 { +namespace esphome::ld2450 { class RestartButton : public button::Button, public Parented { public: @@ -14,5 +13,4 @@ class RestartButton : public button::Button, public Parented { void press_action() override; }; -} // namespace ld2450 -} // namespace esphome +} // namespace esphome::ld2450 diff --git a/esphome/components/ld2450/ld2450.cpp b/esphome/components/ld2450/ld2450.cpp index 8e5287aec7..e69ef31d4f 100644 --- a/esphome/components/ld2450/ld2450.cpp +++ b/esphome/components/ld2450/ld2450.cpp @@ -13,8 +13,7 @@ #include #include -namespace esphome { -namespace ld2450 { +namespace esphome::ld2450 { static const char *const TAG = "ld2450"; @@ -939,5 +938,4 @@ float LD2450Component::restore_from_flash_() { } #endif -} // namespace ld2450 -} // namespace esphome +} // namespace esphome::ld2450 diff --git a/esphome/components/ld2450/ld2450.h b/esphome/components/ld2450/ld2450.h index 44b63be444..b94c3cac37 100644 --- a/esphome/components/ld2450/ld2450.h +++ b/esphome/components/ld2450/ld2450.h @@ -31,8 +31,7 @@ #include -namespace esphome { -namespace ld2450 { +namespace esphome::ld2450 { using namespace ld24xx; @@ -193,5 +192,4 @@ class LD2450Component : public Component, public uart::UARTDevice { #endif }; -} // namespace ld2450 -} // namespace esphome +} // namespace esphome::ld2450 diff --git a/esphome/components/ld2450/number/presence_timeout_number.cpp b/esphome/components/ld2450/number/presence_timeout_number.cpp index ecfe71f484..19a1ada0d7 100644 --- a/esphome/components/ld2450/number/presence_timeout_number.cpp +++ b/esphome/components/ld2450/number/presence_timeout_number.cpp @@ -1,12 +1,10 @@ #include "presence_timeout_number.h" -namespace esphome { -namespace ld2450 { +namespace esphome::ld2450 { void PresenceTimeoutNumber::control(float value) { this->publish_state(value); this->parent_->set_presence_timeout(); } -} // namespace ld2450 -} // namespace esphome +} // namespace esphome::ld2450 diff --git a/esphome/components/ld2450/number/presence_timeout_number.h b/esphome/components/ld2450/number/presence_timeout_number.h index b18699792f..09c8afca55 100644 --- a/esphome/components/ld2450/number/presence_timeout_number.h +++ b/esphome/components/ld2450/number/presence_timeout_number.h @@ -3,8 +3,7 @@ #include "esphome/components/number/number.h" #include "../ld2450.h" -namespace esphome { -namespace ld2450 { +namespace esphome::ld2450 { class PresenceTimeoutNumber : public number::Number, public Parented { public: @@ -14,5 +13,4 @@ class PresenceTimeoutNumber : public number::Number, public Parentedparent_->set_zone_coordinate(this->zone_); } -} // namespace ld2450 -} // namespace esphome +} // namespace esphome::ld2450 diff --git a/esphome/components/ld2450/number/zone_coordinate_number.h b/esphome/components/ld2450/number/zone_coordinate_number.h index 72b83889c4..f5a389d712 100644 --- a/esphome/components/ld2450/number/zone_coordinate_number.h +++ b/esphome/components/ld2450/number/zone_coordinate_number.h @@ -3,8 +3,7 @@ #include "esphome/components/number/number.h" #include "../ld2450.h" -namespace esphome { -namespace ld2450 { +namespace esphome::ld2450 { class ZoneCoordinateNumber : public number::Number, public Parented { public: @@ -15,5 +14,4 @@ class ZoneCoordinateNumber : public number::Number, public Parentedpublish_state(index); this->parent_->set_baud_rate(this->option_at(index)); } -} // namespace ld2450 -} // namespace esphome +} // namespace esphome::ld2450 diff --git a/esphome/components/ld2450/select/baud_rate_select.h b/esphome/components/ld2450/select/baud_rate_select.h index 22810d5f13..cb53118170 100644 --- a/esphome/components/ld2450/select/baud_rate_select.h +++ b/esphome/components/ld2450/select/baud_rate_select.h @@ -3,8 +3,7 @@ #include "esphome/components/select/select.h" #include "../ld2450.h" -namespace esphome { -namespace ld2450 { +namespace esphome::ld2450 { class BaudRateSelect : public select::Select, public Parented { public: @@ -14,5 +13,4 @@ class BaudRateSelect : public select::Select, public Parented { void control(size_t index) override; }; -} // namespace ld2450 -} // namespace esphome +} // namespace esphome::ld2450 diff --git a/esphome/components/ld2450/select/zone_type_select.cpp b/esphome/components/ld2450/select/zone_type_select.cpp index 1111428c7c..39642b99ad 100644 --- a/esphome/components/ld2450/select/zone_type_select.cpp +++ b/esphome/components/ld2450/select/zone_type_select.cpp @@ -1,12 +1,10 @@ #include "zone_type_select.h" -namespace esphome { -namespace ld2450 { +namespace esphome::ld2450 { void ZoneTypeSelect::control(size_t index) { this->publish_state(index); this->parent_->set_zone_type(this->option_at(index)); } -} // namespace ld2450 -} // namespace esphome +} // namespace esphome::ld2450 diff --git a/esphome/components/ld2450/select/zone_type_select.h b/esphome/components/ld2450/select/zone_type_select.h index fc95ec1021..566346eb48 100644 --- a/esphome/components/ld2450/select/zone_type_select.h +++ b/esphome/components/ld2450/select/zone_type_select.h @@ -3,8 +3,7 @@ #include "esphome/components/select/select.h" #include "../ld2450.h" -namespace esphome { -namespace ld2450 { +namespace esphome::ld2450 { class ZoneTypeSelect : public select::Select, public Parented { public: @@ -14,5 +13,4 @@ class ZoneTypeSelect : public select::Select, public Parented { void control(size_t index) override; }; -} // namespace ld2450 -} // namespace esphome +} // namespace esphome::ld2450 diff --git a/esphome/components/ld2450/switch/bluetooth_switch.cpp b/esphome/components/ld2450/switch/bluetooth_switch.cpp index fa0d4fb06a..0e19a3e6c6 100644 --- a/esphome/components/ld2450/switch/bluetooth_switch.cpp +++ b/esphome/components/ld2450/switch/bluetooth_switch.cpp @@ -1,12 +1,10 @@ #include "bluetooth_switch.h" -namespace esphome { -namespace ld2450 { +namespace esphome::ld2450 { void BluetoothSwitch::write_state(bool state) { this->publish_state(state); this->parent_->set_bluetooth(state); } -} // namespace ld2450 -} // namespace esphome +} // namespace esphome::ld2450 diff --git a/esphome/components/ld2450/switch/bluetooth_switch.h b/esphome/components/ld2450/switch/bluetooth_switch.h index 3c1c4f755c..3d48a89b57 100644 --- a/esphome/components/ld2450/switch/bluetooth_switch.h +++ b/esphome/components/ld2450/switch/bluetooth_switch.h @@ -3,8 +3,7 @@ #include "esphome/components/switch/switch.h" #include "../ld2450.h" -namespace esphome { -namespace ld2450 { +namespace esphome::ld2450 { class BluetoothSwitch : public switch_::Switch, public Parented { public: @@ -14,5 +13,4 @@ class BluetoothSwitch : public switch_::Switch, public Parented void write_state(bool state) override; }; -} // namespace ld2450 -} // namespace esphome +} // namespace esphome::ld2450 diff --git a/esphome/components/ld2450/switch/multi_target_switch.cpp b/esphome/components/ld2450/switch/multi_target_switch.cpp index a163e29fc5..0b1cb04a68 100644 --- a/esphome/components/ld2450/switch/multi_target_switch.cpp +++ b/esphome/components/ld2450/switch/multi_target_switch.cpp @@ -1,12 +1,10 @@ #include "multi_target_switch.h" -namespace esphome { -namespace ld2450 { +namespace esphome::ld2450 { void MultiTargetSwitch::write_state(bool state) { this->publish_state(state); this->parent_->set_multi_target(state); } -} // namespace ld2450 -} // namespace esphome +} // namespace esphome::ld2450 diff --git a/esphome/components/ld2450/switch/multi_target_switch.h b/esphome/components/ld2450/switch/multi_target_switch.h index ca6253588d..739f308cce 100644 --- a/esphome/components/ld2450/switch/multi_target_switch.h +++ b/esphome/components/ld2450/switch/multi_target_switch.h @@ -3,8 +3,7 @@ #include "esphome/components/switch/switch.h" #include "../ld2450.h" -namespace esphome { -namespace ld2450 { +namespace esphome::ld2450 { class MultiTargetSwitch : public switch_::Switch, public Parented { public: @@ -14,5 +13,4 @@ class MultiTargetSwitch : public switch_::Switch, public Parented> 8) #define lowbyte(val) (uint8_t)((val) &0xff) -namespace esphome { -namespace ld24xx { +namespace esphome::ld24xx { static const char *const UNKNOWN_MAC = "unknown"; static const char *const VERSION_FMT = "%u.%02X.%02X%02X%02X%02X"; @@ -83,5 +82,4 @@ template class SensorWithDedup { Deduplicator publish_dedup; }; #endif -} // namespace ld24xx -} // namespace esphome +} // namespace esphome::ld24xx From 45c994e4de71c99bc0fb8051c8ca9ea484348ee8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 18 Nov 2025 21:56:23 -0600 Subject: [PATCH 139/415] [light] Modernize namespace declarations to C++17 syntax (#11986) --- esphome/components/light/addressable_light.cpp | 6 ++---- esphome/components/light/addressable_light.h | 6 ++---- esphome/components/light/addressable_light_effect.h | 6 ++---- esphome/components/light/addressable_light_wrapper.h | 6 ++---- esphome/components/light/automation.cpp | 6 ++---- esphome/components/light/automation.h | 6 ++---- esphome/components/light/base_light_effects.h | 6 ++---- esphome/components/light/color_mode.h | 6 ++---- esphome/components/light/esp_color_correction.cpp | 6 ++---- esphome/components/light/esp_color_correction.h | 6 ++---- esphome/components/light/esp_color_view.h | 6 ++---- esphome/components/light/esp_hsv_color.cpp | 6 ++---- esphome/components/light/esp_hsv_color.h | 6 ++---- esphome/components/light/esp_range_view.cpp | 6 ++---- esphome/components/light/esp_range_view.h | 6 ++---- esphome/components/light/light_call.cpp | 6 ++---- esphome/components/light/light_color_values.h | 6 ++---- esphome/components/light/light_effect.cpp | 6 ++---- esphome/components/light/light_effect.h | 6 ++---- esphome/components/light/light_json_schema.cpp | 6 ++---- esphome/components/light/light_json_schema.h | 6 ++---- esphome/components/light/light_output.cpp | 6 ++---- esphome/components/light/light_output.h | 6 ++---- esphome/components/light/light_state.cpp | 6 ++---- esphome/components/light/light_state.h | 6 ++---- esphome/components/light/light_transformer.h | 6 ++---- esphome/components/light/transformers.h | 6 ++---- 27 files changed, 54 insertions(+), 108 deletions(-) diff --git a/esphome/components/light/addressable_light.cpp b/esphome/components/light/addressable_light.cpp index 5cbdcb0e86..2f6ffc9a38 100644 --- a/esphome/components/light/addressable_light.cpp +++ b/esphome/components/light/addressable_light.cpp @@ -1,8 +1,7 @@ #include "addressable_light.h" #include "esphome/core/log.h" -namespace esphome { -namespace light { +namespace esphome::light { static const char *const TAG = "light.addressable"; @@ -112,5 +111,4 @@ optional AddressableLightTransformer::apply() { return {}; } -} // namespace light -} // namespace esphome +} // namespace esphome::light diff --git a/esphome/components/light/addressable_light.h b/esphome/components/light/addressable_light.h index 393cc679bc..2e4b984ce4 100644 --- a/esphome/components/light/addressable_light.h +++ b/esphome/components/light/addressable_light.h @@ -14,8 +14,7 @@ #include "esphome/components/power_supply/power_supply.h" #endif -namespace esphome { -namespace light { +namespace esphome::light { /// Convert the color information from a `LightColorValues` object to a `Color` object (does not apply brightness). Color color_from_light_color_values(LightColorValues val); @@ -116,5 +115,4 @@ class AddressableLightTransformer : public LightTransformer { Color target_color_{}; }; -} // namespace light -} // namespace esphome +} // namespace esphome::light diff --git a/esphome/components/light/addressable_light_effect.h b/esphome/components/light/addressable_light_effect.h index 0847db3770..a85ea4661d 100644 --- a/esphome/components/light/addressable_light_effect.h +++ b/esphome/components/light/addressable_light_effect.h @@ -7,8 +7,7 @@ #include "esphome/components/light/light_state.h" #include "esphome/components/light/addressable_light.h" -namespace esphome { -namespace light { +namespace esphome::light { inline static int16_t sin16_c(uint16_t theta) { static const uint16_t BASE[] = {0, 6393, 12539, 18204, 23170, 27245, 30273, 32137}; @@ -371,5 +370,4 @@ class AddressableFlickerEffect : public AddressableLightEffect { uint8_t intensity_{13}; }; -} // namespace light -} // namespace esphome +} // namespace esphome::light diff --git a/esphome/components/light/addressable_light_wrapper.h b/esphome/components/light/addressable_light_wrapper.h index d358502430..8665e62a79 100644 --- a/esphome/components/light/addressable_light_wrapper.h +++ b/esphome/components/light/addressable_light_wrapper.h @@ -3,8 +3,7 @@ #include "esphome/core/component.h" #include "addressable_light.h" -namespace esphome { -namespace light { +namespace esphome::light { class AddressableLightWrapper : public light::AddressableLight { public: @@ -123,5 +122,4 @@ class AddressableLightWrapper : public light::AddressableLight { ColorMode color_mode_{ColorMode::UNKNOWN}; }; -} // namespace light -} // namespace esphome +} // namespace esphome::light diff --git a/esphome/components/light/automation.cpp b/esphome/components/light/automation.cpp index 8c1785f061..ddac2f9341 100644 --- a/esphome/components/light/automation.cpp +++ b/esphome/components/light/automation.cpp @@ -1,8 +1,7 @@ #include "automation.h" #include "esphome/core/log.h" -namespace esphome { -namespace light { +namespace esphome::light { static const char *const TAG = "light.automation"; @@ -11,5 +10,4 @@ void addressableset_warn_about_scale(const char *field) { field); } -} // namespace light -} // namespace esphome +} // namespace esphome::light diff --git a/esphome/components/light/automation.h b/esphome/components/light/automation.h index 8899db8bba..9893c15e0c 100644 --- a/esphome/components/light/automation.h +++ b/esphome/components/light/automation.h @@ -4,8 +4,7 @@ #include "light_state.h" #include "addressable_light.h" -namespace esphome { -namespace light { +namespace esphome::light { enum class LimitMode { CLAMP, DO_NOTHING }; @@ -216,5 +215,4 @@ template class AddressableSet : public Action { } }; -} // namespace light -} // namespace esphome +} // namespace esphome::light diff --git a/esphome/components/light/base_light_effects.h b/esphome/components/light/base_light_effects.h index 515afc5c59..2eeae574e7 100644 --- a/esphome/components/light/base_light_effects.h +++ b/esphome/components/light/base_light_effects.h @@ -6,8 +6,7 @@ #include "esphome/core/helpers.h" #include "light_effect.h" -namespace esphome { -namespace light { +namespace esphome::light { inline static float random_cubic_float() { const float r = random_float() * 2.0f - 1.0f; @@ -235,5 +234,4 @@ class FlickerLightEffect : public LightEffect { float alpha_{}; }; -} // namespace light -} // namespace esphome +} // namespace esphome::light diff --git a/esphome/components/light/color_mode.h b/esphome/components/light/color_mode.h index aa3448c145..0750ae250d 100644 --- a/esphome/components/light/color_mode.h +++ b/esphome/components/light/color_mode.h @@ -3,8 +3,7 @@ #include #include "esphome/core/finite_set_mask.h" -namespace esphome { -namespace light { +namespace esphome::light { /// Color capabilities are the various outputs that a light has and that can be independently controlled by the user. enum class ColorCapability : uint8_t { @@ -210,5 +209,4 @@ inline bool has_capability(const ColorModeMask &mask, ColorCapability capability return (mask.get_mask() & CAPABILITY_BITMASKS[capability_to_index(capability)]) != 0; } -} // namespace light -} // namespace esphome +} // namespace esphome::light diff --git a/esphome/components/light/esp_color_correction.cpp b/esphome/components/light/esp_color_correction.cpp index e5e68264cc..1b511a94b2 100644 --- a/esphome/components/light/esp_color_correction.cpp +++ b/esphome/components/light/esp_color_correction.cpp @@ -2,8 +2,7 @@ #include "light_color_values.h" #include "esphome/core/log.h" -namespace esphome { -namespace light { +namespace esphome::light { void ESPColorCorrection::calculate_gamma_table(float gamma) { for (uint16_t i = 0; i < 256; i++) { @@ -23,5 +22,4 @@ void ESPColorCorrection::calculate_gamma_table(float gamma) { } } -} // namespace light -} // namespace esphome +} // namespace esphome::light diff --git a/esphome/components/light/esp_color_correction.h b/esphome/components/light/esp_color_correction.h index 14c065058c..d275e045b7 100644 --- a/esphome/components/light/esp_color_correction.h +++ b/esphome/components/light/esp_color_correction.h @@ -2,8 +2,7 @@ #include "esphome/core/color.h" -namespace esphome { -namespace light { +namespace esphome::light { class ESPColorCorrection { public: @@ -73,5 +72,4 @@ class ESPColorCorrection { uint8_t local_brightness_{255}; }; -} // namespace light -} // namespace esphome +} // namespace esphome::light diff --git a/esphome/components/light/esp_color_view.h b/esphome/components/light/esp_color_view.h index 35117e7dd8..440a23e9c9 100644 --- a/esphome/components/light/esp_color_view.h +++ b/esphome/components/light/esp_color_view.h @@ -4,8 +4,7 @@ #include "esp_hsv_color.h" #include "esp_color_correction.h" -namespace esphome { -namespace light { +namespace esphome::light { class ESPColorSettable { public: @@ -106,5 +105,4 @@ class ESPColorView : public ESPColorSettable { const ESPColorCorrection *color_correction_; }; -} // namespace light -} // namespace esphome +} // namespace esphome::light diff --git a/esphome/components/light/esp_hsv_color.cpp b/esphome/components/light/esp_hsv_color.cpp index 450c2e11ce..07205ea6d0 100644 --- a/esphome/components/light/esp_hsv_color.cpp +++ b/esphome/components/light/esp_hsv_color.cpp @@ -1,7 +1,6 @@ #include "esp_hsv_color.h" -namespace esphome { -namespace light { +namespace esphome::light { Color ESPHSVColor::to_rgb() const { // based on FastLED's hsv rainbow to rgb @@ -70,5 +69,4 @@ Color ESPHSVColor::to_rgb() const { return rgb; } -} // namespace light -} // namespace esphome +} // namespace esphome::light diff --git a/esphome/components/light/esp_hsv_color.h b/esphome/components/light/esp_hsv_color.h index cdde91c71c..4b54039258 100644 --- a/esphome/components/light/esp_hsv_color.h +++ b/esphome/components/light/esp_hsv_color.h @@ -3,8 +3,7 @@ #include "esphome/core/color.h" #include "esphome/core/helpers.h" -namespace esphome { -namespace light { +namespace esphome::light { struct ESPHSVColor { union { @@ -32,5 +31,4 @@ struct ESPHSVColor { Color to_rgb() const; }; -} // namespace light -} // namespace esphome +} // namespace esphome::light diff --git a/esphome/components/light/esp_range_view.cpp b/esphome/components/light/esp_range_view.cpp index e1f0a507bd..58d552031a 100644 --- a/esphome/components/light/esp_range_view.cpp +++ b/esphome/components/light/esp_range_view.cpp @@ -1,8 +1,7 @@ #include "esp_range_view.h" #include "addressable_light.h" -namespace esphome { -namespace light { +namespace esphome::light { int32_t HOT interpret_index(int32_t index, int32_t size) { if (index < 0) @@ -92,5 +91,4 @@ ESPRangeView &ESPRangeView::operator=(const ESPRangeView &rhs) { // NOLINT ESPColorView ESPRangeIterator::operator*() const { return this->range_.parent_->get(this->i_); } -} // namespace light -} // namespace esphome +} // namespace esphome::light diff --git a/esphome/components/light/esp_range_view.h b/esphome/components/light/esp_range_view.h index 07d18af79f..f5e4ebb83f 100644 --- a/esphome/components/light/esp_range_view.h +++ b/esphome/components/light/esp_range_view.h @@ -3,8 +3,7 @@ #include "esp_color_view.h" #include "esp_hsv_color.h" -namespace esphome { -namespace light { +namespace esphome::light { int32_t interpret_index(int32_t index, int32_t size); @@ -76,5 +75,4 @@ class ESPRangeIterator { int32_t i_; }; -} // namespace light -} // namespace esphome +} // namespace esphome::light diff --git a/esphome/components/light/light_call.cpp b/esphome/components/light/light_call.cpp index b15ff84b97..b3bdb16c73 100644 --- a/esphome/components/light/light_call.cpp +++ b/esphome/components/light/light_call.cpp @@ -4,8 +4,7 @@ #include "esphome/core/log.h" #include "esphome/core/optional.h" -namespace esphome { -namespace light { +namespace esphome::light { static const char *const TAG = "light"; @@ -647,5 +646,4 @@ LightCall &LightCall::set_rgbw(float red, float green, float blue, float white) return *this; } -} // namespace light -} // namespace esphome +} // namespace esphome::light diff --git a/esphome/components/light/light_color_values.h b/esphome/components/light/light_color_values.h index 04d7d1e7d8..bedfad2c35 100644 --- a/esphome/components/light/light_color_values.h +++ b/esphome/components/light/light_color_values.h @@ -4,8 +4,7 @@ #include "color_mode.h" #include -namespace esphome { -namespace light { +namespace esphome::light { inline static uint8_t to_uint8_scale(float x) { return static_cast(roundf(x * 255.0f)); } @@ -310,5 +309,4 @@ class LightColorValues { ColorMode color_mode_; }; -} // namespace light -} // namespace esphome +} // namespace esphome::light diff --git a/esphome/components/light/light_effect.cpp b/esphome/components/light/light_effect.cpp index a210b48e5b..81b923f7f9 100644 --- a/esphome/components/light/light_effect.cpp +++ b/esphome/components/light/light_effect.cpp @@ -1,8 +1,7 @@ #include "light_effect.h" #include "light_state.h" -namespace esphome { -namespace light { +namespace esphome::light { uint32_t LightEffect::get_index() const { if (this->state_ == nullptr) { @@ -32,5 +31,4 @@ uint32_t LightEffect::get_index_in_parent_() const { return 0; // Not found } -} // namespace light -} // namespace esphome +} // namespace esphome::light diff --git a/esphome/components/light/light_effect.h b/esphome/components/light/light_effect.h index d4c2dc3582..aa1f6f7899 100644 --- a/esphome/components/light/light_effect.h +++ b/esphome/components/light/light_effect.h @@ -2,8 +2,7 @@ #include "esphome/core/component.h" -namespace esphome { -namespace light { +namespace esphome::light { class LightState; @@ -55,5 +54,4 @@ class LightEffect { uint32_t get_index_in_parent_() const; }; -} // namespace light -} // namespace esphome +} // namespace esphome::light diff --git a/esphome/components/light/light_json_schema.cpp b/esphome/components/light/light_json_schema.cpp index e754c453b5..1c9b92f504 100644 --- a/esphome/components/light/light_json_schema.cpp +++ b/esphome/components/light/light_json_schema.cpp @@ -3,8 +3,7 @@ #ifdef USE_JSON -namespace esphome { -namespace light { +namespace esphome::light { // See https://www.home-assistant.io/integrations/light.mqtt/#json-schema for documentation on the schema @@ -169,7 +168,6 @@ void LightJSONSchema::parse_json(LightState &state, LightCall &call, JsonObject } } -} // namespace light -} // namespace esphome +} // namespace esphome::light #endif diff --git a/esphome/components/light/light_json_schema.h b/esphome/components/light/light_json_schema.h index c92dd7b655..dac81e32e3 100644 --- a/esphome/components/light/light_json_schema.h +++ b/esphome/components/light/light_json_schema.h @@ -8,8 +8,7 @@ #include "light_call.h" #include "light_state.h" -namespace esphome { -namespace light { +namespace esphome::light { class LightJSONSchema { public: @@ -22,7 +21,6 @@ class LightJSONSchema { static void parse_color_json(LightState &state, LightCall &call, JsonObject root); }; -} // namespace light -} // namespace esphome +} // namespace esphome::light #endif diff --git a/esphome/components/light/light_output.cpp b/esphome/components/light/light_output.cpp index e805a0b694..a86e8e5bf1 100644 --- a/esphome/components/light/light_output.cpp +++ b/esphome/components/light/light_output.cpp @@ -1,12 +1,10 @@ #include "light_output.h" #include "transformers.h" -namespace esphome { -namespace light { +namespace esphome::light { std::unique_ptr LightOutput::create_default_transition() { return make_unique(); } -} // namespace light -} // namespace esphome +} // namespace esphome::light diff --git a/esphome/components/light/light_output.h b/esphome/components/light/light_output.h index 73ba0371cd..c82d270be8 100644 --- a/esphome/components/light/light_output.h +++ b/esphome/components/light/light_output.h @@ -5,8 +5,7 @@ #include "light_state.h" #include "light_transformer.h" -namespace esphome { -namespace light { +namespace esphome::light { /// Interface to write LightStates to hardware. class LightOutput { @@ -29,5 +28,4 @@ class LightOutput { virtual void write_state(LightState *state) = 0; }; -} // namespace light -} // namespace esphome +} // namespace esphome::light diff --git a/esphome/components/light/light_state.cpp b/esphome/components/light/light_state.cpp index 4c253ec5a8..36b2af03a5 100644 --- a/esphome/components/light/light_state.cpp +++ b/esphome/components/light/light_state.cpp @@ -5,8 +5,7 @@ #include "light_output.h" #include "transformers.h" -namespace esphome { -namespace light { +namespace esphome::light { static const char *const TAG = "light"; @@ -304,5 +303,4 @@ void LightState::save_remote_values_() { this->rtc_.save(&saved); } -} // namespace light -} // namespace esphome +} // namespace esphome::light diff --git a/esphome/components/light/light_state.h b/esphome/components/light/light_state.h index bf63c0ec27..06519cdc14 100644 --- a/esphome/components/light/light_state.h +++ b/esphome/components/light/light_state.h @@ -15,8 +15,7 @@ #include #include -namespace esphome { -namespace light { +namespace esphome::light { class LightOutput; @@ -298,5 +297,4 @@ class LightState : public EntityBase, public Component { LightRestoreMode restore_mode_; }; -} // namespace light -} // namespace esphome +} // namespace esphome::light diff --git a/esphome/components/light/light_transformer.h b/esphome/components/light/light_transformer.h index a84183c03c..079c2d2ae0 100644 --- a/esphome/components/light/light_transformer.h +++ b/esphome/components/light/light_transformer.h @@ -4,8 +4,7 @@ #include "esphome/core/helpers.h" #include "light_color_values.h" -namespace esphome { -namespace light { +namespace esphome::light { /// Base class for all light color transformers, such as transitions or flashes. class LightTransformer { @@ -59,5 +58,4 @@ class LightTransformer { LightColorValues target_values_; }; -} // namespace light -} // namespace esphome +} // namespace esphome::light diff --git a/esphome/components/light/transformers.h b/esphome/components/light/transformers.h index 71d41a66d3..a26713b723 100644 --- a/esphome/components/light/transformers.h +++ b/esphome/components/light/transformers.h @@ -6,8 +6,7 @@ #include "light_state.h" #include "light_transformer.h" -namespace esphome { -namespace light { +namespace esphome::light { class LightTransitionTransformer : public LightTransformer { public: @@ -118,5 +117,4 @@ class LightFlashTransformer : public LightTransformer { bool begun_lightstate_restore_; }; -} // namespace light -} // namespace esphome +} // namespace esphome::light From b3ef05e5e137fd47ec4f781050af9a571f7689b1 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 18 Nov 2025 22:00:39 -0600 Subject: [PATCH 140/415] [ld24xx] Modernize namespace declarations to C++17 syntax (#11988) --- esphome/components/ld2410/automation.h | 6 ++---- esphome/components/ld2410/button/factory_reset_button.cpp | 6 ++---- esphome/components/ld2410/button/factory_reset_button.h | 6 ++---- esphome/components/ld2410/button/query_button.cpp | 6 ++---- esphome/components/ld2410/button/query_button.h | 6 ++---- esphome/components/ld2410/button/restart_button.cpp | 6 ++---- esphome/components/ld2410/button/restart_button.h | 6 ++---- esphome/components/ld2410/ld2410.cpp | 6 ++---- esphome/components/ld2410/ld2410.h | 6 ++---- esphome/components/ld2410/number/gate_threshold_number.cpp | 6 ++---- esphome/components/ld2410/number/gate_threshold_number.h | 6 ++---- esphome/components/ld2410/number/light_threshold_number.cpp | 6 ++---- esphome/components/ld2410/number/light_threshold_number.h | 6 ++---- .../ld2410/number/max_distance_timeout_number.cpp | 6 ++---- .../components/ld2410/number/max_distance_timeout_number.h | 6 ++---- esphome/components/ld2410/select/baud_rate_select.cpp | 6 ++---- esphome/components/ld2410/select/baud_rate_select.h | 6 ++---- .../components/ld2410/select/distance_resolution_select.cpp | 6 ++---- .../components/ld2410/select/distance_resolution_select.h | 6 ++---- .../components/ld2410/select/light_out_control_select.cpp | 6 ++---- esphome/components/ld2410/select/light_out_control_select.h | 6 ++---- esphome/components/ld2410/switch/bluetooth_switch.cpp | 6 ++---- esphome/components/ld2410/switch/bluetooth_switch.h | 6 ++---- .../components/ld2410/switch/engineering_mode_switch.cpp | 6 ++---- esphome/components/ld2410/switch/engineering_mode_switch.h | 6 ++---- esphome/components/ld2412/button/factory_reset_button.cpp | 6 ++---- esphome/components/ld2412/button/factory_reset_button.h | 6 ++---- esphome/components/ld2412/button/query_button.cpp | 6 ++---- esphome/components/ld2412/button/query_button.h | 6 ++---- esphome/components/ld2412/button/restart_button.cpp | 6 ++---- esphome/components/ld2412/button/restart_button.h | 6 ++---- .../button/start_dynamic_background_correction_button.cpp | 6 ++---- .../button/start_dynamic_background_correction_button.h | 6 ++---- esphome/components/ld2412/ld2412.cpp | 6 ++---- esphome/components/ld2412/ld2412.h | 6 ++---- esphome/components/ld2412/number/gate_threshold_number.cpp | 6 ++---- esphome/components/ld2412/number/gate_threshold_number.h | 6 ++---- esphome/components/ld2412/number/light_threshold_number.cpp | 6 ++---- esphome/components/ld2412/number/light_threshold_number.h | 6 ++---- .../ld2412/number/max_distance_timeout_number.cpp | 6 ++---- .../components/ld2412/number/max_distance_timeout_number.h | 6 ++---- esphome/components/ld2412/select/baud_rate_select.cpp | 6 ++---- esphome/components/ld2412/select/baud_rate_select.h | 6 ++---- .../components/ld2412/select/distance_resolution_select.cpp | 6 ++---- .../components/ld2412/select/distance_resolution_select.h | 6 ++---- .../components/ld2412/select/light_out_control_select.cpp | 6 ++---- esphome/components/ld2412/select/light_out_control_select.h | 6 ++---- esphome/components/ld2412/switch/bluetooth_switch.cpp | 6 ++---- esphome/components/ld2412/switch/bluetooth_switch.h | 6 ++---- .../components/ld2412/switch/engineering_mode_switch.cpp | 6 ++---- esphome/components/ld2412/switch/engineering_mode_switch.h | 6 ++---- .../ld2420/binary_sensor/ld2420_binary_sensor.cpp | 6 ++---- .../components/ld2420/binary_sensor/ld2420_binary_sensor.h | 6 ++---- esphome/components/ld2420/button/reconfig_buttons.cpp | 6 ++---- esphome/components/ld2420/button/reconfig_buttons.h | 6 ++---- esphome/components/ld2420/ld2420.cpp | 6 ++---- esphome/components/ld2420/ld2420.h | 6 ++---- esphome/components/ld2420/number/gate_config_number.cpp | 6 ++---- esphome/components/ld2420/number/gate_config_number.h | 6 ++---- esphome/components/ld2420/select/operating_mode_select.cpp | 6 ++---- esphome/components/ld2420/select/operating_mode_select.h | 6 ++---- esphome/components/ld2420/sensor/ld2420_sensor.cpp | 6 ++---- esphome/components/ld2420/sensor/ld2420_sensor.h | 6 ++---- .../components/ld2420/text_sensor/ld2420_text_sensor.cpp | 6 ++---- esphome/components/ld2420/text_sensor/ld2420_text_sensor.h | 6 ++---- esphome/components/ld2450/button/factory_reset_button.cpp | 6 ++---- esphome/components/ld2450/button/factory_reset_button.h | 6 ++---- esphome/components/ld2450/button/restart_button.cpp | 6 ++---- esphome/components/ld2450/button/restart_button.h | 6 ++---- esphome/components/ld2450/ld2450.cpp | 6 ++---- esphome/components/ld2450/ld2450.h | 6 ++---- .../components/ld2450/number/presence_timeout_number.cpp | 6 ++---- esphome/components/ld2450/number/presence_timeout_number.h | 6 ++---- esphome/components/ld2450/number/zone_coordinate_number.cpp | 6 ++---- esphome/components/ld2450/number/zone_coordinate_number.h | 6 ++---- esphome/components/ld2450/select/baud_rate_select.cpp | 6 ++---- esphome/components/ld2450/select/baud_rate_select.h | 6 ++---- esphome/components/ld2450/select/zone_type_select.cpp | 6 ++---- esphome/components/ld2450/select/zone_type_select.h | 6 ++---- esphome/components/ld2450/switch/bluetooth_switch.cpp | 6 ++---- esphome/components/ld2450/switch/bluetooth_switch.h | 6 ++---- esphome/components/ld2450/switch/multi_target_switch.cpp | 6 ++---- esphome/components/ld2450/switch/multi_target_switch.h | 6 ++---- esphome/components/ld24xx/ld24xx.h | 6 ++---- 84 files changed, 168 insertions(+), 336 deletions(-) diff --git a/esphome/components/ld2410/automation.h b/esphome/components/ld2410/automation.h index f4f1c197b2..614453b575 100644 --- a/esphome/components/ld2410/automation.h +++ b/esphome/components/ld2410/automation.h @@ -4,8 +4,7 @@ #include "esphome/core/component.h" #include "ld2410.h" -namespace esphome { -namespace ld2410 { +namespace esphome::ld2410 { template class BluetoothPasswordSetAction : public Action { public: @@ -18,5 +17,4 @@ template class BluetoothPasswordSetAction : public Action LD2410Component *ld2410_comp_; }; -} // namespace ld2410 -} // namespace esphome +} // namespace esphome::ld2410 diff --git a/esphome/components/ld2410/button/factory_reset_button.cpp b/esphome/components/ld2410/button/factory_reset_button.cpp index a848b02a9d..0223df7086 100644 --- a/esphome/components/ld2410/button/factory_reset_button.cpp +++ b/esphome/components/ld2410/button/factory_reset_button.cpp @@ -1,9 +1,7 @@ #include "factory_reset_button.h" -namespace esphome { -namespace ld2410 { +namespace esphome::ld2410 { void FactoryResetButton::press_action() { this->parent_->factory_reset(); } -} // namespace ld2410 -} // namespace esphome +} // namespace esphome::ld2410 diff --git a/esphome/components/ld2410/button/factory_reset_button.h b/esphome/components/ld2410/button/factory_reset_button.h index 45bf979033..715a8c4056 100644 --- a/esphome/components/ld2410/button/factory_reset_button.h +++ b/esphome/components/ld2410/button/factory_reset_button.h @@ -3,8 +3,7 @@ #include "esphome/components/button/button.h" #include "../ld2410.h" -namespace esphome { -namespace ld2410 { +namespace esphome::ld2410 { class FactoryResetButton : public button::Button, public Parented { public: @@ -14,5 +13,4 @@ class FactoryResetButton : public button::Button, public Parentedparent_->read_all_info(); } -} // namespace ld2410 -} // namespace esphome +} // namespace esphome::ld2410 diff --git a/esphome/components/ld2410/button/query_button.h b/esphome/components/ld2410/button/query_button.h index c7a47e32d8..7a786901ae 100644 --- a/esphome/components/ld2410/button/query_button.h +++ b/esphome/components/ld2410/button/query_button.h @@ -3,8 +3,7 @@ #include "esphome/components/button/button.h" #include "../ld2410.h" -namespace esphome { -namespace ld2410 { +namespace esphome::ld2410 { class QueryButton : public button::Button, public Parented { public: @@ -14,5 +13,4 @@ class QueryButton : public button::Button, public Parented { void press_action() override; }; -} // namespace ld2410 -} // namespace esphome +} // namespace esphome::ld2410 diff --git a/esphome/components/ld2410/button/restart_button.cpp b/esphome/components/ld2410/button/restart_button.cpp index de0d36c1ef..0d5002d3c6 100644 --- a/esphome/components/ld2410/button/restart_button.cpp +++ b/esphome/components/ld2410/button/restart_button.cpp @@ -1,9 +1,7 @@ #include "restart_button.h" -namespace esphome { -namespace ld2410 { +namespace esphome::ld2410 { void RestartButton::press_action() { this->parent_->restart_and_read_all_info(); } -} // namespace ld2410 -} // namespace esphome +} // namespace esphome::ld2410 diff --git a/esphome/components/ld2410/button/restart_button.h b/esphome/components/ld2410/button/restart_button.h index d00dc05a53..9bf8639a8c 100644 --- a/esphome/components/ld2410/button/restart_button.h +++ b/esphome/components/ld2410/button/restart_button.h @@ -3,8 +3,7 @@ #include "esphome/components/button/button.h" #include "../ld2410.h" -namespace esphome { -namespace ld2410 { +namespace esphome::ld2410 { class RestartButton : public button::Button, public Parented { public: @@ -14,5 +13,4 @@ class RestartButton : public button::Button, public Parented { void press_action() override; }; -} // namespace ld2410 -} // namespace esphome +} // namespace esphome::ld2410 diff --git a/esphome/components/ld2410/ld2410.cpp b/esphome/components/ld2410/ld2410.cpp index 391f2024cd..bb2e4e2f4c 100644 --- a/esphome/components/ld2410/ld2410.cpp +++ b/esphome/components/ld2410/ld2410.cpp @@ -9,8 +9,7 @@ #include "esphome/core/application.h" -namespace esphome { -namespace ld2410 { +namespace esphome::ld2410 { static const char *const TAG = "ld2410"; @@ -782,5 +781,4 @@ void LD2410Component::set_gate_still_sensor(uint8_t gate, sensor::Sensor *s) { } #endif -} // namespace ld2410 -} // namespace esphome +} // namespace esphome::ld2410 diff --git a/esphome/components/ld2410/ld2410.h b/esphome/components/ld2410/ld2410.h index 52cf76b5b6..efe585fb76 100644 --- a/esphome/components/ld2410/ld2410.h +++ b/esphome/components/ld2410/ld2410.h @@ -29,8 +29,7 @@ #include -namespace esphome { -namespace ld2410 { +namespace esphome::ld2410 { using namespace ld24xx; @@ -133,5 +132,4 @@ class LD2410Component : public Component, public uart::UARTDevice { #endif }; -} // namespace ld2410 -} // namespace esphome +} // namespace esphome::ld2410 diff --git a/esphome/components/ld2410/number/gate_threshold_number.cpp b/esphome/components/ld2410/number/gate_threshold_number.cpp index 5d040554d7..65e864a4d7 100644 --- a/esphome/components/ld2410/number/gate_threshold_number.cpp +++ b/esphome/components/ld2410/number/gate_threshold_number.cpp @@ -1,7 +1,6 @@ #include "gate_threshold_number.h" -namespace esphome { -namespace ld2410 { +namespace esphome::ld2410 { GateThresholdNumber::GateThresholdNumber(uint8_t gate) : gate_(gate) {} @@ -10,5 +9,4 @@ void GateThresholdNumber::control(float value) { this->parent_->set_gate_threshold(this->gate_); } -} // namespace ld2410 -} // namespace esphome +} // namespace esphome::ld2410 diff --git a/esphome/components/ld2410/number/gate_threshold_number.h b/esphome/components/ld2410/number/gate_threshold_number.h index 2806ecce63..63491f18d3 100644 --- a/esphome/components/ld2410/number/gate_threshold_number.h +++ b/esphome/components/ld2410/number/gate_threshold_number.h @@ -3,8 +3,7 @@ #include "esphome/components/number/number.h" #include "../ld2410.h" -namespace esphome { -namespace ld2410 { +namespace esphome::ld2410 { class GateThresholdNumber : public number::Number, public Parented { public: @@ -15,5 +14,4 @@ class GateThresholdNumber : public number::Number, public Parentedpublish_state(value); this->parent_->set_light_out_control(); } -} // namespace ld2410 -} // namespace esphome +} // namespace esphome::ld2410 diff --git a/esphome/components/ld2410/number/light_threshold_number.h b/esphome/components/ld2410/number/light_threshold_number.h index 8f014373c0..3c5e433416 100644 --- a/esphome/components/ld2410/number/light_threshold_number.h +++ b/esphome/components/ld2410/number/light_threshold_number.h @@ -3,8 +3,7 @@ #include "esphome/components/number/number.h" #include "../ld2410.h" -namespace esphome { -namespace ld2410 { +namespace esphome::ld2410 { class LightThresholdNumber : public number::Number, public Parented { public: @@ -14,5 +13,4 @@ class LightThresholdNumber : public number::Number, public Parentedpublish_state(value); this->parent_->set_max_distances_timeout(); } -} // namespace ld2410 -} // namespace esphome +} // namespace esphome::ld2410 diff --git a/esphome/components/ld2410/number/max_distance_timeout_number.h b/esphome/components/ld2410/number/max_distance_timeout_number.h index 7d91b4b5fe..35f4cbbfae 100644 --- a/esphome/components/ld2410/number/max_distance_timeout_number.h +++ b/esphome/components/ld2410/number/max_distance_timeout_number.h @@ -3,8 +3,7 @@ #include "esphome/components/number/number.h" #include "../ld2410.h" -namespace esphome { -namespace ld2410 { +namespace esphome::ld2410 { class MaxDistanceTimeoutNumber : public number::Number, public Parented { public: @@ -14,5 +13,4 @@ class MaxDistanceTimeoutNumber : public number::Number, public Parentedpublish_state(index); this->parent_->set_baud_rate(this->option_at(index)); } -} // namespace ld2410 -} // namespace esphome +} // namespace esphome::ld2410 diff --git a/esphome/components/ld2410/select/baud_rate_select.h b/esphome/components/ld2410/select/baud_rate_select.h index 9385c8cf7e..fb1d016b1f 100644 --- a/esphome/components/ld2410/select/baud_rate_select.h +++ b/esphome/components/ld2410/select/baud_rate_select.h @@ -3,8 +3,7 @@ #include "esphome/components/select/select.h" #include "../ld2410.h" -namespace esphome { -namespace ld2410 { +namespace esphome::ld2410 { class BaudRateSelect : public select::Select, public Parented { public: @@ -14,5 +13,4 @@ class BaudRateSelect : public select::Select, public Parented { void control(size_t index) override; }; -} // namespace ld2410 -} // namespace esphome +} // namespace esphome::ld2410 diff --git a/esphome/components/ld2410/select/distance_resolution_select.cpp b/esphome/components/ld2410/select/distance_resolution_select.cpp index 4fc4c5af02..635bf206d3 100644 --- a/esphome/components/ld2410/select/distance_resolution_select.cpp +++ b/esphome/components/ld2410/select/distance_resolution_select.cpp @@ -1,12 +1,10 @@ #include "distance_resolution_select.h" -namespace esphome { -namespace ld2410 { +namespace esphome::ld2410 { void DistanceResolutionSelect::control(size_t index) { this->publish_state(index); this->parent_->set_distance_resolution(this->option_at(index)); } -} // namespace ld2410 -} // namespace esphome +} // namespace esphome::ld2410 diff --git a/esphome/components/ld2410/select/distance_resolution_select.h b/esphome/components/ld2410/select/distance_resolution_select.h index 1a04f843a6..be2389d36e 100644 --- a/esphome/components/ld2410/select/distance_resolution_select.h +++ b/esphome/components/ld2410/select/distance_resolution_select.h @@ -3,8 +3,7 @@ #include "esphome/components/select/select.h" #include "../ld2410.h" -namespace esphome { -namespace ld2410 { +namespace esphome::ld2410 { class DistanceResolutionSelect : public select::Select, public Parented { public: @@ -14,5 +13,4 @@ class DistanceResolutionSelect : public select::Select, public Parentedpublish_state(index); this->parent_->set_light_out_control(); } -} // namespace ld2410 -} // namespace esphome +} // namespace esphome::ld2410 diff --git a/esphome/components/ld2410/select/light_out_control_select.h b/esphome/components/ld2410/select/light_out_control_select.h index e8cd8f1d6a..608c311af4 100644 --- a/esphome/components/ld2410/select/light_out_control_select.h +++ b/esphome/components/ld2410/select/light_out_control_select.h @@ -3,8 +3,7 @@ #include "esphome/components/select/select.h" #include "../ld2410.h" -namespace esphome { -namespace ld2410 { +namespace esphome::ld2410 { class LightOutControlSelect : public select::Select, public Parented { public: @@ -14,5 +13,4 @@ class LightOutControlSelect : public select::Select, public Parentedpublish_state(state); this->parent_->set_bluetooth(state); } -} // namespace ld2410 -} // namespace esphome +} // namespace esphome::ld2410 diff --git a/esphome/components/ld2410/switch/bluetooth_switch.h b/esphome/components/ld2410/switch/bluetooth_switch.h index 35ae1ec0c9..07804e2292 100644 --- a/esphome/components/ld2410/switch/bluetooth_switch.h +++ b/esphome/components/ld2410/switch/bluetooth_switch.h @@ -3,8 +3,7 @@ #include "esphome/components/switch/switch.h" #include "../ld2410.h" -namespace esphome { -namespace ld2410 { +namespace esphome::ld2410 { class BluetoothSwitch : public switch_::Switch, public Parented { public: @@ -14,5 +13,4 @@ class BluetoothSwitch : public switch_::Switch, public Parented void write_state(bool state) override; }; -} // namespace ld2410 -} // namespace esphome +} // namespace esphome::ld2410 diff --git a/esphome/components/ld2410/switch/engineering_mode_switch.cpp b/esphome/components/ld2410/switch/engineering_mode_switch.cpp index 967c87c887..4f2f08b03e 100644 --- a/esphome/components/ld2410/switch/engineering_mode_switch.cpp +++ b/esphome/components/ld2410/switch/engineering_mode_switch.cpp @@ -1,12 +1,10 @@ #include "engineering_mode_switch.h" -namespace esphome { -namespace ld2410 { +namespace esphome::ld2410 { void EngineeringModeSwitch::write_state(bool state) { this->publish_state(state); this->parent_->set_engineering_mode(state); } -} // namespace ld2410 -} // namespace esphome +} // namespace esphome::ld2410 diff --git a/esphome/components/ld2410/switch/engineering_mode_switch.h b/esphome/components/ld2410/switch/engineering_mode_switch.h index e521200cd6..4dd8e16653 100644 --- a/esphome/components/ld2410/switch/engineering_mode_switch.h +++ b/esphome/components/ld2410/switch/engineering_mode_switch.h @@ -3,8 +3,7 @@ #include "esphome/components/switch/switch.h" #include "../ld2410.h" -namespace esphome { -namespace ld2410 { +namespace esphome::ld2410 { class EngineeringModeSwitch : public switch_::Switch, public Parented { public: @@ -14,5 +13,4 @@ class EngineeringModeSwitch : public switch_::Switch, public Parentedparent_->factory_reset(); } -} // namespace ld2412 -} // namespace esphome +} // namespace esphome::ld2412 diff --git a/esphome/components/ld2412/button/factory_reset_button.h b/esphome/components/ld2412/button/factory_reset_button.h index 36a3fffcd5..1ef6b23b80 100644 --- a/esphome/components/ld2412/button/factory_reset_button.h +++ b/esphome/components/ld2412/button/factory_reset_button.h @@ -3,8 +3,7 @@ #include "esphome/components/button/button.h" #include "../ld2412.h" -namespace esphome { -namespace ld2412 { +namespace esphome::ld2412 { class FactoryResetButton : public button::Button, public Parented { public: @@ -14,5 +13,4 @@ class FactoryResetButton : public button::Button, public Parentedparent_->read_all_info(); } -} // namespace ld2412 -} // namespace esphome +} // namespace esphome::ld2412 diff --git a/esphome/components/ld2412/button/query_button.h b/esphome/components/ld2412/button/query_button.h index 595ef6d1e9..373e135802 100644 --- a/esphome/components/ld2412/button/query_button.h +++ b/esphome/components/ld2412/button/query_button.h @@ -3,8 +3,7 @@ #include "esphome/components/button/button.h" #include "../ld2412.h" -namespace esphome { -namespace ld2412 { +namespace esphome::ld2412 { class QueryButton : public button::Button, public Parented { public: @@ -14,5 +13,4 @@ class QueryButton : public button::Button, public Parented { void press_action() override; }; -} // namespace ld2412 -} // namespace esphome +} // namespace esphome::ld2412 diff --git a/esphome/components/ld2412/button/restart_button.cpp b/esphome/components/ld2412/button/restart_button.cpp index aca0d17841..430f6c998f 100644 --- a/esphome/components/ld2412/button/restart_button.cpp +++ b/esphome/components/ld2412/button/restart_button.cpp @@ -1,9 +1,7 @@ #include "restart_button.h" -namespace esphome { -namespace ld2412 { +namespace esphome::ld2412 { void RestartButton::press_action() { this->parent_->restart_and_read_all_info(); } -} // namespace ld2412 -} // namespace esphome +} // namespace esphome::ld2412 diff --git a/esphome/components/ld2412/button/restart_button.h b/esphome/components/ld2412/button/restart_button.h index 5cd582e2a3..80c79f5e7d 100644 --- a/esphome/components/ld2412/button/restart_button.h +++ b/esphome/components/ld2412/button/restart_button.h @@ -3,8 +3,7 @@ #include "esphome/components/button/button.h" #include "../ld2412.h" -namespace esphome { -namespace ld2412 { +namespace esphome::ld2412 { class RestartButton : public button::Button, public Parented { public: @@ -14,5 +13,4 @@ class RestartButton : public button::Button, public Parented { void press_action() override; }; -} // namespace ld2412 -} // namespace esphome +} // namespace esphome::ld2412 diff --git a/esphome/components/ld2412/button/start_dynamic_background_correction_button.cpp b/esphome/components/ld2412/button/start_dynamic_background_correction_button.cpp index 9b37243b82..8ba41a03fb 100644 --- a/esphome/components/ld2412/button/start_dynamic_background_correction_button.cpp +++ b/esphome/components/ld2412/button/start_dynamic_background_correction_button.cpp @@ -2,10 +2,8 @@ #include "restart_button.h" -namespace esphome { -namespace ld2412 { +namespace esphome::ld2412 { void StartDynamicBackgroundCorrectionButton::press_action() { this->parent_->start_dynamic_background_correction(); } -} // namespace ld2412 -} // namespace esphome +} // namespace esphome::ld2412 diff --git a/esphome/components/ld2412/button/start_dynamic_background_correction_button.h b/esphome/components/ld2412/button/start_dynamic_background_correction_button.h index 3af0a8a149..b1f2127896 100644 --- a/esphome/components/ld2412/button/start_dynamic_background_correction_button.h +++ b/esphome/components/ld2412/button/start_dynamic_background_correction_button.h @@ -3,8 +3,7 @@ #include "esphome/components/button/button.h" #include "../ld2412.h" -namespace esphome { -namespace ld2412 { +namespace esphome::ld2412 { class StartDynamicBackgroundCorrectionButton : public button::Button, public Parented { public: @@ -14,5 +13,4 @@ class StartDynamicBackgroundCorrectionButton : public button::Button, public Par void press_action() override; }; -} // namespace ld2412 -} // namespace esphome +} // namespace esphome::ld2412 diff --git a/esphome/components/ld2412/ld2412.cpp b/esphome/components/ld2412/ld2412.cpp index 4f2fd7c2bd..0f6fe62d30 100644 --- a/esphome/components/ld2412/ld2412.cpp +++ b/esphome/components/ld2412/ld2412.cpp @@ -10,8 +10,7 @@ #include "esphome/core/application.h" #include "esphome/core/helpers.h" -namespace esphome { -namespace ld2412 { +namespace esphome::ld2412 { static const char *const TAG = "ld2412"; @@ -855,5 +854,4 @@ void LD2412Component::set_gate_still_sensor(uint8_t gate, sensor::Sensor *s) { } #endif -} // namespace ld2412 -} // namespace esphome +} // namespace esphome::ld2412 diff --git a/esphome/components/ld2412/ld2412.h b/esphome/components/ld2412/ld2412.h index 2bed34bdd8..5dd5e7bcde 100644 --- a/esphome/components/ld2412/ld2412.h +++ b/esphome/components/ld2412/ld2412.h @@ -29,8 +29,7 @@ #include -namespace esphome { -namespace ld2412 { +namespace esphome::ld2412 { using namespace ld24xx; @@ -137,5 +136,4 @@ class LD2412Component : public Component, public uart::UARTDevice { #endif }; -} // namespace ld2412 -} // namespace esphome +} // namespace esphome::ld2412 diff --git a/esphome/components/ld2412/number/gate_threshold_number.cpp b/esphome/components/ld2412/number/gate_threshold_number.cpp index 47f8cd9107..8d12bad115 100644 --- a/esphome/components/ld2412/number/gate_threshold_number.cpp +++ b/esphome/components/ld2412/number/gate_threshold_number.cpp @@ -1,7 +1,6 @@ #include "gate_threshold_number.h" -namespace esphome { -namespace ld2412 { +namespace esphome::ld2412 { GateThresholdNumber::GateThresholdNumber(uint8_t gate) : gate_(gate) {} @@ -10,5 +9,4 @@ void GateThresholdNumber::control(float value) { this->parent_->set_gate_threshold(); } -} // namespace ld2412 -} // namespace esphome +} // namespace esphome::ld2412 diff --git a/esphome/components/ld2412/number/gate_threshold_number.h b/esphome/components/ld2412/number/gate_threshold_number.h index 61d9945a0a..78c2e54d82 100644 --- a/esphome/components/ld2412/number/gate_threshold_number.h +++ b/esphome/components/ld2412/number/gate_threshold_number.h @@ -3,8 +3,7 @@ #include "esphome/components/number/number.h" #include "../ld2412.h" -namespace esphome { -namespace ld2412 { +namespace esphome::ld2412 { class GateThresholdNumber : public number::Number, public Parented { public: @@ -15,5 +14,4 @@ class GateThresholdNumber : public number::Number, public Parentedpublish_state(value); this->parent_->set_light_out_control(); } -} // namespace ld2412 -} // namespace esphome +} // namespace esphome::ld2412 diff --git a/esphome/components/ld2412/number/light_threshold_number.h b/esphome/components/ld2412/number/light_threshold_number.h index d8727d3c98..81fd73111c 100644 --- a/esphome/components/ld2412/number/light_threshold_number.h +++ b/esphome/components/ld2412/number/light_threshold_number.h @@ -3,8 +3,7 @@ #include "esphome/components/number/number.h" #include "../ld2412.h" -namespace esphome { -namespace ld2412 { +namespace esphome::ld2412 { class LightThresholdNumber : public number::Number, public Parented { public: @@ -14,5 +13,4 @@ class LightThresholdNumber : public number::Number, public Parentedpublish_state(value); this->parent_->set_basic_config(); } -} // namespace ld2412 -} // namespace esphome +} // namespace esphome::ld2412 diff --git a/esphome/components/ld2412/number/max_distance_timeout_number.h b/esphome/components/ld2412/number/max_distance_timeout_number.h index af0dcf68c5..c1e947fa19 100644 --- a/esphome/components/ld2412/number/max_distance_timeout_number.h +++ b/esphome/components/ld2412/number/max_distance_timeout_number.h @@ -3,8 +3,7 @@ #include "esphome/components/number/number.h" #include "../ld2412.h" -namespace esphome { -namespace ld2412 { +namespace esphome::ld2412 { class MaxDistanceTimeoutNumber : public number::Number, public Parented { public: @@ -14,5 +13,4 @@ class MaxDistanceTimeoutNumber : public number::Number, public Parentedpublish_state(index); this->parent_->set_baud_rate(this->option_at(index)); } -} // namespace ld2412 -} // namespace esphome +} // namespace esphome::ld2412 diff --git a/esphome/components/ld2412/select/baud_rate_select.h b/esphome/components/ld2412/select/baud_rate_select.h index ffe0329341..4666dd2fa0 100644 --- a/esphome/components/ld2412/select/baud_rate_select.h +++ b/esphome/components/ld2412/select/baud_rate_select.h @@ -3,8 +3,7 @@ #include "esphome/components/select/select.h" #include "../ld2412.h" -namespace esphome { -namespace ld2412 { +namespace esphome::ld2412 { class BaudRateSelect : public select::Select, public Parented { public: @@ -14,5 +13,4 @@ class BaudRateSelect : public select::Select, public Parented { void control(size_t index) override; }; -} // namespace ld2412 -} // namespace esphome +} // namespace esphome::ld2412 diff --git a/esphome/components/ld2412/select/distance_resolution_select.cpp b/esphome/components/ld2412/select/distance_resolution_select.cpp index 5a6f46a071..95b80f87fb 100644 --- a/esphome/components/ld2412/select/distance_resolution_select.cpp +++ b/esphome/components/ld2412/select/distance_resolution_select.cpp @@ -1,12 +1,10 @@ #include "distance_resolution_select.h" -namespace esphome { -namespace ld2412 { +namespace esphome::ld2412 { void DistanceResolutionSelect::control(size_t index) { this->publish_state(index); this->parent_->set_distance_resolution(this->option_at(index)); } -} // namespace ld2412 -} // namespace esphome +} // namespace esphome::ld2412 diff --git a/esphome/components/ld2412/select/distance_resolution_select.h b/esphome/components/ld2412/select/distance_resolution_select.h index 842f63b7b1..d3b7fad2f9 100644 --- a/esphome/components/ld2412/select/distance_resolution_select.h +++ b/esphome/components/ld2412/select/distance_resolution_select.h @@ -3,8 +3,7 @@ #include "esphome/components/select/select.h" #include "../ld2412.h" -namespace esphome { -namespace ld2412 { +namespace esphome::ld2412 { class DistanceResolutionSelect : public select::Select, public Parented { public: @@ -14,5 +13,4 @@ class DistanceResolutionSelect : public select::Select, public Parentedpublish_state(index); this->parent_->set_light_out_control(); } -} // namespace ld2412 -} // namespace esphome +} // namespace esphome::ld2412 diff --git a/esphome/components/ld2412/select/light_out_control_select.h b/esphome/components/ld2412/select/light_out_control_select.h index 7a50970d0d..9f86189878 100644 --- a/esphome/components/ld2412/select/light_out_control_select.h +++ b/esphome/components/ld2412/select/light_out_control_select.h @@ -3,8 +3,7 @@ #include "esphome/components/select/select.h" #include "../ld2412.h" -namespace esphome { -namespace ld2412 { +namespace esphome::ld2412 { class LightOutControlSelect : public select::Select, public Parented { public: @@ -14,5 +13,4 @@ class LightOutControlSelect : public select::Select, public Parentedpublish_state(state); this->parent_->set_bluetooth(state); } -} // namespace ld2412 -} // namespace esphome +} // namespace esphome::ld2412 diff --git a/esphome/components/ld2412/switch/bluetooth_switch.h b/esphome/components/ld2412/switch/bluetooth_switch.h index 730d338d87..0c0d1fa550 100644 --- a/esphome/components/ld2412/switch/bluetooth_switch.h +++ b/esphome/components/ld2412/switch/bluetooth_switch.h @@ -3,8 +3,7 @@ #include "esphome/components/switch/switch.h" #include "../ld2412.h" -namespace esphome { -namespace ld2412 { +namespace esphome::ld2412 { class BluetoothSwitch : public switch_::Switch, public Parented { public: @@ -14,5 +13,4 @@ class BluetoothSwitch : public switch_::Switch, public Parented void write_state(bool state) override; }; -} // namespace ld2412 -} // namespace esphome +} // namespace esphome::ld2412 diff --git a/esphome/components/ld2412/switch/engineering_mode_switch.cpp b/esphome/components/ld2412/switch/engineering_mode_switch.cpp index 29ca0c22a8..28b4e5d9e6 100644 --- a/esphome/components/ld2412/switch/engineering_mode_switch.cpp +++ b/esphome/components/ld2412/switch/engineering_mode_switch.cpp @@ -1,12 +1,10 @@ #include "engineering_mode_switch.h" -namespace esphome { -namespace ld2412 { +namespace esphome::ld2412 { void EngineeringModeSwitch::write_state(bool state) { this->publish_state(state); this->parent_->set_engineering_mode(state); } -} // namespace ld2412 -} // namespace esphome +} // namespace esphome::ld2412 diff --git a/esphome/components/ld2412/switch/engineering_mode_switch.h b/esphome/components/ld2412/switch/engineering_mode_switch.h index aaa404c673..4e75a8a185 100644 --- a/esphome/components/ld2412/switch/engineering_mode_switch.h +++ b/esphome/components/ld2412/switch/engineering_mode_switch.h @@ -3,8 +3,7 @@ #include "esphome/components/switch/switch.h" #include "../ld2412.h" -namespace esphome { -namespace ld2412 { +namespace esphome::ld2412 { class EngineeringModeSwitch : public switch_::Switch, public Parented { public: @@ -14,5 +13,4 @@ class EngineeringModeSwitch : public switch_::Switch, public Parentedpresence_bsensor_); } -} // namespace ld2420 -} // namespace esphome +} // namespace esphome::ld2420 diff --git a/esphome/components/ld2420/binary_sensor/ld2420_binary_sensor.h b/esphome/components/ld2420/binary_sensor/ld2420_binary_sensor.h index ee06439090..ec52312f92 100644 --- a/esphome/components/ld2420/binary_sensor/ld2420_binary_sensor.h +++ b/esphome/components/ld2420/binary_sensor/ld2420_binary_sensor.h @@ -3,8 +3,7 @@ #include "../ld2420.h" #include "esphome/components/binary_sensor/binary_sensor.h" -namespace esphome { -namespace ld2420 { +namespace esphome::ld2420 { class LD2420BinarySensor : public LD2420Listener, public Component, binary_sensor::BinarySensor { public: @@ -21,5 +20,4 @@ class LD2420BinarySensor : public LD2420Listener, public Component, binary_senso binary_sensor::BinarySensor *presence_bsensor_{nullptr}; }; -} // namespace ld2420 -} // namespace esphome +} // namespace esphome::ld2420 diff --git a/esphome/components/ld2420/button/reconfig_buttons.cpp b/esphome/components/ld2420/button/reconfig_buttons.cpp index fb8ec2b5a6..1e748e59b8 100644 --- a/esphome/components/ld2420/button/reconfig_buttons.cpp +++ b/esphome/components/ld2420/button/reconfig_buttons.cpp @@ -4,13 +4,11 @@ static const char *const TAG = "ld2420.button"; -namespace esphome { -namespace ld2420 { +namespace esphome::ld2420 { void LD2420ApplyConfigButton::press_action() { this->parent_->apply_config_action(); } void LD2420RevertConfigButton::press_action() { this->parent_->revert_config_action(); } void LD2420RestartModuleButton::press_action() { this->parent_->restart_module_action(); } void LD2420FactoryResetButton::press_action() { this->parent_->factory_reset_action(); } -} // namespace ld2420 -} // namespace esphome +} // namespace esphome::ld2420 diff --git a/esphome/components/ld2420/button/reconfig_buttons.h b/esphome/components/ld2420/button/reconfig_buttons.h index 4e9e7a3692..72171ef386 100644 --- a/esphome/components/ld2420/button/reconfig_buttons.h +++ b/esphome/components/ld2420/button/reconfig_buttons.h @@ -3,8 +3,7 @@ #include "esphome/components/button/button.h" #include "../ld2420.h" -namespace esphome { -namespace ld2420 { +namespace esphome::ld2420 { class LD2420ApplyConfigButton : public button::Button, public Parented { public: @@ -38,5 +37,4 @@ class LD2420FactoryResetButton : public button::Button, public Parented listeners_{}; }; -} // namespace ld2420 -} // namespace esphome +} // namespace esphome::ld2420 diff --git a/esphome/components/ld2420/number/gate_config_number.cpp b/esphome/components/ld2420/number/gate_config_number.cpp index a373753770..998eed2188 100644 --- a/esphome/components/ld2420/number/gate_config_number.cpp +++ b/esphome/components/ld2420/number/gate_config_number.cpp @@ -4,8 +4,7 @@ static const char *const TAG = "ld2420.number"; -namespace esphome { -namespace ld2420 { +namespace esphome::ld2420 { void LD2420TimeoutNumber::control(float timeout) { this->publish_state(timeout); @@ -69,5 +68,4 @@ void LD2420StillThresholdNumbers::control(float still_threshold) { } } -} // namespace ld2420 -} // namespace esphome +} // namespace esphome::ld2420 diff --git a/esphome/components/ld2420/number/gate_config_number.h b/esphome/components/ld2420/number/gate_config_number.h index 459a8026e3..8a8b9c61b1 100644 --- a/esphome/components/ld2420/number/gate_config_number.h +++ b/esphome/components/ld2420/number/gate_config_number.h @@ -3,8 +3,7 @@ #include "esphome/components/number/number.h" #include "../ld2420.h" -namespace esphome { -namespace ld2420 { +namespace esphome::ld2420 { class LD2420TimeoutNumber : public number::Number, public Parented { public: @@ -74,5 +73,4 @@ class LD2420MoveThresholdNumbers : public number::Number, public Parentedparent_->set_operating_mode(this->option_at(index)); } -} // namespace ld2420 -} // namespace esphome +} // namespace esphome::ld2420 diff --git a/esphome/components/ld2420/select/operating_mode_select.h b/esphome/components/ld2420/select/operating_mode_select.h index f59eb33432..c1b8e0b11b 100644 --- a/esphome/components/ld2420/select/operating_mode_select.h +++ b/esphome/components/ld2420/select/operating_mode_select.h @@ -3,8 +3,7 @@ #include "../ld2420.h" #include "esphome/components/select/select.h" -namespace esphome { -namespace ld2420 { +namespace esphome::ld2420 { class LD2420Select : public Component, public select::Select, public Parented { public: @@ -14,5 +13,4 @@ class LD2420Select : public Component, public select::Select, public Parenteddistance_sensor_); } -} // namespace ld2420 -} // namespace esphome +} // namespace esphome::ld2420 diff --git a/esphome/components/ld2420/sensor/ld2420_sensor.h b/esphome/components/ld2420/sensor/ld2420_sensor.h index 82730d60e3..4849cfa047 100644 --- a/esphome/components/ld2420/sensor/ld2420_sensor.h +++ b/esphome/components/ld2420/sensor/ld2420_sensor.h @@ -3,8 +3,7 @@ #include "../ld2420.h" #include "esphome/components/sensor/sensor.h" -namespace esphome { -namespace ld2420 { +namespace esphome::ld2420 { class LD2420Sensor : public LD2420Listener, public Component, sensor::Sensor { public: @@ -30,5 +29,4 @@ class LD2420Sensor : public LD2420Listener, public Component, sensor::Sensor { std::vector energy_sensors_ = std::vector(TOTAL_GATES); }; -} // namespace ld2420 -} // namespace esphome +} // namespace esphome::ld2420 diff --git a/esphome/components/ld2420/text_sensor/ld2420_text_sensor.cpp b/esphome/components/ld2420/text_sensor/ld2420_text_sensor.cpp index f647a36936..f7b016c9d9 100644 --- a/esphome/components/ld2420/text_sensor/ld2420_text_sensor.cpp +++ b/esphome/components/ld2420/text_sensor/ld2420_text_sensor.cpp @@ -2,8 +2,7 @@ #include "esphome/core/helpers.h" #include "esphome/core/log.h" -namespace esphome { -namespace ld2420 { +namespace esphome::ld2420 { static const char *const TAG = "ld2420.text_sensor"; @@ -12,5 +11,4 @@ void LD2420TextSensor::dump_config() { LOG_TEXT_SENSOR(" ", "Firmware", this->fw_version_text_sensor_); } -} // namespace ld2420 -} // namespace esphome +} // namespace esphome::ld2420 diff --git a/esphome/components/ld2420/text_sensor/ld2420_text_sensor.h b/esphome/components/ld2420/text_sensor/ld2420_text_sensor.h index 073ddd5d0f..1932eaaf69 100644 --- a/esphome/components/ld2420/text_sensor/ld2420_text_sensor.h +++ b/esphome/components/ld2420/text_sensor/ld2420_text_sensor.h @@ -3,8 +3,7 @@ #include "../ld2420.h" #include "esphome/components/text_sensor/text_sensor.h" -namespace esphome { -namespace ld2420 { +namespace esphome::ld2420 { class LD2420TextSensor : public LD2420Listener, public Component, text_sensor::TextSensor { public: @@ -20,5 +19,4 @@ class LD2420TextSensor : public LD2420Listener, public Component, text_sensor::T text_sensor::TextSensor *fw_version_text_sensor_{nullptr}; }; -} // namespace ld2420 -} // namespace esphome +} // namespace esphome::ld2420 diff --git a/esphome/components/ld2450/button/factory_reset_button.cpp b/esphome/components/ld2450/button/factory_reset_button.cpp index bcac7ada2f..7a8eb5b0dd 100644 --- a/esphome/components/ld2450/button/factory_reset_button.cpp +++ b/esphome/components/ld2450/button/factory_reset_button.cpp @@ -1,9 +1,7 @@ #include "factory_reset_button.h" -namespace esphome { -namespace ld2450 { +namespace esphome::ld2450 { void FactoryResetButton::press_action() { this->parent_->factory_reset(); } -} // namespace ld2450 -} // namespace esphome +} // namespace esphome::ld2450 diff --git a/esphome/components/ld2450/button/factory_reset_button.h b/esphome/components/ld2450/button/factory_reset_button.h index 8e80347119..392fc67ffd 100644 --- a/esphome/components/ld2450/button/factory_reset_button.h +++ b/esphome/components/ld2450/button/factory_reset_button.h @@ -3,8 +3,7 @@ #include "esphome/components/button/button.h" #include "../ld2450.h" -namespace esphome { -namespace ld2450 { +namespace esphome::ld2450 { class FactoryResetButton : public button::Button, public Parented { public: @@ -14,5 +13,4 @@ class FactoryResetButton : public button::Button, public Parentedparent_->restart_and_read_all_info(); } -} // namespace ld2450 -} // namespace esphome +} // namespace esphome::ld2450 diff --git a/esphome/components/ld2450/button/restart_button.h b/esphome/components/ld2450/button/restart_button.h index a44ae5a4d2..9219011f8b 100644 --- a/esphome/components/ld2450/button/restart_button.h +++ b/esphome/components/ld2450/button/restart_button.h @@ -3,8 +3,7 @@ #include "esphome/components/button/button.h" #include "../ld2450.h" -namespace esphome { -namespace ld2450 { +namespace esphome::ld2450 { class RestartButton : public button::Button, public Parented { public: @@ -14,5 +13,4 @@ class RestartButton : public button::Button, public Parented { void press_action() override; }; -} // namespace ld2450 -} // namespace esphome +} // namespace esphome::ld2450 diff --git a/esphome/components/ld2450/ld2450.cpp b/esphome/components/ld2450/ld2450.cpp index 8e5287aec7..e69ef31d4f 100644 --- a/esphome/components/ld2450/ld2450.cpp +++ b/esphome/components/ld2450/ld2450.cpp @@ -13,8 +13,7 @@ #include #include -namespace esphome { -namespace ld2450 { +namespace esphome::ld2450 { static const char *const TAG = "ld2450"; @@ -939,5 +938,4 @@ float LD2450Component::restore_from_flash_() { } #endif -} // namespace ld2450 -} // namespace esphome +} // namespace esphome::ld2450 diff --git a/esphome/components/ld2450/ld2450.h b/esphome/components/ld2450/ld2450.h index 44b63be444..b94c3cac37 100644 --- a/esphome/components/ld2450/ld2450.h +++ b/esphome/components/ld2450/ld2450.h @@ -31,8 +31,7 @@ #include -namespace esphome { -namespace ld2450 { +namespace esphome::ld2450 { using namespace ld24xx; @@ -193,5 +192,4 @@ class LD2450Component : public Component, public uart::UARTDevice { #endif }; -} // namespace ld2450 -} // namespace esphome +} // namespace esphome::ld2450 diff --git a/esphome/components/ld2450/number/presence_timeout_number.cpp b/esphome/components/ld2450/number/presence_timeout_number.cpp index ecfe71f484..19a1ada0d7 100644 --- a/esphome/components/ld2450/number/presence_timeout_number.cpp +++ b/esphome/components/ld2450/number/presence_timeout_number.cpp @@ -1,12 +1,10 @@ #include "presence_timeout_number.h" -namespace esphome { -namespace ld2450 { +namespace esphome::ld2450 { void PresenceTimeoutNumber::control(float value) { this->publish_state(value); this->parent_->set_presence_timeout(); } -} // namespace ld2450 -} // namespace esphome +} // namespace esphome::ld2450 diff --git a/esphome/components/ld2450/number/presence_timeout_number.h b/esphome/components/ld2450/number/presence_timeout_number.h index b18699792f..09c8afca55 100644 --- a/esphome/components/ld2450/number/presence_timeout_number.h +++ b/esphome/components/ld2450/number/presence_timeout_number.h @@ -3,8 +3,7 @@ #include "esphome/components/number/number.h" #include "../ld2450.h" -namespace esphome { -namespace ld2450 { +namespace esphome::ld2450 { class PresenceTimeoutNumber : public number::Number, public Parented { public: @@ -14,5 +13,4 @@ class PresenceTimeoutNumber : public number::Number, public Parentedparent_->set_zone_coordinate(this->zone_); } -} // namespace ld2450 -} // namespace esphome +} // namespace esphome::ld2450 diff --git a/esphome/components/ld2450/number/zone_coordinate_number.h b/esphome/components/ld2450/number/zone_coordinate_number.h index 72b83889c4..f5a389d712 100644 --- a/esphome/components/ld2450/number/zone_coordinate_number.h +++ b/esphome/components/ld2450/number/zone_coordinate_number.h @@ -3,8 +3,7 @@ #include "esphome/components/number/number.h" #include "../ld2450.h" -namespace esphome { -namespace ld2450 { +namespace esphome::ld2450 { class ZoneCoordinateNumber : public number::Number, public Parented { public: @@ -15,5 +14,4 @@ class ZoneCoordinateNumber : public number::Number, public Parentedpublish_state(index); this->parent_->set_baud_rate(this->option_at(index)); } -} // namespace ld2450 -} // namespace esphome +} // namespace esphome::ld2450 diff --git a/esphome/components/ld2450/select/baud_rate_select.h b/esphome/components/ld2450/select/baud_rate_select.h index 22810d5f13..cb53118170 100644 --- a/esphome/components/ld2450/select/baud_rate_select.h +++ b/esphome/components/ld2450/select/baud_rate_select.h @@ -3,8 +3,7 @@ #include "esphome/components/select/select.h" #include "../ld2450.h" -namespace esphome { -namespace ld2450 { +namespace esphome::ld2450 { class BaudRateSelect : public select::Select, public Parented { public: @@ -14,5 +13,4 @@ class BaudRateSelect : public select::Select, public Parented { void control(size_t index) override; }; -} // namespace ld2450 -} // namespace esphome +} // namespace esphome::ld2450 diff --git a/esphome/components/ld2450/select/zone_type_select.cpp b/esphome/components/ld2450/select/zone_type_select.cpp index 1111428c7c..39642b99ad 100644 --- a/esphome/components/ld2450/select/zone_type_select.cpp +++ b/esphome/components/ld2450/select/zone_type_select.cpp @@ -1,12 +1,10 @@ #include "zone_type_select.h" -namespace esphome { -namespace ld2450 { +namespace esphome::ld2450 { void ZoneTypeSelect::control(size_t index) { this->publish_state(index); this->parent_->set_zone_type(this->option_at(index)); } -} // namespace ld2450 -} // namespace esphome +} // namespace esphome::ld2450 diff --git a/esphome/components/ld2450/select/zone_type_select.h b/esphome/components/ld2450/select/zone_type_select.h index fc95ec1021..566346eb48 100644 --- a/esphome/components/ld2450/select/zone_type_select.h +++ b/esphome/components/ld2450/select/zone_type_select.h @@ -3,8 +3,7 @@ #include "esphome/components/select/select.h" #include "../ld2450.h" -namespace esphome { -namespace ld2450 { +namespace esphome::ld2450 { class ZoneTypeSelect : public select::Select, public Parented { public: @@ -14,5 +13,4 @@ class ZoneTypeSelect : public select::Select, public Parented { void control(size_t index) override; }; -} // namespace ld2450 -} // namespace esphome +} // namespace esphome::ld2450 diff --git a/esphome/components/ld2450/switch/bluetooth_switch.cpp b/esphome/components/ld2450/switch/bluetooth_switch.cpp index fa0d4fb06a..0e19a3e6c6 100644 --- a/esphome/components/ld2450/switch/bluetooth_switch.cpp +++ b/esphome/components/ld2450/switch/bluetooth_switch.cpp @@ -1,12 +1,10 @@ #include "bluetooth_switch.h" -namespace esphome { -namespace ld2450 { +namespace esphome::ld2450 { void BluetoothSwitch::write_state(bool state) { this->publish_state(state); this->parent_->set_bluetooth(state); } -} // namespace ld2450 -} // namespace esphome +} // namespace esphome::ld2450 diff --git a/esphome/components/ld2450/switch/bluetooth_switch.h b/esphome/components/ld2450/switch/bluetooth_switch.h index 3c1c4f755c..3d48a89b57 100644 --- a/esphome/components/ld2450/switch/bluetooth_switch.h +++ b/esphome/components/ld2450/switch/bluetooth_switch.h @@ -3,8 +3,7 @@ #include "esphome/components/switch/switch.h" #include "../ld2450.h" -namespace esphome { -namespace ld2450 { +namespace esphome::ld2450 { class BluetoothSwitch : public switch_::Switch, public Parented { public: @@ -14,5 +13,4 @@ class BluetoothSwitch : public switch_::Switch, public Parented void write_state(bool state) override; }; -} // namespace ld2450 -} // namespace esphome +} // namespace esphome::ld2450 diff --git a/esphome/components/ld2450/switch/multi_target_switch.cpp b/esphome/components/ld2450/switch/multi_target_switch.cpp index a163e29fc5..0b1cb04a68 100644 --- a/esphome/components/ld2450/switch/multi_target_switch.cpp +++ b/esphome/components/ld2450/switch/multi_target_switch.cpp @@ -1,12 +1,10 @@ #include "multi_target_switch.h" -namespace esphome { -namespace ld2450 { +namespace esphome::ld2450 { void MultiTargetSwitch::write_state(bool state) { this->publish_state(state); this->parent_->set_multi_target(state); } -} // namespace ld2450 -} // namespace esphome +} // namespace esphome::ld2450 diff --git a/esphome/components/ld2450/switch/multi_target_switch.h b/esphome/components/ld2450/switch/multi_target_switch.h index ca6253588d..739f308cce 100644 --- a/esphome/components/ld2450/switch/multi_target_switch.h +++ b/esphome/components/ld2450/switch/multi_target_switch.h @@ -3,8 +3,7 @@ #include "esphome/components/switch/switch.h" #include "../ld2450.h" -namespace esphome { -namespace ld2450 { +namespace esphome::ld2450 { class MultiTargetSwitch : public switch_::Switch, public Parented { public: @@ -14,5 +13,4 @@ class MultiTargetSwitch : public switch_::Switch, public Parented> 8) #define lowbyte(val) (uint8_t)((val) &0xff) -namespace esphome { -namespace ld24xx { +namespace esphome::ld24xx { static const char *const UNKNOWN_MAC = "unknown"; static const char *const VERSION_FMT = "%u.%02X.%02X%02X%02X%02X"; @@ -83,5 +82,4 @@ template class SensorWithDedup { Deduplicator publish_dedup; }; #endif -} // namespace ld24xx -} // namespace esphome +} // namespace esphome::ld24xx From 100ea46f03eb45b10beed6f8d36133f9d2635e70 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 18 Nov 2025 23:19:54 -0600 Subject: [PATCH 141/415] [tests] Fix SNTP time ID conflicts in component tests for grouped testing (#11990) --- tests/components/lvgl/common.yaml | 4 ++-- tests/components/lvgl/lvgl-package.yaml | 6 +++--- tests/components/mqtt/common.yaml | 1 + tests/components/uptime/common.yaml | 1 + tests/components/wireguard/common.yaml | 2 ++ 5 files changed, 9 insertions(+), 5 deletions(-) diff --git a/tests/components/lvgl/common.yaml b/tests/components/lvgl/common.yaml index c70dd7568d..652ae7e7a1 100644 --- a/tests/components/lvgl/common.yaml +++ b/tests/components/lvgl/common.yaml @@ -115,8 +115,8 @@ wifi: password: PASSWORD123 time: - platform: sntp - id: time_id + - platform: sntp + id: sntp_time text: - id: lvgl_text diff --git a/tests/components/lvgl/lvgl-package.yaml b/tests/components/lvgl/lvgl-package.yaml index eabceff9d9..5839643638 100644 --- a/tests/components/lvgl/lvgl-package.yaml +++ b/tests/components/lvgl/lvgl-package.yaml @@ -478,19 +478,19 @@ lvgl: id: hello_label text: time_format: "%c" - time: time_id + time: sntp_time - lvgl.label.update: id: hello_label text: time_format: "%c" - time: !lambda return id(time_id).now(); + time: !lambda return id(sntp_time).now(); - lvgl.label.update: id: hello_label text: time_format: "%c" time: !lambda |- ESP_LOGD("label", "multi-line lambda"); - return id(time_id).now(); + return id(sntp_time).now(); on_value: logger.log: format: "state now %d" diff --git a/tests/components/mqtt/common.yaml b/tests/components/mqtt/common.yaml index 3f1b83bb01..284ac30337 100644 --- a/tests/components/mqtt/common.yaml +++ b/tests/components/mqtt/common.yaml @@ -4,6 +4,7 @@ wifi: time: - platform: sntp + id: sntp_time mqtt: broker: "192.168.178.84" diff --git a/tests/components/uptime/common.yaml b/tests/components/uptime/common.yaml index 86b764e7ff..279258c670 100644 --- a/tests/components/uptime/common.yaml +++ b/tests/components/uptime/common.yaml @@ -3,6 +3,7 @@ wifi: time: - platform: sntp + id: sntp_time sensor: - platform: uptime diff --git a/tests/components/wireguard/common.yaml b/tests/components/wireguard/common.yaml index cd7ab1075e..342ffa32f6 100644 --- a/tests/components/wireguard/common.yaml +++ b/tests/components/wireguard/common.yaml @@ -4,8 +4,10 @@ wifi: time: - platform: sntp + id: sntp_time wireguard: + time_id: sntp_time address: 172.16.34.100 netmask: 255.255.255.0 # NEVER use the following keys for your VPN -- they are now public! From f2b10ad132dcc4118cd6e0ecbead9da746863b77 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Wed, 19 Nov 2025 10:12:34 -0500 Subject: [PATCH 142/415] [text_sensor] Fix infinite loop in substitute filter (#11989) Co-authored-by: J. Nick Koston --- esphome/components/text_sensor/filter.cpp | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/esphome/components/text_sensor/filter.cpp b/esphome/components/text_sensor/filter.cpp index a242b43b1c..40a37febee 100644 --- a/esphome/components/text_sensor/filter.cpp +++ b/esphome/components/text_sensor/filter.cpp @@ -66,10 +66,14 @@ SubstituteFilter::SubstituteFilter(const std::initializer_list &su : substitutions_(substitutions) {} optional SubstituteFilter::new_value(std::string value) { - std::size_t pos; for (const auto &sub : this->substitutions_) { - while ((pos = value.find(sub.from)) != std::string::npos) + std::size_t pos = 0; + while ((pos = value.find(sub.from, pos)) != std::string::npos) { value.replace(pos, sub.from.size(), sub.to); + // Advance past the replacement to avoid infinite loop when + // the replacement contains the search pattern (e.g., f -> foo) + pos += sub.to.size(); + } } return value; } From 73bc5252a1467a7530b921eb9be16ab6819f3038 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Wed, 19 Nov 2025 10:12:57 -0500 Subject: [PATCH 143/415] [wifi] Fix positive RSSI values on 8266 (#11994) --- esphome/components/wifi/wifi_component_esp8266.cpp | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/esphome/components/wifi/wifi_component_esp8266.cpp b/esphome/components/wifi/wifi_component_esp8266.cpp index a543628e27..274a505db2 100644 --- a/esphome/components/wifi/wifi_component_esp8266.cpp +++ b/esphome/components/wifi/wifi_component_esp8266.cpp @@ -872,7 +872,13 @@ bssid_t WiFiComponent::wifi_bssid() { return bssid; } std::string WiFiComponent::wifi_ssid() { return WiFi.SSID().c_str(); } -int8_t WiFiComponent::wifi_rssi() { return WiFi.status() == WL_CONNECTED ? WiFi.RSSI() : WIFI_RSSI_DISCONNECTED; } +int8_t WiFiComponent::wifi_rssi() { + if (WiFi.status() != WL_CONNECTED) + return WIFI_RSSI_DISCONNECTED; + int8_t rssi = WiFi.RSSI(); + // Values >= 31 are error codes per NONOS SDK API, not valid RSSI readings + return rssi >= 31 ? WIFI_RSSI_DISCONNECTED : rssi; +} int32_t WiFiComponent::get_wifi_channel() { return WiFi.channel(); } network::IPAddress WiFiComponent::wifi_subnet_mask_() { return {(const ip_addr_t *) WiFi.subnetMask()}; } network::IPAddress WiFiComponent::wifi_gateway_ip_() { return {(const ip_addr_t *) WiFi.gatewayIP()}; } From 61cef0a75c4b46df6e0d7761c78dbedfdb8aee72 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Wed, 19 Nov 2025 12:58:47 -0500 Subject: [PATCH 144/415] [api] Fix format warnings in dump (#11999) --- esphome/components/api/api_pb2_dump.cpp | 2 +- script/api_protobuf/api_protobuf.py | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/esphome/components/api/api_pb2_dump.cpp b/esphome/components/api/api_pb2_dump.cpp index d9662483bf..127ef44cd8 100644 --- a/esphome/components/api/api_pb2_dump.cpp +++ b/esphome/components/api/api_pb2_dump.cpp @@ -66,7 +66,7 @@ static void dump_field(std::string &out, const char *field_name, float value, in static void dump_field(std::string &out, const char *field_name, uint64_t value, int indent = 2) { char buffer[64]; append_field_prefix(out, field_name, indent); - snprintf(buffer, 64, "%llu", value); + snprintf(buffer, 64, "%" PRIu64, value); append_with_newline(out, buffer); } diff --git a/script/api_protobuf/api_protobuf.py b/script/api_protobuf/api_protobuf.py index 3b756095a1..b07a249c8d 100755 --- a/script/api_protobuf/api_protobuf.py +++ b/script/api_protobuf/api_protobuf.py @@ -462,7 +462,7 @@ class Int64Type(TypeInfo): wire_type = WireType.VARINT # Uses wire type 0 def dump(self, name: str) -> str: - o = f'snprintf(buffer, sizeof(buffer), "%lld", {name});\n' + o = f'snprintf(buffer, sizeof(buffer), "%" PRId64, {name});\n' o += "out.append(buffer);" return o @@ -482,7 +482,7 @@ class UInt64Type(TypeInfo): wire_type = WireType.VARINT # Uses wire type 0 def dump(self, name: str) -> str: - o = f'snprintf(buffer, sizeof(buffer), "%llu", {name});\n' + o = f'snprintf(buffer, sizeof(buffer), "%" PRIu64, {name});\n' o += "out.append(buffer);" return o @@ -522,7 +522,7 @@ class Fixed64Type(TypeInfo): wire_type = WireType.FIXED64 # Uses wire type 1 def dump(self, name: str) -> str: - o = f'snprintf(buffer, sizeof(buffer), "%llu", {name});\n' + o = f'snprintf(buffer, sizeof(buffer), "%" PRIu64, {name});\n' o += "out.append(buffer);" return o @@ -1106,7 +1106,7 @@ class SFixed64Type(TypeInfo): wire_type = WireType.FIXED64 # Uses wire type 1 def dump(self, name: str) -> str: - o = f'snprintf(buffer, sizeof(buffer), "%lld", {name});\n' + o = f'snprintf(buffer, sizeof(buffer), "%" PRId64, {name});\n' o += "out.append(buffer);" return o @@ -1150,7 +1150,7 @@ class SInt64Type(TypeInfo): wire_type = WireType.VARINT # Uses wire type 0 def dump(self, name: str) -> str: - o = f'snprintf(buffer, sizeof(buffer), "%lld", {name});\n' + o = f'snprintf(buffer, sizeof(buffer), "%" PRId64, {name});\n' o += "out.append(buffer);" return o @@ -2546,7 +2546,7 @@ static void dump_field(std::string &out, const char *field_name, float value, in static void dump_field(std::string &out, const char *field_name, uint64_t value, int indent = 2) { char buffer[64]; append_field_prefix(out, field_name, indent); - snprintf(buffer, 64, "%llu", value); + snprintf(buffer, 64, "%" PRIu64, value); append_with_newline(out, buffer); } From 8804bc28154abb1601cd029971cc0abe12a2f49a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 19 Nov 2025 12:58:33 -0600 Subject: [PATCH 145/415] [web_server_idf] Fix pbuf_free crash by moving shutdown before close (#11995) --- .../web_server_idf/web_server_idf.cpp | 41 ++++++++++++++----- .../web_server_idf/web_server_idf.h | 1 + 2 files changed, 31 insertions(+), 11 deletions(-) diff --git a/esphome/components/web_server_idf/web_server_idf.cpp b/esphome/components/web_server_idf/web_server_idf.cpp index f5a66f6bd9..c910ed06c5 100644 --- a/esphome/components/web_server_idf/web_server_idf.cpp +++ b/esphome/components/web_server_idf/web_server_idf.cpp @@ -87,6 +87,29 @@ int nonblocking_send(httpd_handle_t hd, int sockfd, const char *buf, size_t buf_ } } // namespace +void AsyncWebServer::safe_close_with_shutdown(httpd_handle_t hd, int sockfd) { + // CRITICAL: Shut down receive BEFORE closing to prevent lwIP race conditions + // + // The race condition occurs because close() initiates lwIP teardown while + // the TCP/IP thread can still receive packets, causing assertions when + // recv_tcp() sees partially-torn-down state. + // + // By shutting down receive first, we tell lwIP to stop accepting new data BEFORE + // the teardown begins, eliminating the race window. We only shutdown RD (not RDWR) + // to allow the FIN packet to be sent cleanly during close(). + // + // Note: This function may be called with an already-closed socket if the network + // stack closed it. In that case, shutdown() will fail but close() is safe to call. + // + // See: https://github.com/esphome/esphome-webserver/issues/163 + + // Attempt shutdown - ignore errors as socket may already be closed + shutdown(sockfd, SHUT_RD); + + // Always close - safe even if socket is already closed by network stack + close(sockfd); +} + void AsyncWebServer::end() { if (this->server_) { httpd_stop(this->server_); @@ -115,6 +138,8 @@ void AsyncWebServer::begin() { config.uri_match_fn = [](const char * /*unused*/, const char * /*unused*/, size_t /*unused*/) { return true; }; // Enable LRU purging if requested (e.g., by captive portal to handle probe bursts) config.lru_purge_enable = this->lru_purge_enable_; + // Use custom close function that shuts down before closing to prevent lwIP race conditions + config.close_fn = AsyncWebServer::safe_close_with_shutdown; if (httpd_start(&this->server_, &config) == ESP_OK) { const httpd_uri_t handler_get = { .uri = "", @@ -505,17 +530,11 @@ AsyncEventSourceResponse::AsyncEventSourceResponse(const AsyncWebServerRequest * void AsyncEventSourceResponse::destroy(void *ptr) { auto *rsp = static_cast(ptr); int fd = rsp->fd_.exchange(0); // Atomically get and clear fd - - if (fd > 0) { - ESP_LOGD(TAG, "Event source connection closed (fd: %d)", fd); - // Immediately shut down the socket to prevent lwIP from delivering more data - // This prevents "recv_tcp: recv for wrong pcb!" assertions when the TCP stack - // tries to deliver queued data after the session is marked as dead - // See: https://github.com/esphome/esphome/issues/11936 - shutdown(fd, SHUT_RDWR); - // Note: We don't close() the socket - httpd owns it and will close it - } - // Session will be cleaned up in the main loop to avoid race conditions + ESP_LOGD(TAG, "Event source connection closed (fd: %d)", fd); + // Mark as dead - will be cleaned up in the main loop + // Note: We don't delete or remove from set here to avoid race conditions + // httpd will call our custom close_fn (safe_close_with_shutdown) which handles + // shutdown() before close() to prevent lwIP race conditions } // helper for allowing only unique entries in the queue diff --git a/esphome/components/web_server_idf/web_server_idf.h b/esphome/components/web_server_idf/web_server_idf.h index b9f690b462..a139e9e4df 100644 --- a/esphome/components/web_server_idf/web_server_idf.h +++ b/esphome/components/web_server_idf/web_server_idf.h @@ -209,6 +209,7 @@ class AsyncWebServer { static esp_err_t request_handler(httpd_req_t *r); static esp_err_t request_post_handler(httpd_req_t *r); esp_err_t request_handler_(AsyncWebServerRequest *request) const; + static void safe_close_with_shutdown(httpd_handle_t hd, int sockfd); #ifdef USE_WEBSERVER_OTA esp_err_t handle_multipart_upload_(httpd_req_t *r, const char *content_type); #endif From b02b07ffafaf7dbf0529460638608f69d095a82b Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Thu, 20 Nov 2025 08:11:45 +1300 Subject: [PATCH 146/415] [epaper_spi] Add basic `7.3in-Spectra-E6` model (#12001) --- esphome/components/epaper_spi/display.py | 2 +- esphome/components/epaper_spi/models/spectra_e6.py | 8 ++++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/esphome/components/epaper_spi/display.py b/esphome/components/epaper_spi/display.py index 9ff393b397..182c37ba40 100644 --- a/esphome/components/epaper_spi/display.py +++ b/esphome/components/epaper_spi/display.py @@ -102,7 +102,7 @@ def customise_schema(config): """ config = cv.Schema( { - cv.Required(CONF_MODEL): cv.one_of(*MODELS, upper=True), + cv.Required(CONF_MODEL): cv.one_of(*MODELS, upper=True, space="-"), }, extra=cv.ALLOW_EXTRA, )(config) diff --git a/esphome/components/epaper_spi/models/spectra_e6.py b/esphome/components/epaper_spi/models/spectra_e6.py index 9f0b673d69..42a5a7da72 100644 --- a/esphome/components/epaper_spi/models/spectra_e6.py +++ b/esphome/components/epaper_spi/models/spectra_e6.py @@ -32,11 +32,15 @@ class SpectraE6(EpaperModel): spectra_e6 = SpectraE6("spectra-e6") -spectra_e6.extend( - "Seeed-reTerminal-E1002", +spectra_e6_7p3 = spectra_e6.extend( + "7.3in-Spectra-E6", width=800, height=480, data_rate="20MHz", +) + +spectra_e6_7p3.extend( + "Seeed-reTerminal-E1002", cs_pin=10, dc_pin=11, reset_pin=12, From 13b875c763b990a6d56104bd619f391c09ccceec Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 18 Nov 2025 23:19:54 -0600 Subject: [PATCH 147/415] [tests] Fix SNTP time ID conflicts in component tests for grouped testing (#11990) --- tests/components/lvgl/common.yaml | 4 ++-- tests/components/lvgl/lvgl-package.yaml | 6 +++--- tests/components/mqtt/common.yaml | 1 + tests/components/uptime/common.yaml | 1 + tests/components/wireguard/common.yaml | 2 ++ 5 files changed, 9 insertions(+), 5 deletions(-) diff --git a/tests/components/lvgl/common.yaml b/tests/components/lvgl/common.yaml index c70dd7568d..652ae7e7a1 100644 --- a/tests/components/lvgl/common.yaml +++ b/tests/components/lvgl/common.yaml @@ -115,8 +115,8 @@ wifi: password: PASSWORD123 time: - platform: sntp - id: time_id + - platform: sntp + id: sntp_time text: - id: lvgl_text diff --git a/tests/components/lvgl/lvgl-package.yaml b/tests/components/lvgl/lvgl-package.yaml index fba860a407..cb5b6f59b1 100644 --- a/tests/components/lvgl/lvgl-package.yaml +++ b/tests/components/lvgl/lvgl-package.yaml @@ -478,19 +478,19 @@ lvgl: id: hello_label text: time_format: "%c" - time: time_id + time: sntp_time - lvgl.label.update: id: hello_label text: time_format: "%c" - time: !lambda return id(time_id).now(); + time: !lambda return id(sntp_time).now(); - lvgl.label.update: id: hello_label text: time_format: "%c" time: !lambda |- ESP_LOGD("label", "multi-line lambda"); - return id(time_id).now(); + return id(sntp_time).now(); on_value: logger.log: format: "state now %d" diff --git a/tests/components/mqtt/common.yaml b/tests/components/mqtt/common.yaml index 3f1b83bb01..284ac30337 100644 --- a/tests/components/mqtt/common.yaml +++ b/tests/components/mqtt/common.yaml @@ -4,6 +4,7 @@ wifi: time: - platform: sntp + id: sntp_time mqtt: broker: "192.168.178.84" diff --git a/tests/components/uptime/common.yaml b/tests/components/uptime/common.yaml index 86b764e7ff..279258c670 100644 --- a/tests/components/uptime/common.yaml +++ b/tests/components/uptime/common.yaml @@ -3,6 +3,7 @@ wifi: time: - platform: sntp + id: sntp_time sensor: - platform: uptime diff --git a/tests/components/wireguard/common.yaml b/tests/components/wireguard/common.yaml index cd7ab1075e..342ffa32f6 100644 --- a/tests/components/wireguard/common.yaml +++ b/tests/components/wireguard/common.yaml @@ -4,8 +4,10 @@ wifi: time: - platform: sntp + id: sntp_time wireguard: + time_id: sntp_time address: 172.16.34.100 netmask: 255.255.255.0 # NEVER use the following keys for your VPN -- they are now public! From 7ef4b4f3d9d6cb8ce5414985ae59538647a3d438 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Wed, 19 Nov 2025 10:12:34 -0500 Subject: [PATCH 148/415] [text_sensor] Fix infinite loop in substitute filter (#11989) Co-authored-by: J. Nick Koston --- esphome/components/text_sensor/filter.cpp | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/esphome/components/text_sensor/filter.cpp b/esphome/components/text_sensor/filter.cpp index a242b43b1c..40a37febee 100644 --- a/esphome/components/text_sensor/filter.cpp +++ b/esphome/components/text_sensor/filter.cpp @@ -66,10 +66,14 @@ SubstituteFilter::SubstituteFilter(const std::initializer_list &su : substitutions_(substitutions) {} optional SubstituteFilter::new_value(std::string value) { - std::size_t pos; for (const auto &sub : this->substitutions_) { - while ((pos = value.find(sub.from)) != std::string::npos) + std::size_t pos = 0; + while ((pos = value.find(sub.from, pos)) != std::string::npos) { value.replace(pos, sub.from.size(), sub.to); + // Advance past the replacement to avoid infinite loop when + // the replacement contains the search pattern (e.g., f -> foo) + pos += sub.to.size(); + } } return value; } From 0a224f919b3889651db230fbd047cf05f4996cdf Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Wed, 19 Nov 2025 10:12:57 -0500 Subject: [PATCH 149/415] [wifi] Fix positive RSSI values on 8266 (#11994) --- esphome/components/wifi/wifi_component_esp8266.cpp | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/esphome/components/wifi/wifi_component_esp8266.cpp b/esphome/components/wifi/wifi_component_esp8266.cpp index bdaae5382a..0134fcaed8 100644 --- a/esphome/components/wifi/wifi_component_esp8266.cpp +++ b/esphome/components/wifi/wifi_component_esp8266.cpp @@ -870,7 +870,13 @@ bssid_t WiFiComponent::wifi_bssid() { return bssid; } std::string WiFiComponent::wifi_ssid() { return WiFi.SSID().c_str(); } -int8_t WiFiComponent::wifi_rssi() { return WiFi.status() == WL_CONNECTED ? WiFi.RSSI() : WIFI_RSSI_DISCONNECTED; } +int8_t WiFiComponent::wifi_rssi() { + if (WiFi.status() != WL_CONNECTED) + return WIFI_RSSI_DISCONNECTED; + int8_t rssi = WiFi.RSSI(); + // Values >= 31 are error codes per NONOS SDK API, not valid RSSI readings + return rssi >= 31 ? WIFI_RSSI_DISCONNECTED : rssi; +} int32_t WiFiComponent::get_wifi_channel() { return WiFi.channel(); } network::IPAddress WiFiComponent::wifi_subnet_mask_() { return {(const ip_addr_t *) WiFi.subnetMask()}; } network::IPAddress WiFiComponent::wifi_gateway_ip_() { return {(const ip_addr_t *) WiFi.gatewayIP()}; } From 71dc2d374d18f236c7a5fdae1eee61cbe78c0320 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 19 Nov 2025 12:58:33 -0600 Subject: [PATCH 150/415] [web_server_idf] Fix pbuf_free crash by moving shutdown before close (#11995) --- .../web_server_idf/web_server_idf.cpp | 41 ++++++++++++++----- .../web_server_idf/web_server_idf.h | 1 + 2 files changed, 31 insertions(+), 11 deletions(-) diff --git a/esphome/components/web_server_idf/web_server_idf.cpp b/esphome/components/web_server_idf/web_server_idf.cpp index f5a66f6bd9..c910ed06c5 100644 --- a/esphome/components/web_server_idf/web_server_idf.cpp +++ b/esphome/components/web_server_idf/web_server_idf.cpp @@ -87,6 +87,29 @@ int nonblocking_send(httpd_handle_t hd, int sockfd, const char *buf, size_t buf_ } } // namespace +void AsyncWebServer::safe_close_with_shutdown(httpd_handle_t hd, int sockfd) { + // CRITICAL: Shut down receive BEFORE closing to prevent lwIP race conditions + // + // The race condition occurs because close() initiates lwIP teardown while + // the TCP/IP thread can still receive packets, causing assertions when + // recv_tcp() sees partially-torn-down state. + // + // By shutting down receive first, we tell lwIP to stop accepting new data BEFORE + // the teardown begins, eliminating the race window. We only shutdown RD (not RDWR) + // to allow the FIN packet to be sent cleanly during close(). + // + // Note: This function may be called with an already-closed socket if the network + // stack closed it. In that case, shutdown() will fail but close() is safe to call. + // + // See: https://github.com/esphome/esphome-webserver/issues/163 + + // Attempt shutdown - ignore errors as socket may already be closed + shutdown(sockfd, SHUT_RD); + + // Always close - safe even if socket is already closed by network stack + close(sockfd); +} + void AsyncWebServer::end() { if (this->server_) { httpd_stop(this->server_); @@ -115,6 +138,8 @@ void AsyncWebServer::begin() { config.uri_match_fn = [](const char * /*unused*/, const char * /*unused*/, size_t /*unused*/) { return true; }; // Enable LRU purging if requested (e.g., by captive portal to handle probe bursts) config.lru_purge_enable = this->lru_purge_enable_; + // Use custom close function that shuts down before closing to prevent lwIP race conditions + config.close_fn = AsyncWebServer::safe_close_with_shutdown; if (httpd_start(&this->server_, &config) == ESP_OK) { const httpd_uri_t handler_get = { .uri = "", @@ -505,17 +530,11 @@ AsyncEventSourceResponse::AsyncEventSourceResponse(const AsyncWebServerRequest * void AsyncEventSourceResponse::destroy(void *ptr) { auto *rsp = static_cast(ptr); int fd = rsp->fd_.exchange(0); // Atomically get and clear fd - - if (fd > 0) { - ESP_LOGD(TAG, "Event source connection closed (fd: %d)", fd); - // Immediately shut down the socket to prevent lwIP from delivering more data - // This prevents "recv_tcp: recv for wrong pcb!" assertions when the TCP stack - // tries to deliver queued data after the session is marked as dead - // See: https://github.com/esphome/esphome/issues/11936 - shutdown(fd, SHUT_RDWR); - // Note: We don't close() the socket - httpd owns it and will close it - } - // Session will be cleaned up in the main loop to avoid race conditions + ESP_LOGD(TAG, "Event source connection closed (fd: %d)", fd); + // Mark as dead - will be cleaned up in the main loop + // Note: We don't delete or remove from set here to avoid race conditions + // httpd will call our custom close_fn (safe_close_with_shutdown) which handles + // shutdown() before close() to prevent lwIP race conditions } // helper for allowing only unique entries in the queue diff --git a/esphome/components/web_server_idf/web_server_idf.h b/esphome/components/web_server_idf/web_server_idf.h index b9f690b462..a139e9e4df 100644 --- a/esphome/components/web_server_idf/web_server_idf.h +++ b/esphome/components/web_server_idf/web_server_idf.h @@ -209,6 +209,7 @@ class AsyncWebServer { static esp_err_t request_handler(httpd_req_t *r); static esp_err_t request_post_handler(httpd_req_t *r); esp_err_t request_handler_(AsyncWebServerRequest *request) const; + static void safe_close_with_shutdown(httpd_handle_t hd, int sockfd); #ifdef USE_WEBSERVER_OTA esp_err_t handle_multipart_upload_(httpd_req_t *r, const char *content_type); #endif From 1157b4aee8b9c6a991854928a05f3169715fd0a6 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Thu, 20 Nov 2025 08:11:45 +1300 Subject: [PATCH 151/415] [epaper_spi] Add basic `7.3in-Spectra-E6` model (#12001) --- esphome/components/epaper_spi/display.py | 2 +- esphome/components/epaper_spi/models/spectra_e6.py | 8 ++++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/esphome/components/epaper_spi/display.py b/esphome/components/epaper_spi/display.py index 9ff393b397..182c37ba40 100644 --- a/esphome/components/epaper_spi/display.py +++ b/esphome/components/epaper_spi/display.py @@ -102,7 +102,7 @@ def customise_schema(config): """ config = cv.Schema( { - cv.Required(CONF_MODEL): cv.one_of(*MODELS, upper=True), + cv.Required(CONF_MODEL): cv.one_of(*MODELS, upper=True, space="-"), }, extra=cv.ALLOW_EXTRA, )(config) diff --git a/esphome/components/epaper_spi/models/spectra_e6.py b/esphome/components/epaper_spi/models/spectra_e6.py index 9f0b673d69..42a5a7da72 100644 --- a/esphome/components/epaper_spi/models/spectra_e6.py +++ b/esphome/components/epaper_spi/models/spectra_e6.py @@ -32,11 +32,15 @@ class SpectraE6(EpaperModel): spectra_e6 = SpectraE6("spectra-e6") -spectra_e6.extend( - "Seeed-reTerminal-E1002", +spectra_e6_7p3 = spectra_e6.extend( + "7.3in-Spectra-E6", width=800, height=480, data_rate="20MHz", +) + +spectra_e6_7p3.extend( + "Seeed-reTerminal-E1002", cs_pin=10, dc_pin=11, reset_pin=12, From c75abfb8949ede2182765abcdffab807565fbd05 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Wed, 19 Nov 2025 14:17:03 -0500 Subject: [PATCH 152/415] Bump version to 2025.11.0b5 --- Doxyfile | 2 +- esphome/const.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Doxyfile b/Doxyfile index c7b2187964..56373c5f69 100644 --- a/Doxyfile +++ b/Doxyfile @@ -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.0b4 +PROJECT_NUMBER = 2025.11.0b5 # 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 diff --git a/esphome/const.py b/esphome/const.py index 7a3a79f270..ddd36f69d7 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -4,7 +4,7 @@ from enum import Enum from esphome.enum import StrEnum -__version__ = "2025.11.0b4" +__version__ = "2025.11.0b5" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" VALID_SUBSTITUTIONS_CHARACTERS = ( From 2c3417062ae82512f51b85847ef5fe4deba41ebc Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 19 Nov 2025 13:47:40 -0600 Subject: [PATCH 153/415] Bump pyupgrade from 3.21.1 to 3.21.2 (#12002) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements_test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_test.txt b/requirements_test.txt index e238faa77e..7f6d3f8e26 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -1,7 +1,7 @@ pylint==4.0.3 flake8==7.3.0 # also change in .pre-commit-config.yaml when updating ruff==0.14.5 # also change in .pre-commit-config.yaml when updating -pyupgrade==3.21.1 # also change in .pre-commit-config.yaml when updating +pyupgrade==3.21.2 # also change in .pre-commit-config.yaml when updating pre-commit # Unit tests From 1e9c7d3c6dbc8e9e4da1367aaacf70c73dc3712b Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Wed, 19 Nov 2025 16:02:52 -0500 Subject: [PATCH 154/415] Bump version to 2025.11.0 --- Doxyfile | 2 +- esphome/const.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Doxyfile b/Doxyfile index 56373c5f69..1448fd010d 100644 --- a/Doxyfile +++ b/Doxyfile @@ -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.0b5 +PROJECT_NUMBER = 2025.11.0 # 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 diff --git a/esphome/const.py b/esphome/const.py index ddd36f69d7..3505ad169b 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -4,7 +4,7 @@ from enum import Enum from esphome.enum import StrEnum -__version__ = "2025.11.0b5" +__version__ = "2025.11.0" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" VALID_SUBSTITUTIONS_CHARACTERS = ( From 4398fd84d2ba21a3a4099946d554a6d70cc0e7ad Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Wed, 19 Nov 2025 19:09:22 -0500 Subject: [PATCH 155/415] [graph] Fix legend border (#12000) --- esphome/components/graph/graph.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/graph/graph.cpp b/esphome/components/graph/graph.cpp index 88bb306408..e3b9119108 100644 --- a/esphome/components/graph/graph.cpp +++ b/esphome/components/graph/graph.cpp @@ -337,7 +337,7 @@ void Graph::draw_legend(display::Display *buff, uint16_t x_offset, uint16_t y_of return; /// Plot border - if (this->border_) { + if (legend_->border_) { int w = legend_->width_; int h = legend_->height_; buff->horizontal_line(x_offset, y_offset, w, color); From f478e09972cbeaecc2dc2d453f542c2533116b2d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 19 Nov 2025 18:12:18 -0600 Subject: [PATCH 156/415] [network] Fix uninitialized type field in IPAddress esp_ip4_addr_t constructor --- esphome/components/network/ip_address.h | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/esphome/components/network/ip_address.h b/esphome/components/network/ip_address.h index 5ec6450cce..3d8b062d0b 100644 --- a/esphome/components/network/ip_address.h +++ b/esphome/components/network/ip_address.h @@ -81,7 +81,12 @@ struct IPAddress { ip_addr_.type = IPADDR_TYPE_V6; } #endif /* LWIP_IPV6 */ - IPAddress(esp_ip4_addr_t *other_ip) { memcpy((void *) &ip_addr_, (void *) other_ip, sizeof(esp_ip4_addr_t)); } + IPAddress(esp_ip4_addr_t *other_ip) { + memcpy((void *) &ip_addr_, (void *) other_ip, sizeof(esp_ip4_addr_t)); +#if LWIP_IPV6 + ip_addr_.type = IPADDR_TYPE_V4; +#endif + } IPAddress(esp_ip_addr_t *other_ip) { #if LWIP_IPV6 memcpy((void *) &ip_addr_, (void *) other_ip, sizeof(ip_addr_)); From 317a6082a13fb943b4e31389ad94eab3b2f71efc Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 19 Nov 2025 20:53:27 -0600 Subject: [PATCH 157/415] [select] Modernize namespace declarations to C++17 syntax --- esphome/components/select/automation.h | 6 ++---- esphome/components/select/select.cpp | 6 ++---- esphome/components/select/select.h | 6 ++---- esphome/components/select/select_call.cpp | 6 ++---- esphome/components/select/select_call.h | 6 ++---- esphome/components/select/select_traits.cpp | 6 ++---- esphome/components/select/select_traits.h | 6 ++---- 7 files changed, 14 insertions(+), 28 deletions(-) diff --git a/esphome/components/select/automation.h b/esphome/components/select/automation.h index 3e42eaf98a..768f2621f7 100644 --- a/esphome/components/select/automation.h +++ b/esphome/components/select/automation.h @@ -4,8 +4,7 @@ #include "esphome/core/component.h" #include "select.h" -namespace esphome { -namespace select { +namespace esphome::select { class SelectStateTrigger : public Trigger { public: @@ -63,5 +62,4 @@ template class SelectOperationAction : public Action { Select *select_; }; -} // namespace select -} // namespace esphome +} // namespace esphome::select diff --git a/esphome/components/select/select.cpp b/esphome/components/select/select.cpp index 9fe7a52422..3ec413f167 100644 --- a/esphome/components/select/select.cpp +++ b/esphome/components/select/select.cpp @@ -4,8 +4,7 @@ #include "esphome/core/log.h" #include -namespace esphome { -namespace select { +namespace esphome::select { static const char *const TAG = "select"; @@ -86,5 +85,4 @@ optional Select::at(size_t index) const { const char *Select::option_at(size_t index) const { return traits.get_options().at(index); } -} // namespace select -} // namespace esphome +} // namespace esphome::select diff --git a/esphome/components/select/select.h b/esphome/components/select/select.h index 7459c9d146..c4d7412d50 100644 --- a/esphome/components/select/select.h +++ b/esphome/components/select/select.h @@ -6,8 +6,7 @@ #include "select_call.h" #include "select_traits.h" -namespace esphome { -namespace select { +namespace esphome::select { #define LOG_SELECT(prefix, type, obj) \ if ((obj) != nullptr) { \ @@ -114,5 +113,4 @@ class Select : public EntityBase { CallbackManager state_callback_; }; -} // namespace select -} // namespace esphome +} // namespace esphome::select diff --git a/esphome/components/select/select_call.cpp b/esphome/components/select/select_call.cpp index aa7559e24e..aecfed0d64 100644 --- a/esphome/components/select/select_call.cpp +++ b/esphome/components/select/select_call.cpp @@ -2,8 +2,7 @@ #include "select.h" #include "esphome/core/log.h" -namespace esphome { -namespace select { +namespace esphome::select { static const char *const TAG = "select"; @@ -125,5 +124,4 @@ void SelectCall::perform() { parent->control(idx); } -} // namespace select -} // namespace esphome +} // namespace esphome::select diff --git a/esphome/components/select/select_call.h b/esphome/components/select/select_call.h index eae7d3de1d..b31d890ef6 100644 --- a/esphome/components/select/select_call.h +++ b/esphome/components/select/select_call.h @@ -2,8 +2,7 @@ #include "esphome/core/helpers.h" -namespace esphome { -namespace select { +namespace esphome::select { class Select; @@ -45,5 +44,4 @@ class SelectCall { bool cycle_; }; -} // namespace select -} // namespace esphome +} // namespace esphome::select diff --git a/esphome/components/select/select_traits.cpp b/esphome/components/select/select_traits.cpp index e5e12bdc7a..ff52c0d85b 100644 --- a/esphome/components/select/select_traits.cpp +++ b/esphome/components/select/select_traits.cpp @@ -1,7 +1,6 @@ #include "select_traits.h" -namespace esphome { -namespace select { +namespace esphome::select { void SelectTraits::set_options(const std::initializer_list &options) { this->options_ = options; } @@ -14,5 +13,4 @@ void SelectTraits::set_options(const FixedVector &options) { const FixedVector &SelectTraits::get_options() const { return this->options_; } -} // namespace select -} // namespace esphome +} // namespace esphome::select diff --git a/esphome/components/select/select_traits.h b/esphome/components/select/select_traits.h index ee59a030ad..78a83e5944 100644 --- a/esphome/components/select/select_traits.h +++ b/esphome/components/select/select_traits.h @@ -3,8 +3,7 @@ #include "esphome/core/helpers.h" #include -namespace esphome { -namespace select { +namespace esphome::select { class SelectTraits { public: @@ -16,5 +15,4 @@ class SelectTraits { FixedVector options_; }; -} // namespace select -} // namespace esphome +} // namespace esphome::select From da25951f6e714fe483f5c382a6188225b62509da Mon Sep 17 00:00:00 2001 From: David Woodhouse Date: Thu, 20 Nov 2025 03:01:32 +0000 Subject: [PATCH 158/415] [socket] Fix IPv6 address parsing for BSD sockets (#11996) --- esphome/components/socket/socket.cpp | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/esphome/components/socket/socket.cpp b/esphome/components/socket/socket.cpp index 1c8e72b8fd..cc9232d21a 100644 --- a/esphome/components/socket/socket.cpp +++ b/esphome/components/socket/socket.cpp @@ -61,9 +61,18 @@ socklen_t set_sockaddr(struct sockaddr *addr, socklen_t addrlen, const std::stri server->sin6_family = AF_INET6; server->sin6_port = htons(port); +#ifdef USE_SOCKET_IMPL_BSD_SOCKETS + // Use standard inet_pton for BSD sockets + if (inet_pton(AF_INET6, ip_address.c_str(), &server->sin6_addr) != 1) { + errno = EINVAL; + return 0; + } +#else + // Use LWIP-specific functions ip6_addr_t ip6; inet6_aton(ip_address.c_str(), &ip6); memcpy(server->sin6_addr.un.u32_addr, ip6.addr, sizeof(ip6.addr)); +#endif return sizeof(sockaddr_in6); } #endif /* USE_NETWORK_IPV6 */ From 83307684a3dcc3e183de98e060c9324206a7a4ab Mon Sep 17 00:00:00 2001 From: B48D81EFCC <111175947+B48D81EFCC@users.noreply.github.com> Date: Thu, 20 Nov 2025 04:58:39 +0100 Subject: [PATCH 159/415] [stts22h] Add support for STTS22H temperature sensor (#11778) Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> --- CODEOWNERS | 1 + esphome/components/stts22h/__init__.py | 1 + esphome/components/stts22h/sensor.py | 33 ++++++ esphome/components/stts22h/stts22h.cpp | 101 ++++++++++++++++++ esphome/components/stts22h/stts22h.h | 21 ++++ tests/components/stts22h/common.yaml | 4 + tests/components/stts22h/test.esp32-idf.yaml | 4 + .../components/stts22h/test.esp8266-ard.yaml | 4 + tests/components/stts22h/test.nrf52.yaml | 4 + tests/components/stts22h/test.rp2040-ard.yaml | 4 + 10 files changed, 177 insertions(+) create mode 100644 esphome/components/stts22h/__init__.py create mode 100644 esphome/components/stts22h/sensor.py create mode 100644 esphome/components/stts22h/stts22h.cpp create mode 100644 esphome/components/stts22h/stts22h.h create mode 100644 tests/components/stts22h/common.yaml create mode 100644 tests/components/stts22h/test.esp32-idf.yaml create mode 100644 tests/components/stts22h/test.esp8266-ard.yaml create mode 100644 tests/components/stts22h/test.nrf52.yaml create mode 100644 tests/components/stts22h/test.rp2040-ard.yaml diff --git a/CODEOWNERS b/CODEOWNERS index e6970af47c..250fbbd4d4 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -460,6 +460,7 @@ esphome/components/st7735/* @SenexCrenshaw esphome/components/st7789v/* @kbx81 esphome/components/st7920/* @marsjan155 esphome/components/statsd/* @Links2004 +esphome/components/stts22h/* @B48D81EFCC esphome/components/substitutions/* @esphome/core esphome/components/sun/* @OttoWinter esphome/components/sun_gtil2/* @Mat931 diff --git a/esphome/components/stts22h/__init__.py b/esphome/components/stts22h/__init__.py new file mode 100644 index 0000000000..a33c0b554b --- /dev/null +++ b/esphome/components/stts22h/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ["@B48D81EFCC"] diff --git a/esphome/components/stts22h/sensor.py b/esphome/components/stts22h/sensor.py new file mode 100644 index 0000000000..094c233361 --- /dev/null +++ b/esphome/components/stts22h/sensor.py @@ -0,0 +1,33 @@ +import esphome.codegen as cg +from esphome.components import i2c, sensor +import esphome.config_validation as cv +from esphome.const import ( + DEVICE_CLASS_TEMPERATURE, + STATE_CLASS_MEASUREMENT, + UNIT_CELSIUS, +) + +DEPENDENCIES = ["i2c"] + +sensor_ns = cg.esphome_ns.namespace("stts22h") +stts22h = sensor_ns.class_( + "STTS22HComponent", sensor.Sensor, cg.PollingComponent, i2c.I2CDevice +) + +CONFIG_SCHEMA = ( + sensor.sensor_schema( + stts22h, + accuracy_decimals=2, + unit_of_measurement=UNIT_CELSIUS, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + ) + .extend(cv.polling_component_schema("60s")) + .extend(i2c.i2c_device_schema(0x3C)) +) + + +async def to_code(config): + var = await sensor.new_sensor(config) + await cg.register_component(var, config) + await i2c.register_i2c_device(var, config) diff --git a/esphome/components/stts22h/stts22h.cpp b/esphome/components/stts22h/stts22h.cpp new file mode 100644 index 0000000000..614dc1da8b --- /dev/null +++ b/esphome/components/stts22h/stts22h.cpp @@ -0,0 +1,101 @@ +#include "esphome/core/log.h" +#include "stts22h.h" + +namespace esphome::stts22h { + +static const char *const TAG = "stts22h"; + +static const uint8_t WHOAMI_REG = 0x01; +static const uint8_t CTRL_REG = 0x04; +static const uint8_t TEMPERATURE_REG = 0x06; + +// CTRL_REG flags +static const uint8_t LOW_ODR_CTRL_ENABLE_FLAG = 0x80; // Flag to enable low ODR mode in CTRL_REG +static const uint8_t FREERUN_CTRL_ENABLE_FLAG = 0x04; // Flag to enable FREERUN mode in CTRL_REG +static const uint8_t ADD_INC_ENABLE_FLAG = 0x08; // Flag to enable ADD_INC (IF_ADD_INC) mode in CTRL_REG + +static const uint8_t WHOAMI_STTS22H_IDENTIFICATION = 0xA0; // ID value of STTS22H in WHOAMI_REG + +static const float SENSOR_SCALE = 0.01f; // Sensor resolution in degrees Celsius + +void STTS22HComponent::setup() { + // Check if device is a STTS22H + if (!this->is_stts22h_sensor_()) { + this->mark_failed("Device is not a STTS22H sensor"); + return; + } + + this->initialize_sensor_(); +} + +void STTS22HComponent::update() { + if (this->is_failed()) { + return; + } + + this->publish_state(this->read_temperature_()); +} + +void STTS22HComponent::dump_config() { + LOG_SENSOR("", "STTS22H", this); + LOG_I2C_DEVICE(this); + LOG_UPDATE_INTERVAL(this); + if (this->is_failed()) { + ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL); + } +} + +float STTS22HComponent::read_temperature_() { + uint8_t temp_reg_value[2]; + if (this->read_register(TEMPERATURE_REG, temp_reg_value, 2) != i2c::NO_ERROR) { + ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL); + return NAN; + } + + // Combine the two bytes into a single 16-bit signed integer + // The STTS22H temperature data is in two's complement format + int16_t temp_raw_value = static_cast(encode_uint16(temp_reg_value[1], temp_reg_value[0])); + return temp_raw_value * SENSOR_SCALE; // Apply sensor resolution +} + +bool STTS22HComponent::is_stts22h_sensor_() { + uint8_t whoami_value; + if (this->read_register(WHOAMI_REG, &whoami_value, 1) != i2c::NO_ERROR) { + this->mark_failed(ESP_LOG_MSG_COMM_FAIL); + return false; + } + + if (whoami_value != WHOAMI_STTS22H_IDENTIFICATION) { + this->mark_failed("Unexpected WHOAMI identifier. Sensor is not a STTS22H"); + return false; + } + + return true; +} + +void STTS22HComponent::initialize_sensor_() { + // Read current CTRL_REG configuration + uint8_t ctrl_value; + if (this->read_register(CTRL_REG, &ctrl_value, 1) != i2c::NO_ERROR) { + this->mark_failed(ESP_LOG_MSG_COMM_FAIL); + return; + } + + // Enable low ODR mode and enable ADD_INC + // Before low ODR mode can be used, + // FREERUN bit must be cleared (see sensor documentation) + ctrl_value &= ~FREERUN_CTRL_ENABLE_FLAG; // Clear FREERUN bit + if (this->write_register(CTRL_REG, &ctrl_value, 1) != i2c::NO_ERROR) { + this->mark_failed(ESP_LOG_MSG_COMM_FAIL); + return; + } + + // Enable LOW ODR mode and ADD_INC + ctrl_value |= LOW_ODR_CTRL_ENABLE_FLAG | ADD_INC_ENABLE_FLAG; // Set LOW ODR bit and ADD_INC bit + if (this->write_register(CTRL_REG, &ctrl_value, 1) != i2c::NO_ERROR) { + this->mark_failed(ESP_LOG_MSG_COMM_FAIL); + return; + } +} + +} // namespace esphome::stts22h diff --git a/esphome/components/stts22h/stts22h.h b/esphome/components/stts22h/stts22h.h new file mode 100644 index 0000000000..442a263e49 --- /dev/null +++ b/esphome/components/stts22h/stts22h.h @@ -0,0 +1,21 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/i2c/i2c.h" + +namespace esphome::stts22h { + +class STTS22HComponent : public sensor::Sensor, public PollingComponent, public i2c::I2CDevice { + public: + void setup() override; + void update() override; + void dump_config() override; + + protected: + void initialize_sensor_(); + bool is_stts22h_sensor_(); + float read_temperature_(); +}; + +} // namespace esphome::stts22h diff --git a/tests/components/stts22h/common.yaml b/tests/components/stts22h/common.yaml new file mode 100644 index 0000000000..802afe2065 --- /dev/null +++ b/tests/components/stts22h/common.yaml @@ -0,0 +1,4 @@ +sensor: + - platform: stts22h + name: Temperature + update_interval: 15s diff --git a/tests/components/stts22h/test.esp32-idf.yaml b/tests/components/stts22h/test.esp32-idf.yaml new file mode 100644 index 0000000000..b47e39c389 --- /dev/null +++ b/tests/components/stts22h/test.esp32-idf.yaml @@ -0,0 +1,4 @@ +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml + +<<: !include common.yaml diff --git a/tests/components/stts22h/test.esp8266-ard.yaml b/tests/components/stts22h/test.esp8266-ard.yaml new file mode 100644 index 0000000000..4a98b9388a --- /dev/null +++ b/tests/components/stts22h/test.esp8266-ard.yaml @@ -0,0 +1,4 @@ +packages: + i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml + +<<: !include common.yaml diff --git a/tests/components/stts22h/test.nrf52.yaml b/tests/components/stts22h/test.nrf52.yaml new file mode 100644 index 0000000000..2a0de6241c --- /dev/null +++ b/tests/components/stts22h/test.nrf52.yaml @@ -0,0 +1,4 @@ +packages: + i2c: !include ../../test_build_components/common/i2c/nrf52.yaml + +<<: !include common.yaml diff --git a/tests/components/stts22h/test.rp2040-ard.yaml b/tests/components/stts22h/test.rp2040-ard.yaml new file mode 100644 index 0000000000..319a7c71a6 --- /dev/null +++ b/tests/components/stts22h/test.rp2040-ard.yaml @@ -0,0 +1,4 @@ +packages: + i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml + +<<: !include common.yaml From b400a98fb377826f3bac40b67c469c12dfc8b0fd Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 19 Nov 2025 22:17:16 -0600 Subject: [PATCH 160/415] [api] Store Home Assistant state subscriptions in flash instead of heap --- esphome/components/api/api_connection.cpp | 22 ++++-- esphome/components/api/api_server.cpp | 74 +++++++++++++++---- esphome/components/api/api_server.h | 20 ++++- .../homeassistant_binary_sensor.cpp | 13 ++-- .../homeassistant_binary_sensor.h | 8 +- .../number/homeassistant_number.cpp | 23 +++--- .../number/homeassistant_number.h | 4 +- .../sensor/homeassistant_sensor.cpp | 15 ++-- .../sensor/homeassistant_sensor.h | 8 +- .../switch/homeassistant_switch.cpp | 6 +- .../switch/homeassistant_switch.h | 4 +- .../text_sensor/homeassistant_text_sensor.cpp | 13 ++-- .../text_sensor/homeassistant_text_sensor.h | 8 +- 13 files changed, 141 insertions(+), 77 deletions(-) diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 4acd2fc15c..f968da591e 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -1535,8 +1535,18 @@ bool APIConnection::send_device_info_response(const DeviceInfoRequest &msg) { #ifdef USE_API_HOMEASSISTANT_STATES void APIConnection::on_home_assistant_state_response(const HomeAssistantStateResponse &msg) { for (auto &it : this->parent_->get_state_subs()) { - if (it.entity_id == msg.entity_id && it.attribute.value() == msg.attribute) { - it.callback(msg.state); + // Compare entity_id and attribute with message fields + bool entity_match = (strcmp(it.entity_id_, msg.entity_id.c_str()) == 0); + bool attribute_match = false; + + if (it.has_attribute_) { + attribute_match = (strcmp(it.attribute_, msg.attribute.c_str()) == 0); + } else { + attribute_match = msg.attribute.empty(); + } + + if (entity_match && attribute_match) { + it.callback_(msg.state); } } } @@ -1873,12 +1883,12 @@ void APIConnection::process_state_subscriptions_() { const auto &it = subs[this->state_subs_at_]; SubscribeHomeAssistantStateResponse resp; - resp.set_entity_id(StringRef(it.entity_id)); + resp.set_entity_id(StringRef(it.entity_id_)); - // Avoid string copy by directly using the optional's value if it exists - resp.set_attribute(it.attribute.has_value() ? StringRef(it.attribute.value()) : StringRef("")); + // Avoid string copy by using the const char* pointer if it exists + resp.set_attribute(it.has_attribute_ ? StringRef(it.attribute_) : StringRef("")); - resp.once = it.once; + resp.once = it.once_; if (this->send_message(resp, SubscribeHomeAssistantStateResponse::MESSAGE_TYPE)) { this->state_subs_at_++; } diff --git a/esphome/components/api/api_server.cpp b/esphome/components/api/api_server.cpp index 18601d74ff..61e22ef3eb 100644 --- a/esphome/components/api/api_server.cpp +++ b/esphome/components/api/api_server.cpp @@ -431,25 +431,73 @@ void APIServer::handle_action_response(uint32_t call_id, bool success, const std #endif // USE_API_HOMEASSISTANT_SERVICES #ifdef USE_API_HOMEASSISTANT_STATES +// New const char* overload (for internal components - zero allocation) +void APIServer::subscribe_home_assistant_state(const char *entity_id, const char *attribute, + std::function f) { + HomeAssistantStateSubscription sub; + sub.entity_id_ = entity_id; + sub.attribute_ = attribute; + sub.callback_ = std::move(f); + sub.once_ = false; + sub.has_attribute_ = (attribute != nullptr); + // entity_id_copy_ and attribute_copy_ remain nullptr (no heap allocation) + this->state_subs_.push_back(std::move(sub)); +} + +void APIServer::get_home_assistant_state(const char *entity_id, const char *attribute, + std::function f) { + HomeAssistantStateSubscription sub; + sub.entity_id_ = entity_id; + sub.attribute_ = attribute; + sub.callback_ = std::move(f); + sub.once_ = true; + sub.has_attribute_ = (attribute != nullptr); + // entity_id_copy_ and attribute_copy_ remain nullptr (no heap allocation) + this->state_subs_.push_back(std::move(sub)); +} + +// Existing std::string overload (for custom_api_device.h - heap allocation) void APIServer::subscribe_home_assistant_state(std::string entity_id, optional attribute, std::function f) { - this->state_subs_.push_back(HomeAssistantStateSubscription{ - .entity_id = std::move(entity_id), - .attribute = std::move(attribute), - .callback = std::move(f), - .once = false, - }); + HomeAssistantStateSubscription sub; + // Allocate heap storage for the strings + sub.entity_id_copy_ = std::make_unique(std::move(entity_id)); + sub.entity_id_ = sub.entity_id_copy_->c_str(); + + if (attribute.has_value()) { + sub.attribute_copy_ = std::make_unique(std::move(attribute.value())); + sub.attribute_ = sub.attribute_copy_->c_str(); + sub.has_attribute_ = true; + } else { + sub.attribute_ = nullptr; + sub.has_attribute_ = false; + } + + sub.callback_ = std::move(f); + sub.once_ = false; + this->state_subs_.push_back(std::move(sub)); } void APIServer::get_home_assistant_state(std::string entity_id, optional attribute, std::function f) { - this->state_subs_.push_back(HomeAssistantStateSubscription{ - .entity_id = std::move(entity_id), - .attribute = std::move(attribute), - .callback = std::move(f), - .once = true, - }); -}; + HomeAssistantStateSubscription sub; + // Allocate heap storage for the strings + sub.entity_id_copy_ = std::make_unique(std::move(entity_id)); + sub.entity_id_ = sub.entity_id_copy_->c_str(); + + if (attribute.has_value()) { + sub.attribute_copy_ = std::make_unique(std::move(attribute.value())); + sub.attribute_ = sub.attribute_copy_->c_str(); + sub.has_attribute_ = true; + } else { + sub.attribute_ = nullptr; + sub.has_attribute_ = false; + } + + sub.callback_ = std::move(f); + sub.once_ = true; + this->state_subs_.push_back(std::move(sub)); +} const std::vector &APIServer::get_state_subs() const { return this->state_subs_; diff --git a/esphome/components/api/api_server.h b/esphome/components/api/api_server.h index 2d58063d6c..736b8e2b06 100644 --- a/esphome/components/api/api_server.h +++ b/esphome/components/api/api_server.h @@ -154,16 +154,28 @@ class APIServer : public Component, public Controller { #ifdef USE_API_HOMEASSISTANT_STATES struct HomeAssistantStateSubscription { - std::string entity_id; - optional attribute; - std::function callback; - bool once; + const char *entity_id_; // Pointer to flash (internal) or heap (external) + const char *attribute_; // Pointer to flash or nullptr + std::function callback_; + bool once_; + bool has_attribute_; + + // Storage for external components using std::string API (custom_api_device.h) + // These are only allocated when using the std::string overload + std::unique_ptr entity_id_copy_; + std::unique_ptr attribute_copy_; }; + // New const char* overload (for internal components - zero allocation) + void subscribe_home_assistant_state(const char *entity_id, const char *attribute, std::function f); + void get_home_assistant_state(const char *entity_id, const char *attribute, std::function f); + + // Existing std::string overload (for custom_api_device.h - heap allocation) void subscribe_home_assistant_state(std::string entity_id, optional attribute, std::function f); void get_home_assistant_state(std::string entity_id, optional attribute, std::function f); + const std::vector &get_state_subs() const; #endif #ifdef USE_API_SERVICES diff --git a/esphome/components/homeassistant/binary_sensor/homeassistant_binary_sensor.cpp b/esphome/components/homeassistant/binary_sensor/homeassistant_binary_sensor.cpp index a36fcb204a..5652e7d603 100644 --- a/esphome/components/homeassistant/binary_sensor/homeassistant_binary_sensor.cpp +++ b/esphome/components/homeassistant/binary_sensor/homeassistant_binary_sensor.cpp @@ -19,11 +19,10 @@ void HomeassistantBinarySensor::setup() { case PARSE_ON: case PARSE_OFF: bool new_state = val == PARSE_ON; - if (this->attribute_.has_value()) { - ESP_LOGD(TAG, "'%s::%s': Got attribute state %s", this->entity_id_.c_str(), - this->attribute_.value().c_str(), ONOFF(new_state)); + if (this->attribute_ != nullptr) { + ESP_LOGD(TAG, "'%s::%s': Got attribute state %s", this->entity_id_, this->attribute_, ONOFF(new_state)); } else { - ESP_LOGD(TAG, "'%s': Got state %s", this->entity_id_.c_str(), ONOFF(new_state)); + ESP_LOGD(TAG, "'%s': Got state %s", this->entity_id_, ONOFF(new_state)); } if (this->initial_) { this->publish_initial_state(new_state); @@ -37,9 +36,9 @@ void HomeassistantBinarySensor::setup() { } void HomeassistantBinarySensor::dump_config() { LOG_BINARY_SENSOR("", "Homeassistant Binary Sensor", this); - ESP_LOGCONFIG(TAG, " Entity ID: '%s'", this->entity_id_.c_str()); - if (this->attribute_.has_value()) { - ESP_LOGCONFIG(TAG, " Attribute: '%s'", this->attribute_.value().c_str()); + ESP_LOGCONFIG(TAG, " Entity ID: '%s'", this->entity_id_); + if (this->attribute_ != nullptr) { + ESP_LOGCONFIG(TAG, " Attribute: '%s'", this->attribute_); } } float HomeassistantBinarySensor::get_setup_priority() const { return setup_priority::AFTER_WIFI; } diff --git a/esphome/components/homeassistant/binary_sensor/homeassistant_binary_sensor.h b/esphome/components/homeassistant/binary_sensor/homeassistant_binary_sensor.h index 7026496295..9aec61a370 100644 --- a/esphome/components/homeassistant/binary_sensor/homeassistant_binary_sensor.h +++ b/esphome/components/homeassistant/binary_sensor/homeassistant_binary_sensor.h @@ -8,15 +8,15 @@ namespace homeassistant { class HomeassistantBinarySensor : public binary_sensor::BinarySensor, public Component { public: - void set_entity_id(const std::string &entity_id) { entity_id_ = entity_id; } - void set_attribute(const std::string &attribute) { attribute_ = attribute; } + void set_entity_id(const char *entity_id) { this->entity_id_ = entity_id; } + void set_attribute(const char *attribute) { this->attribute_ = attribute; } void setup() override; void dump_config() override; float get_setup_priority() const override; protected: - std::string entity_id_; - optional attribute_; + const char *entity_id_{nullptr}; + const char *attribute_{nullptr}; bool initial_{true}; }; diff --git a/esphome/components/homeassistant/number/homeassistant_number.cpp b/esphome/components/homeassistant/number/homeassistant_number.cpp index 9963f3431d..1ca90180eb 100644 --- a/esphome/components/homeassistant/number/homeassistant_number.cpp +++ b/esphome/components/homeassistant/number/homeassistant_number.cpp @@ -12,21 +12,21 @@ static const char *const TAG = "homeassistant.number"; void HomeassistantNumber::state_changed_(const std::string &state) { auto number_value = parse_number(state); if (!number_value.has_value()) { - ESP_LOGW(TAG, "'%s': Can't convert '%s' to number!", this->entity_id_.c_str(), state.c_str()); + ESP_LOGW(TAG, "'%s': Can't convert '%s' to number!", this->entity_id_, state.c_str()); this->publish_state(NAN); return; } if (this->state == number_value.value()) { return; } - ESP_LOGD(TAG, "'%s': Got state %s", this->entity_id_.c_str(), state.c_str()); + ESP_LOGD(TAG, "'%s': Got state %s", this->entity_id_, state.c_str()); this->publish_state(number_value.value()); } void HomeassistantNumber::min_retrieved_(const std::string &min) { auto min_value = parse_number(min); if (!min_value.has_value()) { - ESP_LOGE(TAG, "'%s': Can't convert 'min' value '%s' to number!", this->entity_id_.c_str(), min.c_str()); + ESP_LOGE(TAG, "'%s': Can't convert 'min' value '%s' to number!", this->entity_id_, min.c_str()); return; } ESP_LOGD(TAG, "'%s': Min retrieved: %s", get_name().c_str(), min.c_str()); @@ -36,7 +36,7 @@ void HomeassistantNumber::min_retrieved_(const std::string &min) { void HomeassistantNumber::max_retrieved_(const std::string &max) { auto max_value = parse_number(max); if (!max_value.has_value()) { - ESP_LOGE(TAG, "'%s': Can't convert 'max' value '%s' to number!", this->entity_id_.c_str(), max.c_str()); + ESP_LOGE(TAG, "'%s': Can't convert 'max' value '%s' to number!", this->entity_id_, max.c_str()); return; } ESP_LOGD(TAG, "'%s': Max retrieved: %s", get_name().c_str(), max.c_str()); @@ -46,7 +46,7 @@ void HomeassistantNumber::max_retrieved_(const std::string &max) { void HomeassistantNumber::step_retrieved_(const std::string &step) { auto step_value = parse_number(step); if (!step_value.has_value()) { - ESP_LOGE(TAG, "'%s': Can't convert 'step' value '%s' to number!", this->entity_id_.c_str(), step.c_str()); + ESP_LOGE(TAG, "'%s': Can't convert 'step' value '%s' to number!", this->entity_id_, step.c_str()); return; } ESP_LOGD(TAG, "'%s': Step Retrieved %s", get_name().c_str(), step.c_str()); @@ -55,22 +55,19 @@ void HomeassistantNumber::step_retrieved_(const std::string &step) { void HomeassistantNumber::setup() { api::global_api_server->subscribe_home_assistant_state( - this->entity_id_, nullopt, std::bind(&HomeassistantNumber::state_changed_, this, std::placeholders::_1)); + this->entity_id_, nullptr, std::bind(&HomeassistantNumber::state_changed_, this, std::placeholders::_1)); api::global_api_server->get_home_assistant_state( - this->entity_id_, optional("min"), - std::bind(&HomeassistantNumber::min_retrieved_, this, std::placeholders::_1)); + this->entity_id_, "min", std::bind(&HomeassistantNumber::min_retrieved_, this, std::placeholders::_1)); api::global_api_server->get_home_assistant_state( - this->entity_id_, optional("max"), - std::bind(&HomeassistantNumber::max_retrieved_, this, std::placeholders::_1)); + this->entity_id_, "max", std::bind(&HomeassistantNumber::max_retrieved_, this, std::placeholders::_1)); api::global_api_server->get_home_assistant_state( - this->entity_id_, optional("step"), - std::bind(&HomeassistantNumber::step_retrieved_, this, std::placeholders::_1)); + this->entity_id_, "step", std::bind(&HomeassistantNumber::step_retrieved_, this, std::placeholders::_1)); } void HomeassistantNumber::dump_config() { LOG_NUMBER("", "Homeassistant Number", this); - ESP_LOGCONFIG(TAG, " Entity ID: '%s'", this->entity_id_.c_str()); + ESP_LOGCONFIG(TAG, " Entity ID: '%s'", this->entity_id_); } float HomeassistantNumber::get_setup_priority() const { return setup_priority::AFTER_CONNECTION; } diff --git a/esphome/components/homeassistant/number/homeassistant_number.h b/esphome/components/homeassistant/number/homeassistant_number.h index 0860b4e91c..0dffc108cb 100644 --- a/esphome/components/homeassistant/number/homeassistant_number.h +++ b/esphome/components/homeassistant/number/homeassistant_number.h @@ -11,7 +11,7 @@ namespace homeassistant { class HomeassistantNumber : public number::Number, public Component { public: - void set_entity_id(const std::string &entity_id) { this->entity_id_ = entity_id; } + void set_entity_id(const char *entity_id) { this->entity_id_ = entity_id; } void setup() override; void dump_config() override; @@ -25,7 +25,7 @@ class HomeassistantNumber : public number::Number, public Component { void control(float value) override; - std::string entity_id_; + const char *entity_id_{nullptr}; }; } // namespace homeassistant } // namespace esphome diff --git a/esphome/components/homeassistant/sensor/homeassistant_sensor.cpp b/esphome/components/homeassistant/sensor/homeassistant_sensor.cpp index 35e660f7c1..78da47f9a1 100644 --- a/esphome/components/homeassistant/sensor/homeassistant_sensor.cpp +++ b/esphome/components/homeassistant/sensor/homeassistant_sensor.cpp @@ -12,25 +12,24 @@ void HomeassistantSensor::setup() { this->entity_id_, this->attribute_, [this](const std::string &state) { auto val = parse_number(state); if (!val.has_value()) { - ESP_LOGW(TAG, "'%s': Can't convert '%s' to number!", this->entity_id_.c_str(), state.c_str()); + ESP_LOGW(TAG, "'%s': Can't convert '%s' to number!", this->entity_id_, state.c_str()); this->publish_state(NAN); return; } - if (this->attribute_.has_value()) { - ESP_LOGD(TAG, "'%s::%s': Got attribute state %.2f", this->entity_id_.c_str(), - this->attribute_.value().c_str(), *val); + if (this->attribute_ != nullptr) { + ESP_LOGD(TAG, "'%s::%s': Got attribute state %.2f", this->entity_id_, this->attribute_, *val); } else { - ESP_LOGD(TAG, "'%s': Got state %.2f", this->entity_id_.c_str(), *val); + ESP_LOGD(TAG, "'%s': Got state %.2f", this->entity_id_, *val); } this->publish_state(*val); }); } void HomeassistantSensor::dump_config() { LOG_SENSOR("", "Homeassistant Sensor", this); - ESP_LOGCONFIG(TAG, " Entity ID: '%s'", this->entity_id_.c_str()); - if (this->attribute_.has_value()) { - ESP_LOGCONFIG(TAG, " Attribute: '%s'", this->attribute_.value().c_str()); + ESP_LOGCONFIG(TAG, " Entity ID: '%s'", this->entity_id_); + if (this->attribute_ != nullptr) { + ESP_LOGCONFIG(TAG, " Attribute: '%s'", this->attribute_); } } float HomeassistantSensor::get_setup_priority() const { return setup_priority::AFTER_CONNECTION; } diff --git a/esphome/components/homeassistant/sensor/homeassistant_sensor.h b/esphome/components/homeassistant/sensor/homeassistant_sensor.h index 53b288d7d4..d89fc069ff 100644 --- a/esphome/components/homeassistant/sensor/homeassistant_sensor.h +++ b/esphome/components/homeassistant/sensor/homeassistant_sensor.h @@ -8,15 +8,15 @@ namespace homeassistant { class HomeassistantSensor : public sensor::Sensor, public Component { public: - void set_entity_id(const std::string &entity_id) { entity_id_ = entity_id; } - void set_attribute(const std::string &attribute) { attribute_ = attribute; } + void set_entity_id(const char *entity_id) { this->entity_id_ = entity_id; } + void set_attribute(const char *attribute) { this->attribute_ = attribute; } void setup() override; void dump_config() override; float get_setup_priority() const override; protected: - std::string entity_id_; - optional attribute_; + const char *entity_id_{nullptr}; + const char *attribute_{nullptr}; }; } // namespace homeassistant diff --git a/esphome/components/homeassistant/switch/homeassistant_switch.cpp b/esphome/components/homeassistant/switch/homeassistant_switch.cpp index 27d3705fc2..c4abf2295d 100644 --- a/esphome/components/homeassistant/switch/homeassistant_switch.cpp +++ b/esphome/components/homeassistant/switch/homeassistant_switch.cpp @@ -10,7 +10,7 @@ static const char *const TAG = "homeassistant.switch"; using namespace esphome::switch_; void HomeassistantSwitch::setup() { - api::global_api_server->subscribe_home_assistant_state(this->entity_id_, nullopt, [this](const std::string &state) { + api::global_api_server->subscribe_home_assistant_state(this->entity_id_, nullptr, [this](const std::string &state) { auto val = parse_on_off(state.c_str()); switch (val) { case PARSE_NONE: @@ -20,7 +20,7 @@ void HomeassistantSwitch::setup() { case PARSE_ON: case PARSE_OFF: bool new_state = val == PARSE_ON; - ESP_LOGD(TAG, "'%s': Got state %s", this->entity_id_.c_str(), ONOFF(new_state)); + ESP_LOGD(TAG, "'%s': Got state %s", this->entity_id_, ONOFF(new_state)); this->publish_state(new_state); break; } @@ -29,7 +29,7 @@ void HomeassistantSwitch::setup() { void HomeassistantSwitch::dump_config() { LOG_SWITCH("", "Homeassistant Switch", this); - ESP_LOGCONFIG(TAG, " Entity ID: '%s'", this->entity_id_.c_str()); + ESP_LOGCONFIG(TAG, " Entity ID: '%s'", this->entity_id_); } float HomeassistantSwitch::get_setup_priority() const { return setup_priority::AFTER_CONNECTION; } diff --git a/esphome/components/homeassistant/switch/homeassistant_switch.h b/esphome/components/homeassistant/switch/homeassistant_switch.h index a4da257960..c180b7f98a 100644 --- a/esphome/components/homeassistant/switch/homeassistant_switch.h +++ b/esphome/components/homeassistant/switch/homeassistant_switch.h @@ -8,14 +8,14 @@ namespace homeassistant { class HomeassistantSwitch : public switch_::Switch, public Component { public: - void set_entity_id(const std::string &entity_id) { this->entity_id_ = entity_id; } + void set_entity_id(const char *entity_id) { this->entity_id_ = entity_id; } void setup() override; void dump_config() override; float get_setup_priority() const override; protected: void write_state(bool state) override; - std::string entity_id_; + const char *entity_id_{nullptr}; }; } // namespace homeassistant diff --git a/esphome/components/homeassistant/text_sensor/homeassistant_text_sensor.cpp b/esphome/components/homeassistant/text_sensor/homeassistant_text_sensor.cpp index 9b933fbbbe..6154330a4e 100644 --- a/esphome/components/homeassistant/text_sensor/homeassistant_text_sensor.cpp +++ b/esphome/components/homeassistant/text_sensor/homeassistant_text_sensor.cpp @@ -10,20 +10,19 @@ static const char *const TAG = "homeassistant.text_sensor"; void HomeassistantTextSensor::setup() { api::global_api_server->subscribe_home_assistant_state( this->entity_id_, this->attribute_, [this](const std::string &state) { - if (this->attribute_.has_value()) { - ESP_LOGD(TAG, "'%s::%s': Got attribute state '%s'", this->entity_id_.c_str(), - this->attribute_.value().c_str(), state.c_str()); + if (this->attribute_ != nullptr) { + ESP_LOGD(TAG, "'%s::%s': Got attribute state '%s'", this->entity_id_, this->attribute_, state.c_str()); } else { - ESP_LOGD(TAG, "'%s': Got state '%s'", this->entity_id_.c_str(), state.c_str()); + ESP_LOGD(TAG, "'%s': Got state '%s'", this->entity_id_, state.c_str()); } this->publish_state(state); }); } void HomeassistantTextSensor::dump_config() { LOG_TEXT_SENSOR("", "Homeassistant Text Sensor", this); - ESP_LOGCONFIG(TAG, " Entity ID: '%s'", this->entity_id_.c_str()); - if (this->attribute_.has_value()) { - ESP_LOGCONFIG(TAG, " Attribute: '%s'", this->attribute_.value().c_str()); + ESP_LOGCONFIG(TAG, " Entity ID: '%s'", this->entity_id_); + if (this->attribute_ != nullptr) { + ESP_LOGCONFIG(TAG, " Attribute: '%s'", this->attribute_); } } float HomeassistantTextSensor::get_setup_priority() const { return setup_priority::AFTER_CONNECTION; } diff --git a/esphome/components/homeassistant/text_sensor/homeassistant_text_sensor.h b/esphome/components/homeassistant/text_sensor/homeassistant_text_sensor.h index ce6b2c2c3f..4d66c65a17 100644 --- a/esphome/components/homeassistant/text_sensor/homeassistant_text_sensor.h +++ b/esphome/components/homeassistant/text_sensor/homeassistant_text_sensor.h @@ -8,15 +8,15 @@ namespace homeassistant { class HomeassistantTextSensor : public text_sensor::TextSensor, public Component { public: - void set_entity_id(const std::string &entity_id) { entity_id_ = entity_id; } - void set_attribute(const std::string &attribute) { attribute_ = attribute; } + void set_entity_id(const char *entity_id) { this->entity_id_ = entity_id; } + void set_attribute(const char *attribute) { this->attribute_ = attribute; } void setup() override; void dump_config() override; float get_setup_priority() const override; protected: - std::string entity_id_; - optional attribute_; + const char *entity_id_{nullptr}; + const char *attribute_{nullptr}; }; } // namespace homeassistant From c39d17f86486ad5a171a74502f15208523be5e32 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 19 Nov 2025 22:20:12 -0600 Subject: [PATCH 161/415] cleanup --- esphome/components/api/api_server.cpp | 32 +++++++++++++-------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/esphome/components/api/api_server.cpp b/esphome/components/api/api_server.cpp index 61e22ef3eb..aac45804b8 100644 --- a/esphome/components/api/api_server.cpp +++ b/esphome/components/api/api_server.cpp @@ -434,26 +434,26 @@ void APIServer::handle_action_response(uint32_t call_id, bool success, const std // New const char* overload (for internal components - zero allocation) void APIServer::subscribe_home_assistant_state(const char *entity_id, const char *attribute, std::function f) { - HomeAssistantStateSubscription sub; - sub.entity_id_ = entity_id; - sub.attribute_ = attribute; - sub.callback_ = std::move(f); - sub.once_ = false; - sub.has_attribute_ = (attribute != nullptr); - // entity_id_copy_ and attribute_copy_ remain nullptr (no heap allocation) - this->state_subs_.push_back(std::move(sub)); + this->state_subs_.push_back(HomeAssistantStateSubscription{ + .entity_id_ = entity_id, + .attribute_ = attribute, + .callback_ = std::move(f), + .once_ = false, + .has_attribute_ = (attribute != nullptr), + // entity_id_copy_ and attribute_copy_ remain nullptr (no heap allocation) + }); } void APIServer::get_home_assistant_state(const char *entity_id, const char *attribute, std::function f) { - HomeAssistantStateSubscription sub; - sub.entity_id_ = entity_id; - sub.attribute_ = attribute; - sub.callback_ = std::move(f); - sub.once_ = true; - sub.has_attribute_ = (attribute != nullptr); - // entity_id_copy_ and attribute_copy_ remain nullptr (no heap allocation) - this->state_subs_.push_back(std::move(sub)); + this->state_subs_.push_back(HomeAssistantStateSubscription{ + .entity_id_ = entity_id, + .attribute_ = attribute, + .callback_ = std::move(f), + .once_ = true, + .has_attribute_ = (attribute != nullptr), + // entity_id_copy_ and attribute_copy_ remain nullptr (no heap allocation) + }); } // Existing std::string overload (for custom_api_device.h - heap allocation) From 185c1dec43d397c61d5b7568286518d34b5244a5 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 19 Nov 2025 22:24:00 -0600 Subject: [PATCH 162/415] cleanup --- esphome/components/api/api_server.cpp | 81 ++++++++++++--------------- esphome/components/api/api_server.h | 7 +++ 2 files changed, 42 insertions(+), 46 deletions(-) diff --git a/esphome/components/api/api_server.cpp b/esphome/components/api/api_server.cpp index aac45804b8..3b073038c6 100644 --- a/esphome/components/api/api_server.cpp +++ b/esphome/components/api/api_server.cpp @@ -431,72 +431,61 @@ void APIServer::handle_action_response(uint32_t call_id, bool success, const std #endif // USE_API_HOMEASSISTANT_SERVICES #ifdef USE_API_HOMEASSISTANT_STATES -// New const char* overload (for internal components - zero allocation) -void APIServer::subscribe_home_assistant_state(const char *entity_id, const char *attribute, - std::function f) { +// Helper to add subscription (reduces duplication) +void APIServer::add_state_subscription_(const char *entity_id, const char *attribute, + std::function f, bool once) { this->state_subs_.push_back(HomeAssistantStateSubscription{ .entity_id_ = entity_id, .attribute_ = attribute, .callback_ = std::move(f), - .once_ = false, + .once_ = once, .has_attribute_ = (attribute != nullptr), // entity_id_copy_ and attribute_copy_ remain nullptr (no heap allocation) }); } +// Helper to add subscription with heap-allocated strings (reduces duplication) +void APIServer::add_state_subscription_(std::string entity_id, optional attribute, + std::function f, bool once) { + HomeAssistantStateSubscription sub; + // Allocate heap storage for the strings + sub.entity_id_copy_ = std::make_unique(std::move(entity_id)); + sub.entity_id_ = sub.entity_id_copy_->c_str(); + + if (attribute.has_value()) { + sub.attribute_copy_ = std::make_unique(std::move(attribute.value())); + sub.attribute_ = sub.attribute_copy_->c_str(); + sub.has_attribute_ = true; + } else { + sub.attribute_ = nullptr; + sub.has_attribute_ = false; + } + + sub.callback_ = std::move(f); + sub.once_ = once; + this->state_subs_.push_back(std::move(sub)); +} + +// New const char* overload (for internal components - zero allocation) +void APIServer::subscribe_home_assistant_state(const char *entity_id, const char *attribute, + std::function f) { + this->add_state_subscription_(entity_id, attribute, std::move(f), false); +} + void APIServer::get_home_assistant_state(const char *entity_id, const char *attribute, std::function f) { - this->state_subs_.push_back(HomeAssistantStateSubscription{ - .entity_id_ = entity_id, - .attribute_ = attribute, - .callback_ = std::move(f), - .once_ = true, - .has_attribute_ = (attribute != nullptr), - // entity_id_copy_ and attribute_copy_ remain nullptr (no heap allocation) - }); + this->add_state_subscription_(entity_id, attribute, std::move(f), true); } // Existing std::string overload (for custom_api_device.h - heap allocation) void APIServer::subscribe_home_assistant_state(std::string entity_id, optional attribute, std::function f) { - HomeAssistantStateSubscription sub; - // Allocate heap storage for the strings - sub.entity_id_copy_ = std::make_unique(std::move(entity_id)); - sub.entity_id_ = sub.entity_id_copy_->c_str(); - - if (attribute.has_value()) { - sub.attribute_copy_ = std::make_unique(std::move(attribute.value())); - sub.attribute_ = sub.attribute_copy_->c_str(); - sub.has_attribute_ = true; - } else { - sub.attribute_ = nullptr; - sub.has_attribute_ = false; - } - - sub.callback_ = std::move(f); - sub.once_ = false; - this->state_subs_.push_back(std::move(sub)); + this->add_state_subscription_(std::move(entity_id), std::move(attribute), std::move(f), false); } void APIServer::get_home_assistant_state(std::string entity_id, optional attribute, std::function f) { - HomeAssistantStateSubscription sub; - // Allocate heap storage for the strings - sub.entity_id_copy_ = std::make_unique(std::move(entity_id)); - sub.entity_id_ = sub.entity_id_copy_->c_str(); - - if (attribute.has_value()) { - sub.attribute_copy_ = std::make_unique(std::move(attribute.value())); - sub.attribute_ = sub.attribute_copy_->c_str(); - sub.has_attribute_ = true; - } else { - sub.attribute_ = nullptr; - sub.has_attribute_ = false; - } - - sub.callback_ = std::move(f); - sub.once_ = true; - this->state_subs_.push_back(std::move(sub)); + this->add_state_subscription_(std::move(entity_id), std::move(attribute), std::move(f), true); } const std::vector &APIServer::get_state_subs() const { diff --git a/esphome/components/api/api_server.h b/esphome/components/api/api_server.h index 736b8e2b06..697142cbe5 100644 --- a/esphome/components/api/api_server.h +++ b/esphome/components/api/api_server.h @@ -197,6 +197,13 @@ class APIServer : public Component, public Controller { bool update_noise_psk_(const SavedNoisePsk &new_psk, const LogString *save_log_msg, const LogString *fail_log_msg, const psk_t &active_psk, bool make_active); #endif // USE_API_NOISE +#ifdef USE_API_HOMEASSISTANT_STATES + // Helper methods to reduce code duplication + void add_state_subscription_(const char *entity_id, const char *attribute, std::function f, + bool once); + void add_state_subscription_(std::string entity_id, optional attribute, + std::function f, bool once); +#endif // USE_API_HOMEASSISTANT_STATES // Pointers and pointer-like types first (4 bytes each) std::unique_ptr socket_ = nullptr; #ifdef USE_API_CLIENT_CONNECTED_TRIGGER From efe2a1a5064ae2f9c75205c781de95571e178029 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 19 Nov 2025 22:32:41 -0600 Subject: [PATCH 163/415] cleanup --- esphome/components/api/api_connection.cpp | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index f968da591e..f6eab5db9a 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -1537,13 +1537,8 @@ void APIConnection::on_home_assistant_state_response(const HomeAssistantStateRes for (auto &it : this->parent_->get_state_subs()) { // Compare entity_id and attribute with message fields bool entity_match = (strcmp(it.entity_id_, msg.entity_id.c_str()) == 0); - bool attribute_match = false; - - if (it.has_attribute_) { - attribute_match = (strcmp(it.attribute_, msg.attribute.c_str()) == 0); - } else { - attribute_match = msg.attribute.empty(); - } + bool attribute_match = + it.has_attribute_ ? (strcmp(it.attribute_, msg.attribute.c_str()) == 0) : msg.attribute.empty(); if (entity_match && attribute_match) { it.callback_(msg.state); From b9595c0795f7daddbefda261372aef2d207dd41f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 19 Nov 2025 22:49:52 -0600 Subject: [PATCH 164/415] cover --- tests/integration/fixtures/api_custom_services.yaml | 1 + .../custom_api_device_component.cpp | 9 +++++++++ .../custom_api_device_component.h | 3 +++ tests/integration/test_api_custom_services.py | 11 +++++++++++ 4 files changed, 24 insertions(+) diff --git a/tests/integration/fixtures/api_custom_services.yaml b/tests/integration/fixtures/api_custom_services.yaml index a597c74126..827bee93a6 100644 --- a/tests/integration/fixtures/api_custom_services.yaml +++ b/tests/integration/fixtures/api_custom_services.yaml @@ -5,6 +5,7 @@ host: # This is required for CustomAPIDevice to work api: custom_services: true + homeassistant_states: true # Also test that YAML services still work actions: - action: test_yaml_service diff --git a/tests/integration/fixtures/external_components/custom_api_device_component/custom_api_device_component.cpp b/tests/integration/fixtures/external_components/custom_api_device_component/custom_api_device_component.cpp index c8581b3d2f..01bc7dcd98 100644 --- a/tests/integration/fixtures/external_components/custom_api_device_component/custom_api_device_component.cpp +++ b/tests/integration/fixtures/external_components/custom_api_device_component/custom_api_device_component.cpp @@ -17,6 +17,10 @@ void CustomAPIDeviceComponent::setup() { // Test array types register_service(&CustomAPIDeviceComponent::on_service_with_arrays, "custom_service_with_arrays", {"bool_array", "int_array", "float_array", "string_array"}); + + // Test Home Assistant state subscription using std::string API (custom_api_device.h) + // This tests the backward compatibility of the std::string overloads + subscribe_homeassistant_state(&CustomAPIDeviceComponent::on_ha_state_changed, std::string("sensor.custom_test")); } void CustomAPIDeviceComponent::on_test_service() { ESP_LOGI(TAG, "Custom test service called!"); } @@ -48,6 +52,11 @@ void CustomAPIDeviceComponent::on_service_with_arrays(std::vector bool_arr } } +void CustomAPIDeviceComponent::on_ha_state_changed(std::string entity_id, std::string state) { + ESP_LOGI(TAG, "Home Assistant state changed for %s: %s", entity_id.c_str(), state.c_str()); + ESP_LOGI(TAG, "This subscription uses std::string API for backward compatibility"); +} + } // namespace custom_api_device_component } // namespace esphome #endif // USE_API diff --git a/tests/integration/fixtures/external_components/custom_api_device_component/custom_api_device_component.h b/tests/integration/fixtures/external_components/custom_api_device_component/custom_api_device_component.h index 92960746d9..0720b9e7de 100644 --- a/tests/integration/fixtures/external_components/custom_api_device_component/custom_api_device_component.h +++ b/tests/integration/fixtures/external_components/custom_api_device_component/custom_api_device_component.h @@ -22,6 +22,9 @@ class CustomAPIDeviceComponent : public Component, public CustomAPIDevice { void on_service_with_arrays(std::vector bool_array, std::vector int_array, std::vector float_array, std::vector string_array); + + // Test Home Assistant state subscription with std::string API + void on_ha_state_changed(std::string entity_id, std::string state); }; } // namespace custom_api_device_component diff --git a/tests/integration/test_api_custom_services.py b/tests/integration/test_api_custom_services.py index 967c504112..39e35197e8 100644 --- a/tests/integration/test_api_custom_services.py +++ b/tests/integration/test_api_custom_services.py @@ -38,6 +38,7 @@ async def test_api_custom_services( custom_service_future = loop.create_future() custom_args_future = loop.create_future() custom_arrays_future = loop.create_future() + ha_state_future = loop.create_future() # Patterns to match in logs yaml_service_pattern = re.compile(r"YAML service called") @@ -50,6 +51,9 @@ async def test_api_custom_services( custom_arrays_pattern = re.compile( r"Array service called with 2 bools, 3 ints, 2 floats, 2 strings" ) + ha_state_pattern = re.compile( + r"This subscription uses std::string API for backward compatibility" + ) def check_output(line: str) -> None: """Check log output for expected messages.""" @@ -65,6 +69,8 @@ async def test_api_custom_services( custom_args_future.set_result(True) elif not custom_arrays_future.done() and custom_arrays_pattern.search(line): custom_arrays_future.set_result(True) + elif not ha_state_future.done() and ha_state_pattern.search(line): + ha_state_future.set_result(True) # Run with log monitoring async with ( @@ -198,3 +204,8 @@ async def test_api_custom_services( }, ) await asyncio.wait_for(custom_arrays_future, timeout=5.0) + + # Test Home Assistant state subscription (std::string API backward compatibility) + # This verifies that custom_api_device.h can still use std::string overloads + client.send_home_assistant_state("sensor.custom_test", "", "42.5") + await asyncio.wait_for(ha_state_future, timeout=5.0) From 4533b8f92c4b363c23d282ca61415913abaa30af Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 19 Nov 2025 23:20:24 -0600 Subject: [PATCH 165/415] tweaks --- esphome/components/api/api_connection.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index f6eab5db9a..6d0a480ff4 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -1537,8 +1537,8 @@ void APIConnection::on_home_assistant_state_response(const HomeAssistantStateRes for (auto &it : this->parent_->get_state_subs()) { // Compare entity_id and attribute with message fields bool entity_match = (strcmp(it.entity_id_, msg.entity_id.c_str()) == 0); - bool attribute_match = - it.has_attribute_ ? (strcmp(it.attribute_, msg.attribute.c_str()) == 0) : msg.attribute.empty(); + bool attribute_match = (it.has_attribute_ && strcmp(it.attribute_, msg.attribute.c_str()) == 0) || + (!it.has_attribute_ && msg.attribute.empty()); if (entity_match && attribute_match) { it.callback_(msg.state); From 177026d8c47d95e05002a0719329fefbc87b279d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 19 Nov 2025 23:25:45 -0600 Subject: [PATCH 166/415] simplify --- esphome/components/api/api_connection.cpp | 6 +++--- esphome/components/api/api_server.cpp | 8 +------- esphome/components/api/api_server.h | 3 +-- 3 files changed, 5 insertions(+), 12 deletions(-) diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 6d0a480ff4..5dd8ec8719 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -1537,8 +1537,8 @@ void APIConnection::on_home_assistant_state_response(const HomeAssistantStateRes for (auto &it : this->parent_->get_state_subs()) { // Compare entity_id and attribute with message fields bool entity_match = (strcmp(it.entity_id_, msg.entity_id.c_str()) == 0); - bool attribute_match = (it.has_attribute_ && strcmp(it.attribute_, msg.attribute.c_str()) == 0) || - (!it.has_attribute_ && msg.attribute.empty()); + bool attribute_match = (it.attribute_ != nullptr && strcmp(it.attribute_, msg.attribute.c_str()) == 0) || + (it.attribute_ == nullptr && msg.attribute.empty()); if (entity_match && attribute_match) { it.callback_(msg.state); @@ -1881,7 +1881,7 @@ void APIConnection::process_state_subscriptions_() { resp.set_entity_id(StringRef(it.entity_id_)); // Avoid string copy by using the const char* pointer if it exists - resp.set_attribute(it.has_attribute_ ? StringRef(it.attribute_) : StringRef("")); + resp.set_attribute(it.attribute_ != nullptr ? StringRef(it.attribute_) : StringRef("")); resp.once = it.once_; if (this->send_message(resp, SubscribeHomeAssistantStateResponse::MESSAGE_TYPE)) { diff --git a/esphome/components/api/api_server.cpp b/esphome/components/api/api_server.cpp index 3b073038c6..54b8a58de6 100644 --- a/esphome/components/api/api_server.cpp +++ b/esphome/components/api/api_server.cpp @@ -435,11 +435,7 @@ void APIServer::handle_action_response(uint32_t call_id, bool success, const std void APIServer::add_state_subscription_(const char *entity_id, const char *attribute, std::function f, bool once) { this->state_subs_.push_back(HomeAssistantStateSubscription{ - .entity_id_ = entity_id, - .attribute_ = attribute, - .callback_ = std::move(f), - .once_ = once, - .has_attribute_ = (attribute != nullptr), + .entity_id_ = entity_id, .attribute_ = attribute, .callback_ = std::move(f), .once_ = once, // entity_id_copy_ and attribute_copy_ remain nullptr (no heap allocation) }); } @@ -455,10 +451,8 @@ void APIServer::add_state_subscription_(std::string entity_id, optional(std::move(attribute.value())); sub.attribute_ = sub.attribute_copy_->c_str(); - sub.has_attribute_ = true; } else { sub.attribute_ = nullptr; - sub.has_attribute_ = false; } sub.callback_ = std::move(f); diff --git a/esphome/components/api/api_server.h b/esphome/components/api/api_server.h index 697142cbe5..be69153a27 100644 --- a/esphome/components/api/api_server.h +++ b/esphome/components/api/api_server.h @@ -155,10 +155,9 @@ class APIServer : public Component, public Controller { #ifdef USE_API_HOMEASSISTANT_STATES struct HomeAssistantStateSubscription { const char *entity_id_; // Pointer to flash (internal) or heap (external) - const char *attribute_; // Pointer to flash or nullptr + const char *attribute_; // Pointer to flash or nullptr (nullptr means no attribute) std::function callback_; bool once_; - bool has_attribute_; // Storage for external components using std::string API (custom_api_device.h) // These are only allocated when using the std::string overload From b346666a5264294afea8ce002cb5cf955b99339b Mon Sep 17 00:00:00 2001 From: Javier Peletier Date: Thu, 20 Nov 2025 10:05:22 +0100 Subject: [PATCH 167/415] [st7701s] Add explanatory comment (#12014) --- esphome/components/mipi_rgb/models/st7701s.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/esphome/components/mipi_rgb/models/st7701s.py b/esphome/components/mipi_rgb/models/st7701s.py index bfd1c9aa3f..0b0a9548ca 100644 --- a/esphome/components/mipi_rgb/models/st7701s.py +++ b/esphome/components/mipi_rgb/models/st7701s.py @@ -24,6 +24,8 @@ class ST7701S(DriverChip): sdir = 0 if transform.get(CONF_MIRROR_X): sdir |= 0x04 + # XFLIP doesn't do anything in the ST7701S, + # it's set in the madctl byte just so it can be reported at runtime by logconfig madctl |= MADCTL_XFLIP sequence.append((SDIR_CMD, sdir)) return madctl From 946f8deb3d8a884c4562f33d68fb98bb7d11fbc8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 20 Nov 2025 07:22:27 -0600 Subject: [PATCH 168/415] tweak naming --- esphome/components/api/api_server.cpp | 10 +++++----- esphome/components/api/api_server.h | 8 ++++---- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/esphome/components/api/api_server.cpp b/esphome/components/api/api_server.cpp index 54b8a58de6..225ed5178c 100644 --- a/esphome/components/api/api_server.cpp +++ b/esphome/components/api/api_server.cpp @@ -436,7 +436,7 @@ void APIServer::add_state_subscription_(const char *entity_id, const char *attri std::function f, bool once) { this->state_subs_.push_back(HomeAssistantStateSubscription{ .entity_id_ = entity_id, .attribute_ = attribute, .callback_ = std::move(f), .once_ = once, - // entity_id_copy_ and attribute_copy_ remain nullptr (no heap allocation) + // entity_id_dynamic_storage_ and attribute_dynamic_storage_ remain nullptr (no heap allocation) }); } @@ -445,12 +445,12 @@ void APIServer::add_state_subscription_(std::string entity_id, optional f, bool once) { HomeAssistantStateSubscription sub; // Allocate heap storage for the strings - sub.entity_id_copy_ = std::make_unique(std::move(entity_id)); - sub.entity_id_ = sub.entity_id_copy_->c_str(); + sub.entity_id_dynamic_storage_ = std::make_unique(std::move(entity_id)); + sub.entity_id_ = sub.entity_id_dynamic_storage_->c_str(); if (attribute.has_value()) { - sub.attribute_copy_ = std::make_unique(std::move(attribute.value())); - sub.attribute_ = sub.attribute_copy_->c_str(); + sub.attribute_dynamic_storage_ = std::make_unique(std::move(attribute.value())); + sub.attribute_ = sub.attribute_dynamic_storage_->c_str(); } else { sub.attribute_ = nullptr; } diff --git a/esphome/components/api/api_server.h b/esphome/components/api/api_server.h index be69153a27..547a978d50 100644 --- a/esphome/components/api/api_server.h +++ b/esphome/components/api/api_server.h @@ -159,10 +159,10 @@ class APIServer : public Component, public Controller { std::function callback_; bool once_; - // Storage for external components using std::string API (custom_api_device.h) - // These are only allocated when using the std::string overload - std::unique_ptr entity_id_copy_; - std::unique_ptr attribute_copy_; + // Dynamic storage for external components using std::string API (custom_api_device.h) + // These are only allocated when using the std::string overload (nullptr for const char* overload) + std::unique_ptr entity_id_dynamic_storage_; + std::unique_ptr attribute_dynamic_storage_; }; // New const char* overload (for internal components - zero allocation) From 4825da8e9ca27c1b7d9c88bd094da5a620b53a23 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 20 Nov 2025 07:57:04 -0600 Subject: [PATCH 169/415] [select] Modernize namespace declarations to C++17 syntax (#12007) --- esphome/components/select/automation.h | 6 ++---- esphome/components/select/select.cpp | 6 ++---- esphome/components/select/select.h | 6 ++---- esphome/components/select/select_call.cpp | 6 ++---- esphome/components/select/select_call.h | 6 ++---- esphome/components/select/select_traits.cpp | 6 ++---- esphome/components/select/select_traits.h | 6 ++---- 7 files changed, 14 insertions(+), 28 deletions(-) diff --git a/esphome/components/select/automation.h b/esphome/components/select/automation.h index 3e42eaf98a..768f2621f7 100644 --- a/esphome/components/select/automation.h +++ b/esphome/components/select/automation.h @@ -4,8 +4,7 @@ #include "esphome/core/component.h" #include "select.h" -namespace esphome { -namespace select { +namespace esphome::select { class SelectStateTrigger : public Trigger { public: @@ -63,5 +62,4 @@ template class SelectOperationAction : public Action { Select *select_; }; -} // namespace select -} // namespace esphome +} // namespace esphome::select diff --git a/esphome/components/select/select.cpp b/esphome/components/select/select.cpp index 9fe7a52422..3ec413f167 100644 --- a/esphome/components/select/select.cpp +++ b/esphome/components/select/select.cpp @@ -4,8 +4,7 @@ #include "esphome/core/log.h" #include -namespace esphome { -namespace select { +namespace esphome::select { static const char *const TAG = "select"; @@ -86,5 +85,4 @@ optional Select::at(size_t index) const { const char *Select::option_at(size_t index) const { return traits.get_options().at(index); } -} // namespace select -} // namespace esphome +} // namespace esphome::select diff --git a/esphome/components/select/select.h b/esphome/components/select/select.h index 7459c9d146..c4d7412d50 100644 --- a/esphome/components/select/select.h +++ b/esphome/components/select/select.h @@ -6,8 +6,7 @@ #include "select_call.h" #include "select_traits.h" -namespace esphome { -namespace select { +namespace esphome::select { #define LOG_SELECT(prefix, type, obj) \ if ((obj) != nullptr) { \ @@ -114,5 +113,4 @@ class Select : public EntityBase { CallbackManager state_callback_; }; -} // namespace select -} // namespace esphome +} // namespace esphome::select diff --git a/esphome/components/select/select_call.cpp b/esphome/components/select/select_call.cpp index aa7559e24e..aecfed0d64 100644 --- a/esphome/components/select/select_call.cpp +++ b/esphome/components/select/select_call.cpp @@ -2,8 +2,7 @@ #include "select.h" #include "esphome/core/log.h" -namespace esphome { -namespace select { +namespace esphome::select { static const char *const TAG = "select"; @@ -125,5 +124,4 @@ void SelectCall::perform() { parent->control(idx); } -} // namespace select -} // namespace esphome +} // namespace esphome::select diff --git a/esphome/components/select/select_call.h b/esphome/components/select/select_call.h index eae7d3de1d..b31d890ef6 100644 --- a/esphome/components/select/select_call.h +++ b/esphome/components/select/select_call.h @@ -2,8 +2,7 @@ #include "esphome/core/helpers.h" -namespace esphome { -namespace select { +namespace esphome::select { class Select; @@ -45,5 +44,4 @@ class SelectCall { bool cycle_; }; -} // namespace select -} // namespace esphome +} // namespace esphome::select diff --git a/esphome/components/select/select_traits.cpp b/esphome/components/select/select_traits.cpp index e5e12bdc7a..ff52c0d85b 100644 --- a/esphome/components/select/select_traits.cpp +++ b/esphome/components/select/select_traits.cpp @@ -1,7 +1,6 @@ #include "select_traits.h" -namespace esphome { -namespace select { +namespace esphome::select { void SelectTraits::set_options(const std::initializer_list &options) { this->options_ = options; } @@ -14,5 +13,4 @@ void SelectTraits::set_options(const FixedVector &options) { const FixedVector &SelectTraits::get_options() const { return this->options_; } -} // namespace select -} // namespace esphome +} // namespace esphome::select diff --git a/esphome/components/select/select_traits.h b/esphome/components/select/select_traits.h index ee59a030ad..78a83e5944 100644 --- a/esphome/components/select/select_traits.h +++ b/esphome/components/select/select_traits.h @@ -3,8 +3,7 @@ #include "esphome/core/helpers.h" #include -namespace esphome { -namespace select { +namespace esphome::select { class SelectTraits { public: @@ -16,5 +15,4 @@ class SelectTraits { FixedVector options_; }; -} // namespace select -} // namespace esphome +} // namespace esphome::select From 507147376710e8d5e214d78ab51ece81f7a33339 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 20 Nov 2025 07:57:33 -0600 Subject: [PATCH 170/415] [mdns] Modernize to C++17 nested namespace syntax (#11983) --- esphome/components/mdns/mdns_component.cpp | 6 ++---- esphome/components/mdns/mdns_component.h | 6 ++---- esphome/components/mdns/mdns_esp32.cpp | 6 ++---- esphome/components/mdns/mdns_esp8266.cpp | 6 ++---- esphome/components/mdns/mdns_host.cpp | 6 ++---- esphome/components/mdns/mdns_libretiny.cpp | 6 ++---- esphome/components/mdns/mdns_rp2040.cpp | 6 ++---- 7 files changed, 14 insertions(+), 28 deletions(-) diff --git a/esphome/components/mdns/mdns_component.cpp b/esphome/components/mdns/mdns_component.cpp index b66129404e..c81defd19f 100644 --- a/esphome/components/mdns/mdns_component.cpp +++ b/esphome/components/mdns/mdns_component.cpp @@ -21,8 +21,7 @@ #include "esphome/components/dashboard_import/dashboard_import.h" #endif -namespace esphome { -namespace mdns { +namespace esphome::mdns { static const char *const TAG = "mdns"; @@ -189,6 +188,5 @@ void MDNSComponent::dump_config() { #endif } -} // namespace mdns -} // namespace esphome +} // namespace esphome::mdns #endif diff --git a/esphome/components/mdns/mdns_component.h b/esphome/components/mdns/mdns_component.h index f4237d5a69..691c45b7df 100644 --- a/esphome/components/mdns/mdns_component.h +++ b/esphome/components/mdns/mdns_component.h @@ -6,8 +6,7 @@ #include "esphome/core/component.h" #include "esphome/core/helpers.h" -namespace esphome { -namespace mdns { +namespace esphome::mdns { // Helper struct that identifies strings that may be stored in flash storage (similar to LogString) struct MDNSString; @@ -79,6 +78,5 @@ class MDNSComponent : public Component { void compile_records_(StaticVector &services); }; -} // namespace mdns -} // namespace esphome +} // namespace esphome::mdns #endif diff --git a/esphome/components/mdns/mdns_esp32.cpp b/esphome/components/mdns/mdns_esp32.cpp index ecdc926cc9..5547a2524b 100644 --- a/esphome/components/mdns/mdns_esp32.cpp +++ b/esphome/components/mdns/mdns_esp32.cpp @@ -7,8 +7,7 @@ #include "esphome/core/log.h" #include "mdns_component.h" -namespace esphome { -namespace mdns { +namespace esphome::mdns { static const char *const TAG = "mdns"; @@ -56,7 +55,6 @@ void MDNSComponent::on_shutdown() { delay(40); // Allow the mdns packets announcing service removal to be sent } -} // namespace mdns -} // namespace esphome +} // namespace esphome::mdns #endif // USE_ESP32 diff --git a/esphome/components/mdns/mdns_esp8266.cpp b/esphome/components/mdns/mdns_esp8266.cpp index 9bbb406070..06f905884c 100644 --- a/esphome/components/mdns/mdns_esp8266.cpp +++ b/esphome/components/mdns/mdns_esp8266.cpp @@ -9,8 +9,7 @@ #include "esphome/core/log.h" #include "mdns_component.h" -namespace esphome { -namespace mdns { +namespace esphome::mdns { void MDNSComponent::setup() { #ifdef USE_MDNS_STORE_SERVICES @@ -52,7 +51,6 @@ void MDNSComponent::on_shutdown() { delay(10); } -} // namespace mdns -} // namespace esphome +} // namespace esphome::mdns #endif diff --git a/esphome/components/mdns/mdns_host.cpp b/esphome/components/mdns/mdns_host.cpp index f645d8d068..64b8c8f54b 100644 --- a/esphome/components/mdns/mdns_host.cpp +++ b/esphome/components/mdns/mdns_host.cpp @@ -6,8 +6,7 @@ #include "esphome/core/log.h" #include "mdns_component.h" -namespace esphome { -namespace mdns { +namespace esphome::mdns { void MDNSComponent::setup() { // Host platform doesn't have actual mDNS implementation @@ -15,7 +14,6 @@ void MDNSComponent::setup() { void MDNSComponent::on_shutdown() {} -} // namespace mdns -} // namespace esphome +} // namespace esphome::mdns #endif diff --git a/esphome/components/mdns/mdns_libretiny.cpp b/esphome/components/mdns/mdns_libretiny.cpp index fb2088f719..a049fe2109 100644 --- a/esphome/components/mdns/mdns_libretiny.cpp +++ b/esphome/components/mdns/mdns_libretiny.cpp @@ -9,8 +9,7 @@ #include -namespace esphome { -namespace mdns { +namespace esphome::mdns { void MDNSComponent::setup() { #ifdef USE_MDNS_STORE_SERVICES @@ -46,7 +45,6 @@ void MDNSComponent::setup() { void MDNSComponent::on_shutdown() {} -} // namespace mdns -} // namespace esphome +} // namespace esphome::mdns #endif diff --git a/esphome/components/mdns/mdns_rp2040.cpp b/esphome/components/mdns/mdns_rp2040.cpp index a9f5349f14..a102e0b6c3 100644 --- a/esphome/components/mdns/mdns_rp2040.cpp +++ b/esphome/components/mdns/mdns_rp2040.cpp @@ -9,8 +9,7 @@ #include -namespace esphome { -namespace mdns { +namespace esphome::mdns { void MDNSComponent::setup() { #ifdef USE_MDNS_STORE_SERVICES @@ -51,7 +50,6 @@ void MDNSComponent::on_shutdown() { delay(40); } -} // namespace mdns -} // namespace esphome +} // namespace esphome::mdns #endif From 24a6ad148c6a6fe130e1ca74859032a2b787df3f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 20 Nov 2025 07:57:49 -0600 Subject: [PATCH 171/415] [lock] Modernize to C++17 nested namespaces (#11982) --- esphome/components/lock/automation.h | 6 ++---- esphome/components/lock/lock.cpp | 6 ++---- esphome/components/lock/lock.h | 6 ++---- 3 files changed, 6 insertions(+), 12 deletions(-) diff --git a/esphome/components/lock/automation.h b/esphome/components/lock/automation.h index 0f596ef5e6..cba2c3fdda 100644 --- a/esphome/components/lock/automation.h +++ b/esphome/components/lock/automation.h @@ -4,8 +4,7 @@ #include "esphome/core/automation.h" #include "esphome/core/component.h" -namespace esphome { -namespace lock { +namespace esphome::lock { template class LockAction : public Action { public: @@ -72,5 +71,4 @@ class LockUnlockTrigger : public Trigger<> { } }; -} // namespace lock -} // namespace esphome +} // namespace esphome::lock diff --git a/esphome/components/lock/lock.cpp b/esphome/components/lock/lock.cpp index 54fefe8745..b8f0fbe011 100644 --- a/esphome/components/lock/lock.cpp +++ b/esphome/components/lock/lock.cpp @@ -3,8 +3,7 @@ #include "esphome/core/controller_registry.h" #include "esphome/core/log.h" -namespace esphome { -namespace lock { +namespace esphome::lock { static const char *const TAG = "lock"; @@ -108,5 +107,4 @@ LockCall &LockCall::set_state(const std::string &state) { } const optional &LockCall::get_state() const { return this->state_; } -} // namespace lock -} // namespace esphome +} // namespace esphome::lock diff --git a/esphome/components/lock/lock.h b/esphome/components/lock/lock.h index 9737569921..8a906ef9fc 100644 --- a/esphome/components/lock/lock.h +++ b/esphome/components/lock/lock.h @@ -7,8 +7,7 @@ #include "esphome/core/preferences.h" #include -namespace esphome { -namespace lock { +namespace esphome::lock { class Lock; @@ -177,5 +176,4 @@ class Lock : public EntityBase { ESPPreferenceObject rtc_; }; -} // namespace lock -} // namespace esphome +} // namespace esphome::lock From a2321edf3c7dcb7b38adbc77816bcf23dcd44595 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 20 Nov 2025 07:59:16 -0600 Subject: [PATCH 172/415] [network] Fix IPAddress constructor causing comparison failures and garbage output (#12005) --- esphome/components/network/ip_address.h | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/esphome/components/network/ip_address.h b/esphome/components/network/ip_address.h index 5ec6450cce..3d8b062d0b 100644 --- a/esphome/components/network/ip_address.h +++ b/esphome/components/network/ip_address.h @@ -81,7 +81,12 @@ struct IPAddress { ip_addr_.type = IPADDR_TYPE_V6; } #endif /* LWIP_IPV6 */ - IPAddress(esp_ip4_addr_t *other_ip) { memcpy((void *) &ip_addr_, (void *) other_ip, sizeof(esp_ip4_addr_t)); } + IPAddress(esp_ip4_addr_t *other_ip) { + memcpy((void *) &ip_addr_, (void *) other_ip, sizeof(esp_ip4_addr_t)); +#if LWIP_IPV6 + ip_addr_.type = IPADDR_TYPE_V4; +#endif + } IPAddress(esp_ip_addr_t *other_ip) { #if LWIP_IPV6 memcpy((void *) &ip_addr_, (void *) other_ip, sizeof(ip_addr_)); From b62053812b79f9fb9acc6965e7060c05951fc681 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 20 Nov 2025 08:06:28 -0600 Subject: [PATCH 173/415] [core] Document threading model rationale in ThreadModel enum (#11979) --- esphome/components/libretiny/__init__.py | 4 ++++ esphome/const.py | 25 +++++++++++++++++++++++- 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/esphome/components/libretiny/__init__.py b/esphome/components/libretiny/__init__.py index c63d6d7faa..93b66888da 100644 --- a/esphome/components/libretiny/__init__.py +++ b/esphome/components/libretiny/__init__.py @@ -261,6 +261,10 @@ async def component_to_code(config): cg.add_build_flag(f"-DUSE_LIBRETINY_VARIANT_{config[CONF_FAMILY]}") cg.add_define("ESPHOME_BOARD", config[CONF_BOARD]) cg.add_define("ESPHOME_VARIANT", FAMILY_FRIENDLY[config[CONF_FAMILY]]) + # LibreTiny uses MULTI_NO_ATOMICS because platforms like BK7231N (ARM968E-S) lack + # exclusive load/store (no LDREX/STREX). std::atomic RMW operations require libatomic, + # which is not linked to save flash (4-8KB). Even if linked, libatomic would use locks + # (ATOMIC_INT_LOCK_FREE=1), so explicit FreeRTOS mutexes are simpler and equivalent. cg.add_define(ThreadModel.MULTI_NO_ATOMICS) # force using arduino framework diff --git a/esphome/const.py b/esphome/const.py index b4cd3cfd1c..2b6b60d395 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -36,7 +36,30 @@ class Framework(StrEnum): class ThreadModel(StrEnum): - """Threading model identifiers for ESPHome scheduler.""" + """Threading model identifiers for ESPHome scheduler. + + ESPHome currently uses three threading models based on platform capabilities: + + SINGLE: + - Single-threaded platforms (ESP8266, RP2040) + - No RTOS task switching + - No concurrent access to scheduler data structures + - No atomics or locks required + - Minimal overhead + + MULTI_NO_ATOMICS: + - Multi-threaded platforms without hardware atomic RMW support (e.g. LibreTiny BK7231N) + - Uses FreeRTOS or another RTOS with multiple tasks + - CPU lacks exclusive load/store instructions (ARM968E-S has no LDREX/STREX) + - std::atomic cannot provide lock-free RMW; libatomic is avoided to save flash (4–8 KB) + - Scheduler uses explicit FreeRTOS mutexes for synchronization + + MULTI_ATOMICS: + - Multi-threaded platforms with hardware atomic RMW support (ESP32, Cortex-M, Host) + - CPU provides native atomic instructions (ESP32 S32C1I, ARM LDREX/STREX) + - std::atomic is used for lock-free synchronization + - Reduced contention and better performance + """ SINGLE = "ESPHOME_THREAD_SINGLE" MULTI_NO_ATOMICS = "ESPHOME_THREAD_MULTI_NO_ATOMICS" From 5d883c6e06d649651db138807ffd6cfd880c44ac Mon Sep 17 00:00:00 2001 From: tomaszduda23 Date: Thu, 20 Nov 2025 15:06:40 +0100 Subject: [PATCH 174/415] [nrf52,i2c] fix review comment (#11931) --- esphome/components/i2c/i2c_bus_zephyr.cpp | 31 ++++++++++++----------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/esphome/components/i2c/i2c_bus_zephyr.cpp b/esphome/components/i2c/i2c_bus_zephyr.cpp index 658dcee35c..1eb9944dcb 100644 --- a/esphome/components/i2c/i2c_bus_zephyr.cpp +++ b/esphome/components/i2c/i2c_bus_zephyr.cpp @@ -8,6 +8,22 @@ namespace esphome::i2c { static const char *const TAG = "i2c.zephyr"; +static const char *get_speed(uint32_t dev_config) { + switch (I2C_SPEED_GET(dev_config)) { + case I2C_SPEED_STANDARD: + return "100 kHz"; + case I2C_SPEED_FAST: + return "400 kHz"; + case I2C_SPEED_FAST_PLUS: + return "1 MHz"; + case I2C_SPEED_HIGH: + return "3.4 MHz"; + case I2C_SPEED_ULTRA: + return "5 MHz"; + } + return "unknown"; +} + void ZephyrI2CBus::setup() { if (!device_is_ready(this->i2c_dev_)) { ESP_LOGE(TAG, "I2C dev is not ready."); @@ -31,21 +47,6 @@ void ZephyrI2CBus::setup() { } void ZephyrI2CBus::dump_config() { - auto get_speed = [](uint32_t dev_config) { - switch (I2C_SPEED_GET(dev_config)) { - case I2C_SPEED_STANDARD: - return "100 kHz"; - case I2C_SPEED_FAST: - return "400 kHz"; - case I2C_SPEED_FAST_PLUS: - return "1 MHz"; - case I2C_SPEED_HIGH: - return "3.4 MHz"; - case I2C_SPEED_ULTRA: - return "5 MHz"; - } - return "unknown"; - }; ESP_LOGCONFIG(TAG, "I2C Bus:\n" " SDA Pin: GPIO%u\n" From 06bef148f432c5521c8d91743a0134f672ae4be5 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 20 Nov 2025 08:06:52 -0600 Subject: [PATCH 175/415] [core] Optimize DelayAction for no-argument case using if constexpr (#11913) --- esphome/core/base_automation.h | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/esphome/core/base_automation.h b/esphome/core/base_automation.h index a5e6139182..c2519da839 100644 --- a/esphome/core/base_automation.h +++ b/esphome/core/base_automation.h @@ -178,7 +178,6 @@ template class DelayAction : public Action, public Compon TEMPLATABLE_VALUE(uint32_t, delay) void play_complex(const Ts &...x) override { - auto f = std::bind(&DelayAction::play_next_, this, x...); this->num_running_++; // If num_running_ > 1, we have multiple instances running in parallel @@ -187,9 +186,22 @@ template class DelayAction : public Action, public Compon // WARNING: This can accumulate delays if scripts are triggered faster than they complete! // Users should set max_runs on parallel scripts to limit concurrent executions. // Issue #10264: This is a workaround for parallel script delays interfering with each other. - App.scheduler.set_timer_common_(this, Scheduler::SchedulerItem::TIMEOUT, - /* is_static_string= */ true, "delay", this->delay_.value(x...), std::move(f), - /* is_retry= */ false, /* skip_cancel= */ this->num_running_ > 1); + + // Optimization: For no-argument delays (most common case), use direct lambda + // instead of std::bind to avoid bind overhead (~16 bytes heap + faster execution) + if constexpr (sizeof...(Ts) == 0) { + App.scheduler.set_timer_common_( + this, Scheduler::SchedulerItem::TIMEOUT, + /* is_static_string= */ true, "delay", this->delay_.value(), [this]() { this->play_next_(); }, + /* is_retry= */ false, /* skip_cancel= */ this->num_running_ > 1); + } else { + // For delays with arguments, use std::bind to preserve argument values + // Arguments must be copied because original references may be invalid after delay + auto f = std::bind(&DelayAction::play_next_, this, x...); + App.scheduler.set_timer_common_(this, Scheduler::SchedulerItem::TIMEOUT, + /* is_static_string= */ true, "delay", this->delay_.value(x...), std::move(f), + /* is_retry= */ false, /* skip_cancel= */ this->num_running_ > 1); + } } float get_setup_priority() const override { return setup_priority::HARDWARE; } From 3c86f3894b9f331f9df9d49b50725e9e7337df28 Mon Sep 17 00:00:00 2001 From: omartijn <44672243+omartijn@users.noreply.github.com> Date: Thu, 20 Nov 2025 15:24:45 +0100 Subject: [PATCH 176/415] [hc8] Add support for HC8 CO2 sensor (#11872) Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> --- CODEOWNERS | 1 + esphome/components/hc8/__init__.py | 1 + esphome/components/hc8/hc8.cpp | 99 ++++++++++++++++++++++ esphome/components/hc8/hc8.h | 37 ++++++++ esphome/components/hc8/sensor.py | 79 +++++++++++++++++ tests/components/hc8/common.yaml | 13 +++ tests/components/hc8/test.esp32-idf.yaml | 4 + tests/components/hc8/test.esp8266-ard.yaml | 4 + tests/components/hc8/test.rp2040-ard.yaml | 4 + 9 files changed, 242 insertions(+) create mode 100644 esphome/components/hc8/__init__.py create mode 100644 esphome/components/hc8/hc8.cpp create mode 100644 esphome/components/hc8/hc8.h create mode 100644 esphome/components/hc8/sensor.py create mode 100644 tests/components/hc8/common.yaml create mode 100644 tests/components/hc8/test.esp32-idf.yaml create mode 100644 tests/components/hc8/test.esp8266-ard.yaml create mode 100644 tests/components/hc8/test.rp2040-ard.yaml diff --git a/CODEOWNERS b/CODEOWNERS index 250fbbd4d4..c3d8f4350f 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -202,6 +202,7 @@ esphome/components/havells_solar/* @sourabhjaiswal esphome/components/hbridge/fan/* @WeekendWarrior esphome/components/hbridge/light/* @DotNetDann esphome/components/hbridge/switch/* @dwmw2 +esphome/components/hc8/* @omartijn esphome/components/hdc2010/* @optimusprimespace @ssieb esphome/components/he60r/* @clydebarrow esphome/components/heatpumpir/* @rob-deutsch diff --git a/esphome/components/hc8/__init__.py b/esphome/components/hc8/__init__.py new file mode 100644 index 0000000000..e1028456b0 --- /dev/null +++ b/esphome/components/hc8/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ["@omartijn"] diff --git a/esphome/components/hc8/hc8.cpp b/esphome/components/hc8/hc8.cpp new file mode 100644 index 0000000000..5b649c2735 --- /dev/null +++ b/esphome/components/hc8/hc8.cpp @@ -0,0 +1,99 @@ +#include "hc8.h" +#include "esphome/core/application.h" +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" + +#include + +namespace esphome::hc8 { + +static const char *const TAG = "hc8"; +static const std::array HC8_COMMAND_GET_PPM{0x64, 0x69, 0x03, 0x5E, 0x4E}; +static const std::array HC8_COMMAND_CALIBRATE_PREAMBLE{0x11, 0x03, 0x03}; + +void HC8Component::setup() { + // send an initial query to the device, this will + // get it out of "active output mode", where it + // generates data every second + this->write_array(HC8_COMMAND_GET_PPM); + this->flush(); + + // ensure the buffer is empty + while (this->available()) + this->read(); +} + +void HC8Component::update() { + uint32_t now_ms = App.get_loop_component_start_time(); + uint32_t warmup_ms = this->warmup_seconds_ * 1000; + if (now_ms < warmup_ms) { + ESP_LOGW(TAG, "HC8 warming up, %" PRIu32 " s left", (warmup_ms - now_ms) / 1000); + this->status_set_warning(); + return; + } + + while (this->available()) + this->read(); + + this->write_array(HC8_COMMAND_GET_PPM); + this->flush(); + + // the sensor is a bit slow in responding, so trying to + // read immediately after sending a query will timeout + this->set_timeout(50, [this]() { + std::array response; + if (!this->read_array(response.data(), response.size())) { + ESP_LOGW(TAG, "Reading data from HC8 failed!"); + this->status_set_warning(); + return; + } + + if (response[0] != 0x64 || response[1] != 0x69) { + ESP_LOGW(TAG, "Invalid preamble from HC8!"); + this->status_set_warning(); + return; + } + + if (crc16(response.data(), 12) != encode_uint16(response[13], response[12])) { + ESP_LOGW(TAG, "HC8 Checksum mismatch"); + this->status_set_warning(); + return; + } + + this->status_clear_warning(); + + const uint16_t ppm = encode_uint16(response[5], response[4]); + ESP_LOGD(TAG, "HC8 Received CO₂=%uppm", ppm); + if (this->co2_sensor_ != nullptr) + this->co2_sensor_->publish_state(ppm); + }); +} + +void HC8Component::calibrate(uint16_t baseline) { + ESP_LOGD(TAG, "HC8 Calibrating baseline to %uppm", baseline); + + std::array command{}; + std::copy(begin(HC8_COMMAND_CALIBRATE_PREAMBLE), end(HC8_COMMAND_CALIBRATE_PREAMBLE), begin(command)); + command[3] = baseline >> 8; + command[4] = baseline; + command[5] = 0; + + // the last byte is a checksum over the data + for (uint8_t i = 0; i < 5; ++i) + command[5] -= command[i]; + + this->write_array(command); + this->flush(); +} + +float HC8Component::get_setup_priority() const { return setup_priority::DATA; } + +void HC8Component::dump_config() { + ESP_LOGCONFIG(TAG, "HC8:"); + LOG_SENSOR(" ", "CO2", this->co2_sensor_); + this->check_uart_settings(9600); + + ESP_LOGCONFIG(TAG, " Warmup time: %" PRIu32 " s", this->warmup_seconds_); +} + +} // namespace esphome::hc8 diff --git a/esphome/components/hc8/hc8.h b/esphome/components/hc8/hc8.h new file mode 100644 index 0000000000..7711fb8c97 --- /dev/null +++ b/esphome/components/hc8/hc8.h @@ -0,0 +1,37 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/automation.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/uart/uart.h" + +#include + +namespace esphome::hc8 { + +class HC8Component : public PollingComponent, public uart::UARTDevice { + public: + float get_setup_priority() const override; + + void setup() override; + void update() override; + void dump_config() override; + + void calibrate(uint16_t baseline); + + void set_co2_sensor(sensor::Sensor *co2_sensor) { co2_sensor_ = co2_sensor; } + void set_warmup_seconds(uint32_t seconds) { warmup_seconds_ = seconds; } + + protected: + sensor::Sensor *co2_sensor_{nullptr}; + uint32_t warmup_seconds_{0}; +}; + +template class HC8CalibrateAction : public Action, public Parented { + public: + TEMPLATABLE_VALUE(uint16_t, baseline) + + void play(const Ts &...x) override { this->parent_->calibrate(this->baseline_.value(x...)); } +}; + +} // namespace esphome::hc8 diff --git a/esphome/components/hc8/sensor.py b/esphome/components/hc8/sensor.py new file mode 100644 index 0000000000..90698b2661 --- /dev/null +++ b/esphome/components/hc8/sensor.py @@ -0,0 +1,79 @@ +from esphome import automation +import esphome.codegen as cg +from esphome.components import sensor, uart +import esphome.config_validation as cv +from esphome.const import ( + CONF_BASELINE, + CONF_CO2, + CONF_ID, + DEVICE_CLASS_CARBON_DIOXIDE, + ICON_MOLECULE_CO2, + STATE_CLASS_MEASUREMENT, + UNIT_PARTS_PER_MILLION, +) + +DEPENDENCIES = ["uart"] + +CONF_WARMUP_TIME = "warmup_time" + +hc8_ns = cg.esphome_ns.namespace("hc8") +HC8Component = hc8_ns.class_("HC8Component", cg.PollingComponent, uart.UARTDevice) +HC8CalibrateAction = hc8_ns.class_("HC8CalibrateAction", automation.Action) + +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(HC8Component), + cv.Optional(CONF_CO2): sensor.sensor_schema( + unit_of_measurement=UNIT_PARTS_PER_MILLION, + icon=ICON_MOLECULE_CO2, + accuracy_decimals=0, + device_class=DEVICE_CLASS_CARBON_DIOXIDE, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional( + CONF_WARMUP_TIME, default="75s" + ): cv.positive_time_period_seconds, + } + ) + .extend(cv.polling_component_schema("60s")) + .extend(uart.UART_DEVICE_SCHEMA) +) + +FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema( + "hc8", + baud_rate=9600, + require_rx=True, + require_tx=True, +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await uart.register_uart_device(var, config) + + if co2 := config.get(CONF_CO2): + sens = await sensor.new_sensor(co2) + cg.add(var.set_co2_sensor(sens)) + + cg.add(var.set_warmup_seconds(config[CONF_WARMUP_TIME])) + + +CALIBRATION_ACTION_SCHEMA = cv.Schema( + { + cv.Required(CONF_ID): cv.use_id(HC8Component), + cv.Required(CONF_BASELINE): cv.templatable(cv.uint16_t), + } +) + + +@automation.register_action( + "hc8.calibrate", HC8CalibrateAction, CALIBRATION_ACTION_SCHEMA +) +async def hc8_calibration_to_code(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + await cg.register_parented(var, config[CONF_ID]) + template_ = await cg.templatable(config[CONF_BASELINE], args, cg.uint16) + cg.add(var.set_baseline(template_)) + return var diff --git a/tests/components/hc8/common.yaml b/tests/components/hc8/common.yaml new file mode 100644 index 0000000000..ac3b454315 --- /dev/null +++ b/tests/components/hc8/common.yaml @@ -0,0 +1,13 @@ +esphome: + on_boot: + then: + - hc8.calibrate: + id: hc8_sensor + baseline: 420 + +sensor: + - platform: hc8 + id: hc8_sensor + co2: + name: HC8 CO2 Value + update_interval: 15s diff --git a/tests/components/hc8/test.esp32-idf.yaml b/tests/components/hc8/test.esp32-idf.yaml new file mode 100644 index 0000000000..2d29656c94 --- /dev/null +++ b/tests/components/hc8/test.esp32-idf.yaml @@ -0,0 +1,4 @@ +packages: + uart: !include ../../test_build_components/common/uart/esp32-idf.yaml + +<<: !include common.yaml diff --git a/tests/components/hc8/test.esp8266-ard.yaml b/tests/components/hc8/test.esp8266-ard.yaml new file mode 100644 index 0000000000..5a05efa259 --- /dev/null +++ b/tests/components/hc8/test.esp8266-ard.yaml @@ -0,0 +1,4 @@ +packages: + uart: !include ../../test_build_components/common/uart/esp8266-ard.yaml + +<<: !include common.yaml diff --git a/tests/components/hc8/test.rp2040-ard.yaml b/tests/components/hc8/test.rp2040-ard.yaml new file mode 100644 index 0000000000..f1df2daf83 --- /dev/null +++ b/tests/components/hc8/test.rp2040-ard.yaml @@ -0,0 +1,4 @@ +packages: + uart: !include ../../test_build_components/common/uart/rp2040-ard.yaml + +<<: !include common.yaml From 59cd6dbf70235fa89c41eeedffa610a9c823ac76 Mon Sep 17 00:00:00 2001 From: damib Date: Thu, 20 Nov 2025 15:28:14 +0100 Subject: [PATCH 177/415] [climate_ir] Add optional humidity sensor (#9805) Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> Co-authored-by: Djordje Mandic <6750655+DjordjeMandic@users.noreply.github.com> Co-authored-by: J. Nick Koston Co-authored-by: J. Nick Koston Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> --- esphome/components/climate_ir/__init__.py | 11 ++++++++++- esphome/components/climate_ir/climate_ir.cpp | 15 ++++++++++++--- esphome/components/climate_ir/climate_ir.h | 2 ++ tests/components/climate_ir_lg/common.yaml | 12 ++++++++++++ 4 files changed, 36 insertions(+), 4 deletions(-) diff --git a/esphome/components/climate_ir/__init__.py b/esphome/components/climate_ir/__init__.py index 6d66abf4cd..5315be3db6 100644 --- a/esphome/components/climate_ir/__init__.py +++ b/esphome/components/climate_ir/__init__.py @@ -3,7 +3,12 @@ import logging import esphome.codegen as cg from esphome.components import climate, remote_base, sensor import esphome.config_validation as cv -from esphome.const import CONF_SENSOR, CONF_SUPPORTS_COOL, CONF_SUPPORTS_HEAT +from esphome.const import ( + CONF_HUMIDITY_SENSOR, + CONF_SENSOR, + CONF_SUPPORTS_COOL, + CONF_SUPPORTS_HEAT, +) from esphome.cpp_generator import MockObjClass _LOGGER = logging.getLogger(__name__) @@ -32,6 +37,7 @@ def climate_ir_schema( cv.Optional(CONF_SUPPORTS_COOL, default=True): cv.boolean, cv.Optional(CONF_SUPPORTS_HEAT, default=True): cv.boolean, cv.Optional(CONF_SENSOR): cv.use_id(sensor.Sensor), + cv.Optional(CONF_HUMIDITY_SENSOR): cv.use_id(sensor.Sensor), } ) .extend(cv.COMPONENT_SCHEMA) @@ -61,6 +67,9 @@ async def register_climate_ir(var, config): if sensor_id := config.get(CONF_SENSOR): sens = await cg.get_variable(sensor_id) cg.add(var.set_sensor(sens)) + if sensor_id := config.get(CONF_HUMIDITY_SENSOR): + sens = await cg.get_variable(sensor_id) + cg.add(var.set_humidity_sensor(sens)) async def new_climate_ir(config, *args): diff --git a/esphome/components/climate_ir/climate_ir.cpp b/esphome/components/climate_ir/climate_ir.cpp index 2b95792a6c..50c8d459b0 100644 --- a/esphome/components/climate_ir/climate_ir.cpp +++ b/esphome/components/climate_ir/climate_ir.cpp @@ -11,7 +11,9 @@ climate::ClimateTraits ClimateIR::traits() { if (this->sensor_ != nullptr) { traits.add_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE); } - + if (this->humidity_sensor_ != nullptr) { + traits.add_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_HUMIDITY); + } traits.set_supported_modes({climate::CLIMATE_MODE_OFF, climate::CLIMATE_MODE_HEAT_COOL}); if (this->supports_cool_) traits.add_supported_mode(climate::CLIMATE_MODE_COOL); @@ -39,9 +41,16 @@ void ClimateIR::setup() { this->publish_state(); }); this->current_temperature = this->sensor_->state; - } else { - this->current_temperature = NAN; } + if (this->humidity_sensor_ != nullptr) { + this->humidity_sensor_->add_on_state_callback([this](float state) { + this->current_humidity = state; + // current humidity changed, publish state + this->publish_state(); + }); + this->current_humidity = this->humidity_sensor_->state; + } + // restore set points auto restore = this->restore_state_(); if (restore.has_value()) { diff --git a/esphome/components/climate_ir/climate_ir.h b/esphome/components/climate_ir/climate_ir.h index 62a43f0b2d..ac76d33853 100644 --- a/esphome/components/climate_ir/climate_ir.h +++ b/esphome/components/climate_ir/climate_ir.h @@ -43,6 +43,7 @@ class ClimateIR : public Component, void set_supports_cool(bool supports_cool) { this->supports_cool_ = supports_cool; } void set_supports_heat(bool supports_heat) { this->supports_heat_ = supports_heat; } void set_sensor(sensor::Sensor *sensor) { this->sensor_ = sensor; } + void set_humidity_sensor(sensor::Sensor *sensor) { this->humidity_sensor_ = sensor; } protected: float minimum_temperature_, maximum_temperature_, temperature_step_; @@ -67,6 +68,7 @@ class ClimateIR : public Component, climate::ClimatePresetMask presets_{}; sensor::Sensor *sensor_{nullptr}; + sensor::Sensor *humidity_sensor_{nullptr}; }; } // namespace climate_ir diff --git a/tests/components/climate_ir_lg/common.yaml b/tests/components/climate_ir_lg/common.yaml index da0d656b21..37011b16ee 100644 --- a/tests/components/climate_ir_lg/common.yaml +++ b/tests/components/climate_ir_lg/common.yaml @@ -1,4 +1,16 @@ +sensor: + - platform: template + id: temp_sensor + lambda: return 22.0; + update_interval: 60s + - platform: template + id: humidity_sensor + lambda: return 50.0; + update_interval: 60s + climate: - platform: climate_ir_lg name: LG Climate transmitter_id: xmitr + sensor: temp_sensor + humidity_sensor: humidity_sensor From 1accb4ff3488c1763da426faea38f73408714332 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Thu, 20 Nov 2025 10:58:21 -0500 Subject: [PATCH 178/415] [ltr501][ltr_als_ps] Rename enum to avoid collision with lwip defines (#12017) --- esphome/components/ltr501/ltr501.cpp | 10 +++++----- esphome/components/ltr501/ltr501.h | 4 ++-- esphome/components/ltr_als_ps/ltr_als_ps.cpp | 12 ++++++------ esphome/components/ltr_als_ps/ltr_als_ps.h | 4 ++-- 4 files changed, 15 insertions(+), 15 deletions(-) diff --git a/esphome/components/ltr501/ltr501.cpp b/esphome/components/ltr501/ltr501.cpp index be5a4ddccf..04de91e362 100644 --- a/esphome/components/ltr501/ltr501.cpp +++ b/esphome/components/ltr501/ltr501.cpp @@ -174,7 +174,7 @@ void LTRAlsPs501Component::loop() { break; case State::WAITING_FOR_DATA: - if (this->is_als_data_ready_(this->als_readings_) == DataAvail::DATA_OK) { + if (this->is_als_data_ready_(this->als_readings_) == LtrDataAvail::LTR_DATA_OK) { tries = 0; ESP_LOGV(TAG, "Reading sensor data assuming gain = %.0fx, time = %d ms", get_gain_coeff(this->als_readings_.gain), get_itime_ms(this->als_readings_.integration_time)); @@ -379,18 +379,18 @@ void LTRAlsPs501Component::configure_integration_time_(IntegrationTime501 time) } } -DataAvail LTRAlsPs501Component::is_als_data_ready_(AlsReadings &data) { +LtrDataAvail LTRAlsPs501Component::is_als_data_ready_(AlsReadings &data) { AlsPsStatusRegister als_status{0}; als_status.raw = this->reg((uint8_t) CommandRegisters::ALS_PS_STATUS).get(); if (!als_status.als_new_data) - return DataAvail::NO_DATA; + return LtrDataAvail::LTR_NO_DATA; ESP_LOGV(TAG, "Data ready, reported gain is %.0fx", get_gain_coeff(als_status.gain)); if (data.gain != als_status.gain) { ESP_LOGW(TAG, "Actual gain differs from requested (%.0f)", get_gain_coeff(data.gain)); - return DataAvail::BAD_DATA; + return LtrDataAvail::LTR_BAD_DATA; } data.gain = als_status.gain; - return DataAvail::DATA_OK; + return LtrDataAvail::LTR_DATA_OK; } void LTRAlsPs501Component::read_sensor_data_(AlsReadings &data) { diff --git a/esphome/components/ltr501/ltr501.h b/esphome/components/ltr501/ltr501.h index 849ff6bc23..02c025da30 100644 --- a/esphome/components/ltr501/ltr501.h +++ b/esphome/components/ltr501/ltr501.h @@ -11,7 +11,7 @@ namespace esphome { namespace ltr501 { -enum DataAvail : uint8_t { NO_DATA, BAD_DATA, DATA_OK }; +enum LtrDataAvail : uint8_t { LTR_NO_DATA, LTR_BAD_DATA, LTR_DATA_OK }; enum LtrType : uint8_t { LTR_TYPE_UNKNOWN = 0, @@ -106,7 +106,7 @@ class LTRAlsPs501Component : public PollingComponent, public i2c::I2CDevice { void configure_als_(); void configure_integration_time_(IntegrationTime501 time); void configure_gain_(AlsGain501 gain); - DataAvail is_als_data_ready_(AlsReadings &data); + LtrDataAvail is_als_data_ready_(AlsReadings &data); void read_sensor_data_(AlsReadings &data); bool are_adjustments_required_(AlsReadings &data); void apply_lux_calculation_(AlsReadings &data); diff --git a/esphome/components/ltr_als_ps/ltr_als_ps.cpp b/esphome/components/ltr_als_ps/ltr_als_ps.cpp index c3ea5848c8..f9c1474c85 100644 --- a/esphome/components/ltr_als_ps/ltr_als_ps.cpp +++ b/esphome/components/ltr_als_ps/ltr_als_ps.cpp @@ -165,7 +165,7 @@ void LTRAlsPsComponent::loop() { break; case State::WAITING_FOR_DATA: - if (this->is_als_data_ready_(this->als_readings_) == DataAvail::DATA_OK) { + if (this->is_als_data_ready_(this->als_readings_) == LtrDataAvail::LTR_DATA_OK) { tries = 0; ESP_LOGV(TAG, "Reading sensor data having gain = %.0fx, time = %d ms", get_gain_coeff(this->als_readings_.gain), get_itime_ms(this->als_readings_.integration_time)); @@ -376,23 +376,23 @@ void LTRAlsPsComponent::configure_integration_time_(IntegrationTime time) { } } -DataAvail LTRAlsPsComponent::is_als_data_ready_(AlsReadings &data) { +LtrDataAvail LTRAlsPsComponent::is_als_data_ready_(AlsReadings &data) { AlsPsStatusRegister als_status{0}; als_status.raw = this->reg((uint8_t) CommandRegisters::ALS_PS_STATUS).get(); if (!als_status.als_new_data) - return DataAvail::NO_DATA; + return LtrDataAvail::LTR_NO_DATA; if (als_status.data_invalid) { ESP_LOGW(TAG, "Data available but not valid"); - return DataAvail::BAD_DATA; + return LtrDataAvail::LTR_BAD_DATA; } ESP_LOGV(TAG, "Data ready, reported gain is %.0f", get_gain_coeff(als_status.gain)); if (data.gain != als_status.gain) { ESP_LOGW(TAG, "Actual gain differs from requested (%.0f)", get_gain_coeff(data.gain)); - return DataAvail::BAD_DATA; + return LtrDataAvail::LTR_BAD_DATA; } - return DataAvail::DATA_OK; + return LtrDataAvail::LTR_DATA_OK; } void LTRAlsPsComponent::read_sensor_data_(AlsReadings &data) { diff --git a/esphome/components/ltr_als_ps/ltr_als_ps.h b/esphome/components/ltr_als_ps/ltr_als_ps.h index 2c768009ab..c6052300de 100644 --- a/esphome/components/ltr_als_ps/ltr_als_ps.h +++ b/esphome/components/ltr_als_ps/ltr_als_ps.h @@ -11,7 +11,7 @@ namespace esphome { namespace ltr_als_ps { -enum DataAvail : uint8_t { NO_DATA, BAD_DATA, DATA_OK }; +enum LtrDataAvail : uint8_t { LTR_NO_DATA, LTR_BAD_DATA, LTR_DATA_OK }; enum LtrType : uint8_t { LTR_TYPE_UNKNOWN = 0, @@ -106,7 +106,7 @@ class LTRAlsPsComponent : public PollingComponent, public i2c::I2CDevice { void configure_als_(); void configure_integration_time_(IntegrationTime time); void configure_gain_(AlsGain gain); - DataAvail is_als_data_ready_(AlsReadings &data); + LtrDataAvail is_als_data_ready_(AlsReadings &data); void read_sensor_data_(AlsReadings &data); bool are_adjustments_required_(AlsReadings &data); void apply_lux_calculation_(AlsReadings &data); From a1e507baf817f99d81593130796dff92b25b948d Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Thu, 20 Nov 2025 12:10:28 -0500 Subject: [PATCH 179/415] [cst816][packet_transport][udp][wake_on_lan] Fix error messages (#12019) --- .../cst816/touchscreen/cst816_touchscreen.cpp | 2 +- .../components/packet_transport/packet_transport.cpp | 2 +- esphome/components/udp/udp_component.cpp | 10 +++++----- esphome/components/wake_on_lan/wake_on_lan.cpp | 2 +- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/esphome/components/cst816/touchscreen/cst816_touchscreen.cpp b/esphome/components/cst816/touchscreen/cst816_touchscreen.cpp index 0ba2d9df94..8ed9fa3f87 100644 --- a/esphome/components/cst816/touchscreen/cst816_touchscreen.cpp +++ b/esphome/components/cst816/touchscreen/cst816_touchscreen.cpp @@ -19,8 +19,8 @@ void CST816Touchscreen::continue_setup_() { case CST816T_CHIP_ID: break; default: - this->mark_failed(); this->status_set_error(str_sprintf("Unknown chip ID 0x%02X", this->chip_id_).c_str()); + this->mark_failed(); return; } this->write_byte(REG_IRQ_CTL, IRQ_EN_MOTION); diff --git a/esphome/components/packet_transport/packet_transport.cpp b/esphome/components/packet_transport/packet_transport.cpp index 8bde4ee505..857b40ca0e 100644 --- a/esphome/components/packet_transport/packet_transport.cpp +++ b/esphome/components/packet_transport/packet_transport.cpp @@ -195,8 +195,8 @@ static void add(std::vector &vec, const char *str) { void PacketTransport::setup() { this->name_ = App.get_name().c_str(); if (strlen(this->name_) > 255) { - this->mark_failed(); this->status_set_error("Device name exceeds 255 chars"); + this->mark_failed(); return; } this->resend_ping_key_ = this->ping_pong_enable_; diff --git a/esphome/components/udp/udp_component.cpp b/esphome/components/udp/udp_component.cpp index 8a9ce612b4..7714793e1c 100644 --- a/esphome/components/udp/udp_component.cpp +++ b/esphome/components/udp/udp_component.cpp @@ -21,8 +21,8 @@ void UDPComponent::setup() { if (this->should_broadcast_) { this->broadcast_socket_ = socket::socket(AF_INET, SOCK_DGRAM, IPPROTO_IP); if (this->broadcast_socket_ == nullptr) { - this->mark_failed(); this->status_set_error("Could not create socket"); + this->mark_failed(); return; } int enable = 1; @@ -41,15 +41,15 @@ void UDPComponent::setup() { if (this->should_listen_) { this->listen_socket_ = socket::socket(AF_INET, SOCK_DGRAM, IPPROTO_IP); if (this->listen_socket_ == nullptr) { - this->mark_failed(); this->status_set_error("Could not create socket"); + this->mark_failed(); return; } auto err = this->listen_socket_->setblocking(false); if (err < 0) { ESP_LOGE(TAG, "Unable to set nonblocking: errno %d", errno); - this->mark_failed(); this->status_set_error("Unable to set nonblocking"); + this->mark_failed(); return; } int enable = 1; @@ -73,8 +73,8 @@ void UDPComponent::setup() { err = this->listen_socket_->setsockopt(IPPROTO_IP, IP_ADD_MEMBERSHIP, &imreq, sizeof(imreq)); if (err < 0) { ESP_LOGE(TAG, "Failed to set IP_ADD_MEMBERSHIP. Error %d", errno); - this->mark_failed(); this->status_set_error("Failed to set IP_ADD_MEMBERSHIP"); + this->mark_failed(); return; } } @@ -82,8 +82,8 @@ void UDPComponent::setup() { err = this->listen_socket_->bind((struct sockaddr *) &server, sizeof(server)); if (err != 0) { ESP_LOGE(TAG, "Socket unable to bind: errno %d", errno); - this->mark_failed(); this->status_set_error("Unable to bind socket"); + this->mark_failed(); return; } } diff --git a/esphome/components/wake_on_lan/wake_on_lan.cpp b/esphome/components/wake_on_lan/wake_on_lan.cpp index adf5a080e5..7993abd7e7 100644 --- a/esphome/components/wake_on_lan/wake_on_lan.cpp +++ b/esphome/components/wake_on_lan/wake_on_lan.cpp @@ -67,8 +67,8 @@ void WakeOnLanButton::setup() { #if defined(USE_SOCKET_IMPL_BSD_SOCKETS) || defined(USE_SOCKET_IMPL_LWIP_SOCKETS) this->broadcast_socket_ = socket::socket(AF_INET, SOCK_DGRAM, IPPROTO_IP); if (this->broadcast_socket_ == nullptr) { - this->mark_failed(); this->status_set_error("Could not create socket"); + this->mark_failed(); return; } int enable = 1; From 3955b6637935cbdd129d2924d38c1ac9f712081d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 20 Nov 2025 12:04:20 -0600 Subject: [PATCH 180/415] [core] Deprecate status_set_error(const char*) and require LogString to prevent dangling pointers --- .../absolute_humidity/absolute_humidity.cpp | 2 +- esphome/components/aht10/aht10.cpp | 2 +- esphome/components/camera/camera.cpp | 2 +- .../cst816/touchscreen/cst816_touchscreen.cpp | 5 ++-- .../update/esp32_hosted_update.cpp | 10 ++++---- esphome/components/gdk101/gdk101.cpp | 6 ++--- .../update/http_request_update.cpp | 15 +++++------ esphome/components/lvgl/lvgl_esphome.cpp | 4 +-- esphome/components/max17043/max17043.cpp | 4 +-- .../mixer/speaker/mixer_speaker.cpp | 13 +++++----- esphome/components/nau7802/nau7802.cpp | 2 +- .../packet_transport/packet_transport.cpp | 2 +- .../resampler/speaker/resampler_speaker.cpp | 12 ++++----- esphome/components/sht4x/sht4x.cpp | 2 +- esphome/components/udp/udp_component.cpp | 10 ++++---- .../components/usb_host/usb_host_client.cpp | 2 +- .../usb_host/usb_host_component.cpp | 2 +- esphome/components/usb_uart/usb_uart.cpp | 4 +-- .../voice_assistant/voice_assistant.cpp | 2 +- .../components/wake_on_lan/wake_on_lan.cpp | 2 +- esphome/core/component.cpp | 25 +++++++++++++++++++ esphome/core/component.h | 3 +++ 22 files changed, 81 insertions(+), 50 deletions(-) diff --git a/esphome/components/absolute_humidity/absolute_humidity.cpp b/esphome/components/absolute_humidity/absolute_humidity.cpp index 2c5603ee3d..d16a024d86 100644 --- a/esphome/components/absolute_humidity/absolute_humidity.cpp +++ b/esphome/components/absolute_humidity/absolute_humidity.cpp @@ -87,7 +87,7 @@ void AbsoluteHumidityComponent::loop() { break; default: this->publish_state(NAN); - this->status_set_error("Invalid saturation vapor pressure equation selection!"); + this->status_set_error(LOG_STR("Invalid saturation vapor pressure equation selection!")); return; } ESP_LOGD(TAG, "Saturation vapor pressure %f kPa", es); diff --git a/esphome/components/aht10/aht10.cpp b/esphome/components/aht10/aht10.cpp index 53c712a7a7..03d9d9cd9e 100644 --- a/esphome/components/aht10/aht10.cpp +++ b/esphome/components/aht10/aht10.cpp @@ -83,7 +83,7 @@ void AHT10Component::setup() { void AHT10Component::restart_read_() { if (this->read_count_ == AHT10_ATTEMPTS) { this->read_count_ = 0; - this->status_set_error("Reading timed out"); + this->status_set_error(LOG_STR("Reading timed out")); return; } this->read_count_++; diff --git a/esphome/components/camera/camera.cpp b/esphome/components/camera/camera.cpp index 3bd632af5c..66b8138f38 100644 --- a/esphome/components/camera/camera.cpp +++ b/esphome/components/camera/camera.cpp @@ -8,7 +8,7 @@ Camera *Camera::global_camera = nullptr; Camera::Camera() { if (global_camera != nullptr) { - this->status_set_error("Multiple cameras are configured, but only one is supported."); + this->status_set_error(LOG_STR("Multiple cameras are configured, but only one is supported.")); this->mark_failed(); return; } diff --git a/esphome/components/cst816/touchscreen/cst816_touchscreen.cpp b/esphome/components/cst816/touchscreen/cst816_touchscreen.cpp index 8ed9fa3f87..f6280a75a1 100644 --- a/esphome/components/cst816/touchscreen/cst816_touchscreen.cpp +++ b/esphome/components/cst816/touchscreen/cst816_touchscreen.cpp @@ -19,13 +19,14 @@ void CST816Touchscreen::continue_setup_() { case CST816T_CHIP_ID: break; default: - this->status_set_error(str_sprintf("Unknown chip ID 0x%02X", this->chip_id_).c_str()); + ESP_LOGE(TAG, "Unknown chip ID: 0x%02X", this->chip_id_); + this->status_set_error(LOG_STR("Unknown chip ID")); this->mark_failed(); return; } this->write_byte(REG_IRQ_CTL, IRQ_EN_MOTION); } else if (!this->skip_probe_) { - this->status_set_error("Failed to read chip id"); + this->status_set_error(LOG_STR("Failed to read chip id")); this->mark_failed(); return; } diff --git a/esphome/components/esp32_hosted/update/esp32_hosted_update.cpp b/esphome/components/esp32_hosted/update/esp32_hosted_update.cpp index adbcc5bf11..f34a0ae10e 100644 --- a/esphome/components/esp32_hosted/update/esp32_hosted_update.cpp +++ b/esphome/components/esp32_hosted/update/esp32_hosted_update.cpp @@ -88,7 +88,7 @@ void Esp32HostedUpdate::perform(bool force) { hasher.add(this->firmware_data_, this->firmware_size_); hasher.calculate(); if (!hasher.equals_bytes(this->firmware_sha256_.data())) { - this->status_set_error("SHA256 verification failed"); + this->status_set_error(LOG_STR("SHA256 verification failed")); this->publish_state(); return; } @@ -105,7 +105,7 @@ void Esp32HostedUpdate::perform(bool force) { if (err != ESP_OK) { ESP_LOGE(TAG, "Failed to begin OTA: %s", esp_err_to_name(err)); this->state_ = prev_state; - this->status_set_error("Failed to begin OTA"); + this->status_set_error(LOG_STR("Failed to begin OTA")); this->publish_state(); return; } @@ -121,7 +121,7 @@ void Esp32HostedUpdate::perform(bool force) { ESP_LOGE(TAG, "Failed to write OTA data: %s", esp_err_to_name(err)); esp_hosted_slave_ota_end(); // NOLINT this->state_ = prev_state; - this->status_set_error("Failed to write OTA data"); + this->status_set_error(LOG_STR("Failed to write OTA data")); this->publish_state(); return; } @@ -134,7 +134,7 @@ void Esp32HostedUpdate::perform(bool force) { if (err != ESP_OK) { ESP_LOGE(TAG, "Failed to end OTA: %s", esp_err_to_name(err)); this->state_ = prev_state; - this->status_set_error("Failed to end OTA"); + this->status_set_error(LOG_STR("Failed to end OTA")); this->publish_state(); return; } @@ -144,7 +144,7 @@ void Esp32HostedUpdate::perform(bool force) { if (err != ESP_OK) { ESP_LOGE(TAG, "Failed to activate OTA: %s", esp_err_to_name(err)); this->state_ = prev_state; - this->status_set_error("Failed to activate OTA"); + this->status_set_error(LOG_STR("Failed to activate OTA")); this->publish_state(); return; } diff --git a/esphome/components/gdk101/gdk101.cpp b/esphome/components/gdk101/gdk101.cpp index 6c218f03d9..617e2138fb 100644 --- a/esphome/components/gdk101/gdk101.cpp +++ b/esphome/components/gdk101/gdk101.cpp @@ -36,20 +36,20 @@ void GDK101Component::setup() { uint8_t data[2]; // first, reset the sensor if (!this->reset_sensor_(data)) { - this->status_set_error("Reset failed!"); + this->status_set_error(LOG_STR("Reset failed!")); this->mark_failed(); return; } // sensor should acknowledge success of the reset procedure if (data[0] != 1) { - this->status_set_error("Reset not acknowledged!"); + this->status_set_error(LOG_STR("Reset not acknowledged!")); this->mark_failed(); return; } delay(10); // read firmware version if (!this->read_fw_version_(data)) { - this->status_set_error("Failed to read firmware version"); + this->status_set_error(LOG_STR("Failed to read firmware version")); this->mark_failed(); return; } diff --git a/esphome/components/http_request/update/http_request_update.cpp b/esphome/components/http_request/update/http_request_update.cpp index 06aa6da6a4..c91b0eba73 100644 --- a/esphome/components/http_request/update/http_request_update.cpp +++ b/esphome/components/http_request/update/http_request_update.cpp @@ -29,7 +29,7 @@ void HttpRequestUpdate::setup() { this->publish_state(); } else if (state == ota::OTAState::OTA_ABORT || state == ota::OTAState::OTA_ERROR) { this->state_ = update::UPDATE_STATE_AVAILABLE; - this->status_set_error("Failed to install firmware"); + this->status_set_error(LOG_STR("Failed to install firmware")); this->publish_state(); } }); @@ -49,18 +49,19 @@ void HttpRequestUpdate::update_task(void *params) { auto container = this_update->request_parent_->get(this_update->source_url_); if (container == nullptr || container->status_code != HTTP_STATUS_OK) { - std::string msg = str_sprintf("Failed to fetch manifest from %s", this_update->source_url_.c_str()); + ESP_LOGE(TAG, "Failed to fetch manifest from %s", this_update->source_url_.c_str()); // Defer to main loop to avoid race condition on component_state_ read-modify-write - this_update->defer([this_update, msg]() { this_update->status_set_error(msg.c_str()); }); + this_update->defer([this_update]() { this_update->status_set_error(LOG_STR("Failed to fetch manifest")); }); UPDATE_RETURN; } RAMAllocator allocator; uint8_t *data = allocator.allocate(container->content_length); if (data == nullptr) { - std::string msg = str_sprintf("Failed to allocate %zu bytes for manifest", container->content_length); + ESP_LOGE(TAG, "Failed to allocate %zu bytes for manifest", container->content_length); // Defer to main loop to avoid race condition on component_state_ read-modify-write - this_update->defer([this_update, msg]() { this_update->status_set_error(msg.c_str()); }); + this_update->defer( + [this_update]() { this_update->status_set_error(LOG_STR("Failed to allocate memory for manifest")); }); container->end(); UPDATE_RETURN; } @@ -121,9 +122,9 @@ void HttpRequestUpdate::update_task(void *params) { } if (!valid) { - std::string msg = str_sprintf("Failed to parse JSON from %s", this_update->source_url_.c_str()); + ESP_LOGE(TAG, "Failed to parse JSON from %s", this_update->source_url_.c_str()); // Defer to main loop to avoid race condition on component_state_ read-modify-write - this_update->defer([this_update, msg]() { this_update->status_set_error(msg.c_str()); }); + this_update->defer([this_update]() { this_update->status_set_error(LOG_STR("Failed to parse manifest JSON")); }); UPDATE_RETURN; } diff --git a/esphome/components/lvgl/lvgl_esphome.cpp b/esphome/components/lvgl/lvgl_esphome.cpp index 05005b0217..fbcd68378c 100644 --- a/esphome/components/lvgl/lvgl_esphome.cpp +++ b/esphome/components/lvgl/lvgl_esphome.cpp @@ -466,7 +466,7 @@ void LvglComponent::setup() { buffer = lv_custom_mem_alloc(buf_bytes); // NOLINT } if (buffer == nullptr) { - this->status_set_error("Memory allocation failure"); + this->status_set_error(LOG_STR("Memory allocation failure")); this->mark_failed(); return; } @@ -479,7 +479,7 @@ void LvglComponent::setup() { if (this->rotation != display::DISPLAY_ROTATION_0_DEGREES) { this->rotate_buf_ = static_cast(lv_custom_mem_alloc(buf_bytes)); // NOLINT if (this->rotate_buf_ == nullptr) { - this->status_set_error("Memory allocation failure"); + this->status_set_error(LOG_STR("Memory allocation failure")); this->mark_failed(); return; } diff --git a/esphome/components/max17043/max17043.cpp b/esphome/components/max17043/max17043.cpp index f605fb1324..e8cf4d5ab1 100644 --- a/esphome/components/max17043/max17043.cpp +++ b/esphome/components/max17043/max17043.cpp @@ -57,14 +57,14 @@ void MAX17043Component::setup() { if (config_reg != MAX17043_CONFIG_POWER_UP_DEFAULT) { ESP_LOGE(TAG, "Device does not appear to be a MAX17043"); - this->status_set_error("unrecognised"); + this->status_set_error(LOG_STR("unrecognised")); this->mark_failed(); return; } // need to write back to config register to reset the sleep bit if (!this->write_byte_16(MAX17043_CONFIG, MAX17043_CONFIG_POWER_UP_DEFAULT)) { - this->status_set_error("sleep reset failed"); + this->status_set_error(LOG_STR("sleep reset failed")); this->mark_failed(); return; } diff --git a/esphome/components/mixer/speaker/mixer_speaker.cpp b/esphome/components/mixer/speaker/mixer_speaker.cpp index b0b64f5709..043b629cf1 100644 --- a/esphome/components/mixer/speaker/mixer_speaker.cpp +++ b/esphome/components/mixer/speaker/mixer_speaker.cpp @@ -78,19 +78,20 @@ void SourceSpeaker::loop() { } else { switch (err) { case ESP_ERR_NO_MEM: - this->status_set_error("Failed to start mixer: not enough memory"); + this->status_set_error(LOG_STR("Failed to start mixer: not enough memory")); break; case ESP_ERR_NOT_SUPPORTED: - this->status_set_error("Failed to start mixer: unsupported bits per sample"); + this->status_set_error(LOG_STR("Failed to start mixer: unsupported bits per sample")); break; case ESP_ERR_INVALID_ARG: - this->status_set_error("Failed to start mixer: audio stream isn't compatible with the other audio stream."); + this->status_set_error( + LOG_STR("Failed to start mixer: audio stream isn't compatible with the other audio stream.")); break; case ESP_ERR_INVALID_STATE: - this->status_set_error("Failed to start mixer: mixer task failed to start"); + this->status_set_error(LOG_STR("Failed to start mixer: mixer task failed to start")); break; default: - this->status_set_error("Failed to start mixer"); + this->status_set_error(LOG_STR("Failed to start mixer")); break; } @@ -317,7 +318,7 @@ void MixerSpeaker::loop() { xEventGroupClearBits(this->event_group_, MixerEventGroupBits::STATE_STARTING); } if (event_group_bits & MixerEventGroupBits::ERR_ESP_NO_MEM) { - this->status_set_error("Failed to allocate the mixer's internal buffer"); + this->status_set_error(LOG_STR("Failed to allocate the mixer's internal buffer")); xEventGroupClearBits(this->event_group_, MixerEventGroupBits::ERR_ESP_NO_MEM); } if (event_group_bits & MixerEventGroupBits::STATE_RUNNING) { diff --git a/esphome/components/nau7802/nau7802.cpp b/esphome/components/nau7802/nau7802.cpp index 6a31b754f7..11f63a9a33 100644 --- a/esphome/components/nau7802/nau7802.cpp +++ b/esphome/components/nau7802/nau7802.cpp @@ -278,7 +278,7 @@ void NAU7802Sensor::loop() { this->set_calibration_failure_(true); this->state_ = CalibrationState::INACTIVE; ESP_LOGE(TAG, "Failed to calibrate sensor"); - this->status_set_error("Calibration Failed"); + this->status_set_error(LOG_STR("Calibration Failed")); return; } diff --git a/esphome/components/packet_transport/packet_transport.cpp b/esphome/components/packet_transport/packet_transport.cpp index 857b40ca0e..37e5f3d9e1 100644 --- a/esphome/components/packet_transport/packet_transport.cpp +++ b/esphome/components/packet_transport/packet_transport.cpp @@ -195,7 +195,7 @@ static void add(std::vector &vec, const char *str) { void PacketTransport::setup() { this->name_ = App.get_name().c_str(); if (strlen(this->name_) > 255) { - this->status_set_error("Device name exceeds 255 chars"); + this->status_set_error(LOG_STR("Device name exceeds 255 chars")); this->mark_failed(); return; } diff --git a/esphome/components/resampler/speaker/resampler_speaker.cpp b/esphome/components/resampler/speaker/resampler_speaker.cpp index 5e5615cbb9..ad61aca084 100644 --- a/esphome/components/resampler/speaker/resampler_speaker.cpp +++ b/esphome/components/resampler/speaker/resampler_speaker.cpp @@ -66,17 +66,17 @@ void ResamplerSpeaker::loop() { } if (event_group_bits & ResamplingEventGroupBits::ERR_ESP_NO_MEM) { - this->status_set_error("Resampler task failed to allocate the internal buffers"); + this->status_set_error(LOG_STR("Resampler task failed to allocate the internal buffers")); xEventGroupClearBits(this->event_group_, ResamplingEventGroupBits::ERR_ESP_NO_MEM); this->state_ = speaker::STATE_STOPPING; } if (event_group_bits & ResamplingEventGroupBits::ERR_ESP_NOT_SUPPORTED) { - this->status_set_error("Cannot resample due to an unsupported audio stream"); + this->status_set_error(LOG_STR("Cannot resample due to an unsupported audio stream")); xEventGroupClearBits(this->event_group_, ResamplingEventGroupBits::ERR_ESP_NOT_SUPPORTED); this->state_ = speaker::STATE_STOPPING; } if (event_group_bits & ResamplingEventGroupBits::ERR_ESP_FAIL) { - this->status_set_error("Resampler task failed"); + this->status_set_error(LOG_STR("Resampler task failed")); xEventGroupClearBits(this->event_group_, ResamplingEventGroupBits::ERR_ESP_FAIL); this->state_ = speaker::STATE_STOPPING; } @@ -106,12 +106,12 @@ void ResamplerSpeaker::loop() { } else { switch (err) { case ESP_ERR_INVALID_STATE: - this->status_set_error("Failed to start resampler: resampler task failed to start"); + this->status_set_error(LOG_STR("Failed to start resampler: resampler task failed to start")); break; case ESP_ERR_NO_MEM: - this->status_set_error("Failed to start resampler: not enough memory for task stack"); + this->status_set_error(LOG_STR("Failed to start resampler: not enough memory for task stack")); default: - this->status_set_error("Failed to start resampler"); + this->status_set_error(LOG_STR("Failed to start resampler")); break; } diff --git a/esphome/components/sht4x/sht4x.cpp b/esphome/components/sht4x/sht4x.cpp index 62b8717ded..617b19ef3e 100644 --- a/esphome/components/sht4x/sht4x.cpp +++ b/esphome/components/sht4x/sht4x.cpp @@ -13,7 +13,7 @@ void SHT4XComponent::start_heater_() { ESP_LOGD(TAG, "Heater turning on"); if (this->write(cmd, 1) != i2c::ERROR_OK) { - this->status_set_error("Failed to turn on heater"); + this->status_set_error(LOG_STR("Failed to turn on heater")); } } diff --git a/esphome/components/udp/udp_component.cpp b/esphome/components/udp/udp_component.cpp index 7714793e1c..9105ced21e 100644 --- a/esphome/components/udp/udp_component.cpp +++ b/esphome/components/udp/udp_component.cpp @@ -21,7 +21,7 @@ void UDPComponent::setup() { if (this->should_broadcast_) { this->broadcast_socket_ = socket::socket(AF_INET, SOCK_DGRAM, IPPROTO_IP); if (this->broadcast_socket_ == nullptr) { - this->status_set_error("Could not create socket"); + this->status_set_error(LOG_STR("Could not create socket")); this->mark_failed(); return; } @@ -41,14 +41,14 @@ void UDPComponent::setup() { if (this->should_listen_) { this->listen_socket_ = socket::socket(AF_INET, SOCK_DGRAM, IPPROTO_IP); if (this->listen_socket_ == nullptr) { - this->status_set_error("Could not create socket"); + this->status_set_error(LOG_STR("Could not create socket")); this->mark_failed(); return; } auto err = this->listen_socket_->setblocking(false); if (err < 0) { ESP_LOGE(TAG, "Unable to set nonblocking: errno %d", errno); - this->status_set_error("Unable to set nonblocking"); + this->status_set_error(LOG_STR("Unable to set nonblocking")); this->mark_failed(); return; } @@ -73,7 +73,7 @@ void UDPComponent::setup() { err = this->listen_socket_->setsockopt(IPPROTO_IP, IP_ADD_MEMBERSHIP, &imreq, sizeof(imreq)); if (err < 0) { ESP_LOGE(TAG, "Failed to set IP_ADD_MEMBERSHIP. Error %d", errno); - this->status_set_error("Failed to set IP_ADD_MEMBERSHIP"); + this->status_set_error(LOG_STR("Failed to set IP_ADD_MEMBERSHIP")); this->mark_failed(); return; } @@ -82,7 +82,7 @@ void UDPComponent::setup() { err = this->listen_socket_->bind((struct sockaddr *) &server, sizeof(server)); if (err != 0) { ESP_LOGE(TAG, "Socket unable to bind: errno %d", errno); - this->status_set_error("Unable to bind socket"); + this->status_set_error(LOG_STR("Unable to bind socket")); this->mark_failed(); return; } diff --git a/esphome/components/usb_host/usb_host_client.cpp b/esphome/components/usb_host/usb_host_client.cpp index 4c09cf8a49..fe61353b5d 100644 --- a/esphome/components/usb_host/usb_host_client.cpp +++ b/esphome/components/usb_host/usb_host_client.cpp @@ -188,7 +188,7 @@ void USBClient::setup() { auto err = usb_host_client_register(&config, &this->handle_); if (err != ESP_OK) { ESP_LOGE(TAG, "client register failed: %s", esp_err_to_name(err)); - this->status_set_error("Client register failed"); + this->status_set_error(LOG_STR("Client register failed")); this->mark_failed(); return; } diff --git a/esphome/components/usb_host/usb_host_component.cpp b/esphome/components/usb_host/usb_host_component.cpp index fb19239c73..1e70c289df 100644 --- a/esphome/components/usb_host/usb_host_component.cpp +++ b/esphome/components/usb_host/usb_host_component.cpp @@ -11,7 +11,7 @@ void USBHost::setup() { usb_host_config_t config{}; if (usb_host_install(&config) != ESP_OK) { - this->status_set_error("usb_host_install failed"); + this->status_set_error(LOG_STR("usb_host_install failed")); this->mark_failed(); return; } diff --git a/esphome/components/usb_uart/usb_uart.cpp b/esphome/components/usb_uart/usb_uart.cpp index c24fffb11d..6720c1e690 100644 --- a/esphome/components/usb_uart/usb_uart.cpp +++ b/esphome/components/usb_uart/usb_uart.cpp @@ -320,7 +320,7 @@ static void fix_mps(const usb_ep_desc_t *ep) { void USBUartTypeCdcAcm::on_connected() { auto cdc_devs = this->parse_descriptors(this->device_handle_); if (cdc_devs.empty()) { - this->status_set_error("No CDC-ACM device found"); + this->status_set_error(LOG_STR("No CDC-ACM device found")); this->disconnect(); return; } @@ -341,7 +341,7 @@ void USBUartTypeCdcAcm::on_connected() { if (err != ESP_OK) { ESP_LOGE(TAG, "usb_host_interface_claim failed: %s, channel=%d, intf=%d", esp_err_to_name(err), channel->index_, channel->cdc_dev_.bulk_interface_number); - this->status_set_error("usb_host_interface_claim failed"); + this->status_set_error(LOG_STR("usb_host_interface_claim failed")); this->disconnect(); return; } diff --git a/esphome/components/voice_assistant/voice_assistant.cpp b/esphome/components/voice_assistant/voice_assistant.cpp index fd35dc7d09..551f0370f2 100644 --- a/esphome/components/voice_assistant/voice_assistant.cpp +++ b/esphome/components/voice_assistant/voice_assistant.cpp @@ -206,7 +206,7 @@ void VoiceAssistant::loop() { case State::START_MICROPHONE: { ESP_LOGD(TAG, "Starting Microphone"); if (!this->allocate_buffers_()) { - this->status_set_error("Failed to allocate buffers"); + this->status_set_error(LOG_STR("Failed to allocate buffers")); return; } if (this->status_has_error()) { diff --git a/esphome/components/wake_on_lan/wake_on_lan.cpp b/esphome/components/wake_on_lan/wake_on_lan.cpp index 7993abd7e7..8c5bdac54b 100644 --- a/esphome/components/wake_on_lan/wake_on_lan.cpp +++ b/esphome/components/wake_on_lan/wake_on_lan.cpp @@ -67,7 +67,7 @@ void WakeOnLanButton::setup() { #if defined(USE_SOCKET_IMPL_BSD_SOCKETS) || defined(USE_SOCKET_IMPL_LWIP_SOCKETS) this->broadcast_socket_ = socket::socket(AF_INET, SOCK_DGRAM, IPPROTO_IP); if (this->broadcast_socket_ == nullptr) { - this->status_set_error("Could not create socket"); + this->status_set_error(LOG_STR("Could not create socket")); this->mark_failed(); return; } diff --git a/esphome/core/component.cpp b/esphome/core/component.cpp index de3dd99d0c..6ccebd0771 100644 --- a/esphome/core/component.cpp +++ b/esphome/core/component.cpp @@ -330,6 +330,31 @@ void Component::status_set_error(const char *message) { component_error_messages->emplace_back(ComponentErrorMessage{this, message}); } } +void Component::status_set_error(const LogString *message) { + if ((this->component_state_ & STATUS_LED_ERROR) != 0) + return; + this->component_state_ |= STATUS_LED_ERROR; + App.app_state_ |= STATUS_LED_ERROR; + ESP_LOGE(TAG, "%s set Error flag: %s", LOG_STR_ARG(this->get_component_log_str()), + message ? LOG_STR_ARG(message) : LOG_STR_LITERAL("unspecified")); + if (message != nullptr) { + // Lazy allocate the error messages vector if needed + if (!component_error_messages) { + component_error_messages = std::make_unique>(); + } + // Store the LogString pointer directly (safe because LogString is always in flash/static memory) + const char *msg_ptr = LOG_STR_ARG(message); + // Check if this component already has an error message + for (auto &entry : *component_error_messages) { + if (entry.component == this) { + entry.message = msg_ptr; + return; + } + } + // Add new error message + component_error_messages->emplace_back(ComponentErrorMessage{this, msg_ptr}); + } +} void Component::status_clear_warning() { if ((this->component_state_ & STATUS_LED_WARNING) == 0) return; diff --git a/esphome/core/component.h b/esphome/core/component.h index 462e0e301c..e8782f1327 100644 --- a/esphome/core/component.h +++ b/esphome/core/component.h @@ -216,7 +216,10 @@ class Component { void status_set_warning(const char *message = nullptr); void status_set_warning(const LogString *message); + // Remove before 2026.12.0 + ESPDEPRECATED("Use status_set_error(LOG_STR(\"message\")) instead. Removed in 2026.12.0", "2025.6.0") void status_set_error(const char *message = nullptr); + void status_set_error(const LogString *message); void status_clear_warning(); From 7cbc890c0d9446c08d2647332adb8f6dc78cb2d0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 20 Nov 2025 12:12:14 -0600 Subject: [PATCH 181/415] syntax --- esphome/core/component.h | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/esphome/core/component.h b/esphome/core/component.h index e8782f1327..35f92c5d24 100644 --- a/esphome/core/component.h +++ b/esphome/core/component.h @@ -5,6 +5,7 @@ #include #include +#include "esphome/core/helpers.h" #include "esphome/core/log.h" #include "esphome/core/optional.h" @@ -217,7 +218,7 @@ class Component { void status_set_warning(const LogString *message); // Remove before 2026.12.0 - ESPDEPRECATED("Use status_set_error(LOG_STR(\"message\")) instead. Removed in 2026.12.0", "2025.6.0") + ESPDEPRECATED("Use status_set_error(LOG_STR(\"message\")) instead. Removed in 2026.12.0", "2025.6.0"); void status_set_error(const char *message = nullptr); void status_set_error(const LogString *message); From e37885ade546da1ee1fc01c26ddc79ff132513e4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 20 Nov 2025 12:12:42 -0600 Subject: [PATCH 182/415] syntax --- esphome/core/component.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/core/component.h b/esphome/core/component.h index 35f92c5d24..e1153fdf53 100644 --- a/esphome/core/component.h +++ b/esphome/core/component.h @@ -218,7 +218,7 @@ class Component { void status_set_warning(const LogString *message); // Remove before 2026.12.0 - ESPDEPRECATED("Use status_set_error(LOG_STR(\"message\")) instead. Removed in 2026.12.0", "2025.6.0"); + ESPDEPRECATED("Use status_set_error(LOG_STR(message)) instead. Removed in 2026.12.0", "2025.6.0") void status_set_error(const char *message = nullptr); void status_set_error(const LogString *message); From 01addeae080b0cce517ed296d6ea4a998d454ae6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 20 Nov 2025 13:11:41 -0600 Subject: [PATCH 183/415] Bump actions/checkout from 5.0.1 to 6.0.0 (#12022) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/auto-label-pr.yml | 2 +- .github/workflows/ci-api-proto.yml | 2 +- .github/workflows/ci-clang-tidy-hash.yml | 2 +- .github/workflows/ci-docker.yml | 2 +- .../workflows/ci-memory-impact-comment.yml | 2 +- .github/workflows/ci.yml | 30 +++++++++---------- .github/workflows/codeql.yml | 2 +- .github/workflows/release.yml | 8 ++--- .github/workflows/sync-device-classes.yml | 4 +-- 9 files changed, 27 insertions(+), 27 deletions(-) diff --git a/.github/workflows/auto-label-pr.yml b/.github/workflows/auto-label-pr.yml index fb284c9d8c..8d8e08a5fc 100644 --- a/.github/workflows/auto-label-pr.yml +++ b/.github/workflows/auto-label-pr.yml @@ -22,7 +22,7 @@ jobs: if: github.event.action != 'labeled' || github.event.sender.type != 'Bot' steps: - name: Checkout - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 + uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 - name: Generate a token id: generate-token diff --git a/.github/workflows/ci-api-proto.yml b/.github/workflows/ci-api-proto.yml index be5af1aff1..b377ca76d8 100644 --- a/.github/workflows/ci-api-proto.yml +++ b/.github/workflows/ci-api-proto.yml @@ -21,7 +21,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 + uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 - name: Set up Python uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0 with: diff --git a/.github/workflows/ci-clang-tidy-hash.yml b/.github/workflows/ci-clang-tidy-hash.yml index aebf07949d..9556b99015 100644 --- a/.github/workflows/ci-clang-tidy-hash.yml +++ b/.github/workflows/ci-clang-tidy-hash.yml @@ -21,7 +21,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 + uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 - name: Set up Python uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0 diff --git a/.github/workflows/ci-docker.yml b/.github/workflows/ci-docker.yml index 9bb983b993..5287d92b10 100644 --- a/.github/workflows/ci-docker.yml +++ b/.github/workflows/ci-docker.yml @@ -43,7 +43,7 @@ jobs: - "docker" # - "lint" steps: - - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 + - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 - name: Set up Python uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0 with: diff --git a/.github/workflows/ci-memory-impact-comment.yml b/.github/workflows/ci-memory-impact-comment.yml index c82ae30f55..6ca58e252e 100644 --- a/.github/workflows/ci-memory-impact-comment.yml +++ b/.github/workflows/ci-memory-impact-comment.yml @@ -49,7 +49,7 @@ jobs: - name: Check out code from base repository if: steps.pr.outputs.skip != 'true' - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 + uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 with: # Always check out from the base repository (esphome/esphome), never from forks # Use the PR's target branch to ensure we run trusted code from the main repo diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5293c62d34..9c2fab0912 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -36,7 +36,7 @@ jobs: cache-key: ${{ steps.cache-key.outputs.key }} steps: - name: Check out code from GitHub - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 + uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 - name: Generate cache-key id: cache-key run: echo key="${{ hashFiles('requirements.txt', 'requirements_test.txt', '.pre-commit-config.yaml') }}" >> $GITHUB_OUTPUT @@ -70,7 +70,7 @@ jobs: if: needs.determine-jobs.outputs.python-linters == 'true' steps: - name: Check out code from GitHub - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 + uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 - name: Restore Python uses: ./.github/actions/restore-python with: @@ -91,7 +91,7 @@ jobs: - common steps: - name: Check out code from GitHub - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 + uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 - name: Restore Python uses: ./.github/actions/restore-python with: @@ -132,7 +132,7 @@ jobs: - common steps: - name: Check out code from GitHub - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 + uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 - name: Restore Python id: restore-python uses: ./.github/actions/restore-python @@ -183,7 +183,7 @@ jobs: component-test-batches: ${{ steps.determine.outputs.component-test-batches }} steps: - name: Check out code from GitHub - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 + uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 with: # Fetch enough history to find the merge base fetch-depth: 2 @@ -237,7 +237,7 @@ jobs: if: needs.determine-jobs.outputs.integration-tests == 'true' steps: - name: Check out code from GitHub - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 + uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 - name: Set up Python 3.13 id: python uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0 @@ -273,7 +273,7 @@ jobs: if: github.event_name == 'pull_request' && (needs.determine-jobs.outputs.cpp-unit-tests-run-all == 'true' || needs.determine-jobs.outputs.cpp-unit-tests-components != '[]') steps: - name: Check out code from GitHub - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 + uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 - name: Restore Python uses: ./.github/actions/restore-python @@ -321,7 +321,7 @@ jobs: steps: - name: Check out code from GitHub - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 + uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 with: # Need history for HEAD~1 to work for checking changed files fetch-depth: 2 @@ -400,7 +400,7 @@ jobs: GH_TOKEN: ${{ github.token }} steps: - name: Check out code from GitHub - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 + uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 with: # Need history for HEAD~1 to work for checking changed files fetch-depth: 2 @@ -489,7 +489,7 @@ jobs: steps: - name: Check out code from GitHub - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 + uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 with: # Need history for HEAD~1 to work for checking changed files fetch-depth: 2 @@ -577,7 +577,7 @@ jobs: version: 1.0 - name: Check out code from GitHub - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 + uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 - name: Restore Python uses: ./.github/actions/restore-python with: @@ -662,7 +662,7 @@ jobs: if: github.event_name == 'pull_request' && !startsWith(github.base_ref, 'beta') && !startsWith(github.base_ref, 'release') steps: - name: Check out code from GitHub - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 + uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 - name: Restore Python uses: ./.github/actions/restore-python with: @@ -688,7 +688,7 @@ jobs: skip: ${{ steps.check-script.outputs.skip }} steps: - name: Check out target branch - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 + uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 with: ref: ${{ github.base_ref }} @@ -840,7 +840,7 @@ jobs: flash_usage: ${{ steps.extract.outputs.flash_usage }} steps: - name: Check out PR branch - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 + uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 - name: Restore Python uses: ./.github/actions/restore-python with: @@ -908,7 +908,7 @@ jobs: GH_TOKEN: ${{ github.token }} steps: - name: Check out code - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 + uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 - name: Restore Python uses: ./.github/actions/restore-python with: diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 21fff10c95..80fab8819a 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -54,7 +54,7 @@ jobs: # your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages steps: - name: Checkout repository - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 + uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a064f6ef3a..497ecd29e7 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -20,7 +20,7 @@ jobs: branch_build: ${{ steps.tag.outputs.branch_build }} deploy_env: ${{ steps.tag.outputs.deploy_env }} steps: - - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 + - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 - name: Get tag id: tag # yamllint disable rule:line-length @@ -60,7 +60,7 @@ jobs: contents: read id-token: write steps: - - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 + - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 - name: Set up Python uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0 with: @@ -92,7 +92,7 @@ jobs: os: "ubuntu-24.04-arm" steps: - - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 + - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 - name: Set up Python uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0 with: @@ -168,7 +168,7 @@ jobs: - ghcr - dockerhub steps: - - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 + - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 - name: Download digests uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0 diff --git a/.github/workflows/sync-device-classes.yml b/.github/workflows/sync-device-classes.yml index 4fc287b067..2e36dc517d 100644 --- a/.github/workflows/sync-device-classes.yml +++ b/.github/workflows/sync-device-classes.yml @@ -13,10 +13,10 @@ jobs: if: github.repository == 'esphome/esphome' steps: - name: Checkout - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 + uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 - name: Checkout Home Assistant - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 + uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 with: repository: home-assistant/core path: lib/home-assistant From fae833b73bc651e48473f8b7ead14ba3aab34610 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 20 Nov 2025 19:49:10 +0000 Subject: [PATCH 184/415] Initial plan From 55d73440869736bb485845da651dd26ef34b2b6f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 20 Nov 2025 19:54:05 +0000 Subject: [PATCH 185/415] Remove gpio_intr_enable() call to fix level-triggered interrupt panic Co-authored-by: jesserockz <3060199+jesserockz@users.noreply.github.com> --- esphome/components/esp32/gpio.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/esphome/components/esp32/gpio.cpp b/esphome/components/esp32/gpio.cpp index a98245b889..392499836c 100644 --- a/esphome/components/esp32/gpio.cpp +++ b/esphome/components/esp32/gpio.cpp @@ -85,7 +85,6 @@ void ESP32InternalGPIOPin::attach_interrupt(void (*func)(void *), void *arg, gpi break; } gpio_set_intr_type(this->get_pin_num(), idf_type); - gpio_intr_enable(this->get_pin_num()); if (!isr_service_installed) { auto res = gpio_install_isr_service(ESP_INTR_FLAG_LEVEL3); if (res != ESP_OK) { From ddf1e27ac34a014ae66fd80936cd00be885cd0d0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 20 Nov 2025 22:55:08 +0000 Subject: [PATCH 186/415] Move gpio_intr_enable after gpio_isr_handler_add per review feedback Co-authored-by: jesserockz <3060199+jesserockz@users.noreply.github.com> --- esphome/components/esp32/gpio.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/esphome/components/esp32/gpio.cpp b/esphome/components/esp32/gpio.cpp index 392499836c..5c4872e6fb 100644 --- a/esphome/components/esp32/gpio.cpp +++ b/esphome/components/esp32/gpio.cpp @@ -94,6 +94,7 @@ void ESP32InternalGPIOPin::attach_interrupt(void (*func)(void *), void *arg, gpi isr_service_installed = true; } gpio_isr_handler_add(this->get_pin_num(), func, arg); + gpio_intr_enable(this->get_pin_num()); } std::string ESP32InternalGPIOPin::dump_summary() const { From 0dea7a23e3d0a035cdee7ccfeea303eb083da072 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Fri, 21 Nov 2025 07:39:59 -0500 Subject: [PATCH 187/415] [jsn_sr04t] Fix model AJ_SR04M (#11992) --- esphome/components/jsn_sr04t/jsn_sr04t.cpp | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/esphome/components/jsn_sr04t/jsn_sr04t.cpp b/esphome/components/jsn_sr04t/jsn_sr04t.cpp index 077d4e58ea..84181dac48 100644 --- a/esphome/components/jsn_sr04t/jsn_sr04t.cpp +++ b/esphome/components/jsn_sr04t/jsn_sr04t.cpp @@ -10,7 +10,7 @@ namespace jsn_sr04t { static const char *const TAG = "jsn_sr04t.sensor"; void Jsnsr04tComponent::update() { - this->write_byte(0x55); + this->write_byte((this->model_ == AJ_SR04M) ? 0x01 : 0x55); ESP_LOGV(TAG, "Request read out from sensor"); } @@ -31,19 +31,10 @@ void Jsnsr04tComponent::loop() { } void Jsnsr04tComponent::check_buffer_() { - uint8_t checksum = 0; - switch (this->model_) { - case JSN_SR04T: - checksum = this->buffer_[0] + this->buffer_[1] + this->buffer_[2]; - break; - case AJ_SR04M: - checksum = this->buffer_[1] + this->buffer_[2]; - break; - } - + uint8_t checksum = this->buffer_[0] + this->buffer_[1] + this->buffer_[2]; if (this->buffer_[3] == checksum) { uint16_t distance = encode_uint16(this->buffer_[1], this->buffer_[2]); - if (distance > 250) { + if (distance > ((this->model_ == AJ_SR04M) ? 200 : 250)) { float meters = distance / 1000.0f; ESP_LOGV(TAG, "Distance from sensor: %umm, %.3fm", distance, meters); this->publish_state(meters); From 150e26dc2bfbf2155459c39c5f05873301254968 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 21 Nov 2025 06:41:48 -0600 Subject: [PATCH 188/415] [cst816][http_request] Fix status_set_error() dangling pointer bugs (#12033) --- .../cst816/touchscreen/cst816_touchscreen.cpp | 3 ++- .../http_request/update/http_request_update.cpp | 12 ++++++------ 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/esphome/components/cst816/touchscreen/cst816_touchscreen.cpp b/esphome/components/cst816/touchscreen/cst816_touchscreen.cpp index 8ed9fa3f87..0560f1b475 100644 --- a/esphome/components/cst816/touchscreen/cst816_touchscreen.cpp +++ b/esphome/components/cst816/touchscreen/cst816_touchscreen.cpp @@ -19,7 +19,8 @@ void CST816Touchscreen::continue_setup_() { case CST816T_CHIP_ID: break; default: - this->status_set_error(str_sprintf("Unknown chip ID 0x%02X", this->chip_id_).c_str()); + ESP_LOGE(TAG, "Unknown chip ID: 0x%02X", this->chip_id_); + this->status_set_error("Unknown chip ID"); this->mark_failed(); return; } diff --git a/esphome/components/http_request/update/http_request_update.cpp b/esphome/components/http_request/update/http_request_update.cpp index 06aa6da6a4..9dbf8d181a 100644 --- a/esphome/components/http_request/update/http_request_update.cpp +++ b/esphome/components/http_request/update/http_request_update.cpp @@ -49,18 +49,18 @@ void HttpRequestUpdate::update_task(void *params) { auto container = this_update->request_parent_->get(this_update->source_url_); if (container == nullptr || container->status_code != HTTP_STATUS_OK) { - std::string msg = str_sprintf("Failed to fetch manifest from %s", this_update->source_url_.c_str()); + ESP_LOGE(TAG, "Failed to fetch manifest from %s", this_update->source_url_.c_str()); // Defer to main loop to avoid race condition on component_state_ read-modify-write - this_update->defer([this_update, msg]() { this_update->status_set_error(msg.c_str()); }); + this_update->defer([this_update]() { this_update->status_set_error("Failed to fetch manifest"); }); UPDATE_RETURN; } RAMAllocator allocator; uint8_t *data = allocator.allocate(container->content_length); if (data == nullptr) { - std::string msg = str_sprintf("Failed to allocate %zu bytes for manifest", container->content_length); + ESP_LOGE(TAG, "Failed to allocate %zu bytes for manifest", container->content_length); // Defer to main loop to avoid race condition on component_state_ read-modify-write - this_update->defer([this_update, msg]() { this_update->status_set_error(msg.c_str()); }); + this_update->defer([this_update]() { this_update->status_set_error("Failed to allocate memory for manifest"); }); container->end(); UPDATE_RETURN; } @@ -121,9 +121,9 @@ void HttpRequestUpdate::update_task(void *params) { } if (!valid) { - std::string msg = str_sprintf("Failed to parse JSON from %s", this_update->source_url_.c_str()); + ESP_LOGE(TAG, "Failed to parse JSON from %s", this_update->source_url_.c_str()); // Defer to main loop to avoid race condition on component_state_ read-modify-write - this_update->defer([this_update, msg]() { this_update->status_set_error(msg.c_str()); }); + this_update->defer([this_update]() { this_update->status_set_error("Failed to parse manifest JSON"); }); UPDATE_RETURN; } From 972b7e84fe800ea9dbd6b69d6f4bcc023b89e026 Mon Sep 17 00:00:00 2001 From: Thomas Rupprecht Date: Fri, 21 Nov 2025 14:38:44 +0100 Subject: [PATCH 189/415] [tests] Fix mipi_spi test board (#12031) Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> --- tests/component_tests/mipi_spi/test_init.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/component_tests/mipi_spi/test_init.py b/tests/component_tests/mipi_spi/test_init.py index e68f6fbfba..56a52df2ab 100644 --- a/tests/component_tests/mipi_spi/test_init.py +++ b/tests/component_tests/mipi_spi/test_init.py @@ -220,7 +220,7 @@ def test_esp32s3_specific_errors( set_core_config( PlatformFramework.ESP32_IDF, - platform_data={KEY_BOARD: "esp32dev", KEY_VARIANT: VARIANT_ESP32S3}, + platform_data={KEY_BOARD: "esp32-s3-devkitc-1", KEY_VARIANT: VARIANT_ESP32S3}, ) with pytest.raises(cv.Invalid, match=error_match): @@ -250,7 +250,7 @@ def test_custom_model_with_all_options( """Test custom model configuration with all available options.""" set_core_config( PlatformFramework.ESP32_IDF, - platform_data={KEY_BOARD: "esp32dev", KEY_VARIANT: VARIANT_ESP32S3}, + platform_data={KEY_BOARD: "esp32-s3-devkitc-1", KEY_VARIANT: VARIANT_ESP32S3}, ) run_schema_validation( @@ -293,7 +293,7 @@ def test_all_predefined_models( """Test all predefined display models validate successfully with appropriate defaults.""" set_core_config( PlatformFramework.ESP32_IDF, - platform_data={KEY_BOARD: "esp32dev", KEY_VARIANT: VARIANT_ESP32S3}, + platform_data={KEY_BOARD: "esp32-s3-devkitc-1", KEY_VARIANT: VARIANT_ESP32S3}, ) # Enable PSRAM which is required for some models From 7de66024ca3d4650cdb15529961774f2c9cf139b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 21 Nov 2025 10:56:12 -0600 Subject: [PATCH 190/415] dry, fix load protected on esp8266 --- esphome/core/component.cpp | 57 +++++++++++++++++++------------------- 1 file changed, 29 insertions(+), 28 deletions(-) diff --git a/esphome/core/component.cpp b/esphome/core/component.cpp index 6ccebd0771..b6a18c1d85 100644 --- a/esphome/core/component.cpp +++ b/esphome/core/component.cpp @@ -36,6 +36,9 @@ namespace { struct ComponentErrorMessage { const Component *component; const char *message; + // Track if message is flash pointer (needs LOG_STR_ARG) or RAM pointer + // Remove before 2026.12.0 when deprecated const char* API is removed + bool is_flash_ptr; }; struct ComponentPriorityOverride { @@ -49,6 +52,25 @@ std::unique_ptr> component_error_messages; // Setup priority overrides - freed after setup completes // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) std::unique_ptr> setup_priority_overrides; + +// Helper to store error messages - reduces duplication between deprecated and new API +// Remove before 2026.12.0 when deprecated const char* API is removed +void store_component_error_message(const Component *component, const char *message, bool is_flash_ptr) { + // Lazy allocate the error messages vector if needed + if (!component_error_messages) { + component_error_messages = std::make_unique>(); + } + // Check if this component already has an error message + for (auto &entry : *component_error_messages) { + if (entry.component == component) { + entry.message = message; + entry.is_flash_ptr = is_flash_ptr; + return; + } + } + // Add new error message + component_error_messages->emplace_back(ComponentErrorMessage{component, message, is_flash_ptr}); +} } // namespace namespace setup_priority { @@ -143,16 +165,20 @@ void Component::call_dump_config() { if (this->is_failed()) { // Look up error message from global vector const char *error_msg = nullptr; + bool is_flash_ptr = false; if (component_error_messages) { for (const auto &entry : *component_error_messages) { if (entry.component == this) { error_msg = entry.message; + is_flash_ptr = entry.is_flash_ptr; break; } } } + // Log with appropriate format based on pointer type ESP_LOGE(TAG, " %s is marked FAILED: %s", LOG_STR_ARG(this->get_component_log_str()), - error_msg ? error_msg : LOG_STR_LITERAL("unspecified")); + error_msg ? (is_flash_ptr ? LOG_STR_ARG((const LogString *) error_msg) : error_msg) + : LOG_STR_LITERAL("unspecified")); } } @@ -315,19 +341,7 @@ void Component::status_set_error(const char *message) { ESP_LOGE(TAG, "%s set Error flag: %s", LOG_STR_ARG(this->get_component_log_str()), message ? message : LOG_STR_LITERAL("unspecified")); if (message != nullptr) { - // Lazy allocate the error messages vector if needed - if (!component_error_messages) { - component_error_messages = std::make_unique>(); - } - // Check if this component already has an error message - for (auto &entry : *component_error_messages) { - if (entry.component == this) { - entry.message = message; - return; - } - } - // Add new error message - component_error_messages->emplace_back(ComponentErrorMessage{this, message}); + store_component_error_message(this, message, false); } } void Component::status_set_error(const LogString *message) { @@ -338,21 +352,8 @@ void Component::status_set_error(const LogString *message) { ESP_LOGE(TAG, "%s set Error flag: %s", LOG_STR_ARG(this->get_component_log_str()), message ? LOG_STR_ARG(message) : LOG_STR_LITERAL("unspecified")); if (message != nullptr) { - // Lazy allocate the error messages vector if needed - if (!component_error_messages) { - component_error_messages = std::make_unique>(); - } // Store the LogString pointer directly (safe because LogString is always in flash/static memory) - const char *msg_ptr = LOG_STR_ARG(message); - // Check if this component already has an error message - for (auto &entry : *component_error_messages) { - if (entry.component == this) { - entry.message = msg_ptr; - return; - } - } - // Add new error message - component_error_messages->emplace_back(ComponentErrorMessage{this, msg_ptr}); + store_component_error_message(this, LOG_STR_ARG(message), true); } } void Component::status_clear_warning() { From 1fe1a3d2c85dfde6e955ae4d656f2122440813e7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 21 Nov 2025 11:00:23 -0600 Subject: [PATCH 191/415] fix date --- esphome/core/component.cpp | 4 ++-- esphome/core/component.h | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/esphome/core/component.cpp b/esphome/core/component.cpp index b6a18c1d85..aa487d8ef5 100644 --- a/esphome/core/component.cpp +++ b/esphome/core/component.cpp @@ -37,7 +37,7 @@ struct ComponentErrorMessage { const Component *component; const char *message; // Track if message is flash pointer (needs LOG_STR_ARG) or RAM pointer - // Remove before 2026.12.0 when deprecated const char* API is removed + // Remove before 2026.6.0 when deprecated const char* API is removed bool is_flash_ptr; }; @@ -54,7 +54,7 @@ std::unique_ptr> component_error_messages; std::unique_ptr> setup_priority_overrides; // Helper to store error messages - reduces duplication between deprecated and new API -// Remove before 2026.12.0 when deprecated const char* API is removed +// Remove before 2026.6.0 when deprecated const char* API is removed void store_component_error_message(const Component *component, const char *message, bool is_flash_ptr) { // Lazy allocate the error messages vector if needed if (!component_error_messages) { diff --git a/esphome/core/component.h b/esphome/core/component.h index e1153fdf53..3415114992 100644 --- a/esphome/core/component.h +++ b/esphome/core/component.h @@ -217,8 +217,8 @@ class Component { void status_set_warning(const char *message = nullptr); void status_set_warning(const LogString *message); - // Remove before 2026.12.0 - ESPDEPRECATED("Use status_set_error(LOG_STR(message)) instead. Removed in 2026.12.0", "2025.6.0") + // Remove before 2026.6.0 + ESPDEPRECATED("Use status_set_error(LOG_STR(message)) instead. Will stop working in 2026.6.0", "2025.12.0") void status_set_error(const char *message = nullptr); void status_set_error(const LogString *message); From 782aee92a77a2a3d991c97f671c259b1b961883e Mon Sep 17 00:00:00 2001 From: Marko Draca Date: Fri, 21 Nov 2025 20:50:07 +0100 Subject: [PATCH 192/415] [mcp3204] differential mode support (#7436) Co-authored-by: marko Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> --- esphome/components/mcp3204/mcp3204.cpp | 21 ++++++++++++------- esphome/components/mcp3204/mcp3204.h | 2 +- esphome/components/mcp3204/sensor/__init__.py | 3 +++ .../mcp3204/sensor/mcp3204_sensor.cpp | 5 ++--- .../mcp3204/sensor/mcp3204_sensor.h | 3 ++- tests/components/mcp3204/common.yaml | 16 +++++++++++++- 6 files changed, 36 insertions(+), 14 deletions(-) diff --git a/esphome/components/mcp3204/mcp3204.cpp b/esphome/components/mcp3204/mcp3204.cpp index 4bb0cbed76..f0dd171a14 100644 --- a/esphome/components/mcp3204/mcp3204.cpp +++ b/esphome/components/mcp3204/mcp3204.cpp @@ -16,16 +16,21 @@ void MCP3204::dump_config() { ESP_LOGCONFIG(TAG, " Reference Voltage: %.2fV", this->reference_voltage_); } -float MCP3204::read_data(uint8_t pin) { - uint8_t adc_primary_config = 0b00000110 | (pin >> 2); - uint8_t adc_secondary_config = pin << 6; +float MCP3204::read_data(uint8_t pin, bool differential) { + uint8_t command, b0, b1; + + command = (1 << 6) | // start bit + ((differential ? 0 : 1) << 5) | // single or differential bit + ((pin & 0x07) << 2); // pin + this->enable(); - this->transfer_byte(adc_primary_config); - uint8_t adc_primary_byte = this->transfer_byte(adc_secondary_config); - uint8_t adc_secondary_byte = this->transfer_byte(0x00); + this->transfer_byte(command); + b0 = this->transfer_byte(0x00); + b1 = this->transfer_byte(0x00); this->disable(); - uint16_t digital_value = (adc_primary_byte << 8 | adc_secondary_byte) & 0b111111111111; - return float(digital_value) / 4096.000 * this->reference_voltage_; + + uint16_t digital_value = encode_uint16(b0, b1) >> 4; + return float(digital_value) / 4096.000 * this->reference_voltage_; // in V } } // namespace mcp3204 diff --git a/esphome/components/mcp3204/mcp3204.h b/esphome/components/mcp3204/mcp3204.h index 27261aa373..6287263a2a 100644 --- a/esphome/components/mcp3204/mcp3204.h +++ b/esphome/components/mcp3204/mcp3204.h @@ -18,7 +18,7 @@ class MCP3204 : public Component, void setup() override; void dump_config() override; float get_setup_priority() const override; - float read_data(uint8_t pin); + float read_data(uint8_t pin, bool differential); protected: float reference_voltage_; diff --git a/esphome/components/mcp3204/sensor/__init__.py b/esphome/components/mcp3204/sensor/__init__.py index a4b177cbcf..5f9aa9fdb6 100644 --- a/esphome/components/mcp3204/sensor/__init__.py +++ b/esphome/components/mcp3204/sensor/__init__.py @@ -13,6 +13,7 @@ MCP3204Sensor = mcp3204_ns.class_( "MCP3204Sensor", sensor.Sensor, cg.PollingComponent, voltage_sampler.VoltageSampler ) CONF_MCP3204_ID = "mcp3204_id" +CONF_DIFF_MODE = "diff_mode" CONFIG_SCHEMA = ( sensor.sensor_schema(MCP3204Sensor) @@ -20,6 +21,7 @@ CONFIG_SCHEMA = ( { cv.GenerateID(CONF_MCP3204_ID): cv.use_id(MCP3204), cv.Required(CONF_NUMBER): cv.int_range(min=0, max=7), + cv.Optional(CONF_DIFF_MODE, default=False): cv.boolean, } ) .extend(cv.polling_component_schema("60s")) @@ -30,6 +32,7 @@ async def to_code(config): var = cg.new_Pvariable( config[CONF_ID], config[CONF_NUMBER], + config[CONF_DIFF_MODE], ) await cg.register_parented(var, config[CONF_MCP3204_ID]) await cg.register_component(var, config) diff --git a/esphome/components/mcp3204/sensor/mcp3204_sensor.cpp b/esphome/components/mcp3204/sensor/mcp3204_sensor.cpp index ce0fd25462..4c4abef4a7 100644 --- a/esphome/components/mcp3204/sensor/mcp3204_sensor.cpp +++ b/esphome/components/mcp3204/sensor/mcp3204_sensor.cpp @@ -7,16 +7,15 @@ namespace mcp3204 { static const char *const TAG = "mcp3204.sensor"; -MCP3204Sensor::MCP3204Sensor(uint8_t pin) : pin_(pin) {} - float MCP3204Sensor::get_setup_priority() const { return setup_priority::DATA; } void MCP3204Sensor::dump_config() { LOG_SENSOR("", "MCP3204 Sensor", this); ESP_LOGCONFIG(TAG, " Pin: %u", this->pin_); + ESP_LOGCONFIG(TAG, " Differential Mode: %s", YESNO(this->differential_mode_)); LOG_UPDATE_INTERVAL(this); } -float MCP3204Sensor::sample() { return this->parent_->read_data(this->pin_); } +float MCP3204Sensor::sample() { return this->parent_->read_data(this->pin_, this->differential_mode_); } void MCP3204Sensor::update() { this->publish_state(this->sample()); } } // namespace mcp3204 diff --git a/esphome/components/mcp3204/sensor/mcp3204_sensor.h b/esphome/components/mcp3204/sensor/mcp3204_sensor.h index 21c45590ab..5665b80b98 100644 --- a/esphome/components/mcp3204/sensor/mcp3204_sensor.h +++ b/esphome/components/mcp3204/sensor/mcp3204_sensor.h @@ -15,7 +15,7 @@ class MCP3204Sensor : public PollingComponent, public sensor::Sensor, public voltage_sampler::VoltageSampler { public: - MCP3204Sensor(uint8_t pin); + MCP3204Sensor(uint8_t pin, bool differential_mode) : pin_(pin), differential_mode_(differential_mode) {} void update() override; void dump_config() override; @@ -24,6 +24,7 @@ class MCP3204Sensor : public PollingComponent, protected: uint8_t pin_; + bool differential_mode_; }; } // namespace mcp3204 diff --git a/tests/components/mcp3204/common.yaml b/tests/components/mcp3204/common.yaml index eca6ec44f4..9750f0af8e 100644 --- a/tests/components/mcp3204/common.yaml +++ b/tests/components/mcp3204/common.yaml @@ -4,7 +4,21 @@ mcp3204: sensor: - platform: mcp3204 - id: mcp3204_sensor + id: mcp3204_default_single_0 mcp3204_id: mcp3204_hub number: 0 update_interval: 5s + + - platform: mcp3204 + id: mcp3204_single_0 + mcp3204_id: mcp3204_hub + number: 0 + diff_mode: false + update_interval: 5s + + - platform: mcp3204 + id: mcp3204_diff_0_1 + mcp3204_id: mcp3204_hub + number: 0 + diff_mode: true + update_interval: 5s From 3f6f2d7d650feee79896b13903c7a26e98559d59 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ab=C3=ADlio=20Costa?= Date: Fri, 21 Nov 2025 21:28:42 +0100 Subject: [PATCH 193/415] [bm8563] Add bm8563 component (#11616) Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> --- CODEOWNERS | 1 + esphome/components/bm8563/__init__.py | 1 + esphome/components/bm8563/bm8563.cpp | 198 ++++++++++++++++++ esphome/components/bm8563/bm8563.h | 57 +++++ esphome/components/bm8563/time.py | 80 +++++++ tests/components/bm8563/common.yaml | 10 + tests/components/bm8563/test.esp32-ard.yaml | 4 + tests/components/bm8563/test.esp32-idf.yaml | 4 + tests/components/bm8563/test.esp8266-ard.yaml | 4 + tests/components/bm8563/test.rp2040-ard.yaml | 4 + 10 files changed, 363 insertions(+) create mode 100644 esphome/components/bm8563/__init__.py create mode 100644 esphome/components/bm8563/bm8563.cpp create mode 100644 esphome/components/bm8563/bm8563.h create mode 100644 esphome/components/bm8563/time.py create mode 100644 tests/components/bm8563/common.yaml create mode 100644 tests/components/bm8563/test.esp32-ard.yaml create mode 100644 tests/components/bm8563/test.esp32-idf.yaml create mode 100644 tests/components/bm8563/test.esp8266-ard.yaml create mode 100644 tests/components/bm8563/test.rp2040-ard.yaml diff --git a/CODEOWNERS b/CODEOWNERS index c3d8f4350f..d6ec7b882e 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -72,6 +72,7 @@ esphome/components/bl0942/* @dbuezas @dwmw2 esphome/components/ble_client/* @buxtronix @clydebarrow esphome/components/ble_nus/* @tomaszduda23 esphome/components/bluetooth_proxy/* @bdraco @jesserockz +esphome/components/bm8563/* @abmantis esphome/components/bme280_base/* @esphome/core esphome/components/bme280_spi/* @apbodrov esphome/components/bme680_bsec/* @trvrnrth diff --git a/esphome/components/bm8563/__init__.py b/esphome/components/bm8563/__init__.py new file mode 100644 index 0000000000..20254a8b00 --- /dev/null +++ b/esphome/components/bm8563/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ["@abmantis"] diff --git a/esphome/components/bm8563/bm8563.cpp b/esphome/components/bm8563/bm8563.cpp new file mode 100644 index 0000000000..07831485c1 --- /dev/null +++ b/esphome/components/bm8563/bm8563.cpp @@ -0,0 +1,198 @@ +#include "bm8563.h" +#include "esphome/core/log.h" + +namespace esphome::bm8563 { + +static const char *const TAG = "bm8563"; + +static constexpr uint8_t CONTROL_STATUS_1_REG = 0x00; +static constexpr uint8_t CONTROL_STATUS_2_REG = 0x01; +static constexpr uint8_t TIME_FIRST_REG = 0x02; // Time uses reg 2, 3, 4 +static constexpr uint8_t DATE_FIRST_REG = 0x05; // Date uses reg 5, 6, 7, 8 +static constexpr uint8_t TIMER_CONTROL_REG = 0x0E; +static constexpr uint8_t TIMER_VALUE_REG = 0x0F; +static constexpr uint8_t CLOCK_1_HZ = 0x82; +static constexpr uint8_t CLOCK_1_60_HZ = 0x83; +// Maximum duration: 255 minutes (at 1/60 Hz) = 15300 seconds +static constexpr uint32_t MAX_TIMER_DURATION_S = 255 * 60; + +void BM8563::setup() { + if (!this->write_byte_16(CONTROL_STATUS_1_REG, 0)) { + this->mark_failed(); + return; + } +} + +void BM8563::update() { this->read_time(); } + +void BM8563::dump_config() { + ESP_LOGCONFIG(TAG, "BM8563:"); + LOG_I2C_DEVICE(this); + if (this->is_failed()) { + ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL); + } +} + +void BM8563::start_timer(uint32_t duration_s) { + this->clear_irq_(); + this->set_timer_irq_(duration_s); +} + +void BM8563::write_time() { + auto now = time::RealTimeClock::utcnow(); + if (!now.is_valid()) { + ESP_LOGE(TAG, "Invalid system time, not syncing to RTC."); + return; + } + + ESP_LOGD(TAG, "Writing time: %i-%i-%i %i, %i:%i:%i", now.year, now.month, now.day_of_month, now.day_of_week, now.hour, + now.minute, now.second); + + this->set_time_(now); + this->set_date_(now); +} + +void BM8563::read_time() { + ESPTime rtc_time; + this->get_time_(rtc_time); + this->get_date_(rtc_time); + rtc_time.day_of_year = 1; // unused by recalc_timestamp_utc, but needs to be valid + ESP_LOGD(TAG, "Read time: %i-%i-%i %i, %i:%i:%i", rtc_time.year, rtc_time.month, rtc_time.day_of_month, + rtc_time.day_of_week, rtc_time.hour, rtc_time.minute, rtc_time.second); + + rtc_time.recalc_timestamp_utc(false); + if (!rtc_time.is_valid()) { + ESP_LOGE(TAG, "Invalid RTC time, not syncing to system clock."); + return; + } + time::RealTimeClock::synchronize_epoch_(rtc_time.timestamp); +} + +uint8_t BM8563::bcd2_to_byte_(uint8_t value) { + const uint8_t tmp = ((value & 0xF0) >> 0x4) * 10; + return tmp + (value & 0x0F); +} + +uint8_t BM8563::byte_to_bcd2_(uint8_t value) { + const uint8_t bcdhigh = value / 10; + value -= bcdhigh * 10; + return (bcdhigh << 4) | value; +} + +void BM8563::get_time_(ESPTime &time) { + uint8_t buf[3] = {0}; + this->read_register(TIME_FIRST_REG, buf, 3); + + time.second = this->bcd2_to_byte_(buf[0] & 0x7f); + time.minute = this->bcd2_to_byte_(buf[1] & 0x7f); + time.hour = this->bcd2_to_byte_(buf[2] & 0x3f); +} + +void BM8563::set_time_(const ESPTime &time) { + uint8_t buf[3] = {this->byte_to_bcd2_(time.second), this->byte_to_bcd2_(time.minute), this->byte_to_bcd2_(time.hour)}; + this->write_register_(TIME_FIRST_REG, buf, 3); +} + +void BM8563::get_date_(ESPTime &time) { + uint8_t buf[4] = {0}; + this->read_register(DATE_FIRST_REG, buf, sizeof(buf)); + + time.day_of_month = this->bcd2_to_byte_(buf[0] & 0x3f); + time.day_of_week = this->bcd2_to_byte_(buf[1] & 0x07); + time.month = this->bcd2_to_byte_(buf[2] & 0x1f); + + uint8_t year_byte = this->bcd2_to_byte_(buf[3] & 0xff); + + if (buf[2] & 0x80) { + time.year = 1900 + year_byte; + } else { + time.year = 2000 + year_byte; + } +} + +void BM8563::set_date_(const ESPTime &time) { + uint8_t buf[4] = { + this->byte_to_bcd2_(time.day_of_month), + this->byte_to_bcd2_(time.day_of_week), + this->byte_to_bcd2_(time.month), + this->byte_to_bcd2_(time.year % 100), + }; + + if (time.year < 2000) { + buf[2] = buf[2] | 0x80; + } + + this->write_register_(DATE_FIRST_REG, buf, 4); +} + +void BM8563::write_byte_(uint8_t reg, uint8_t value) { + if (!this->write_byte(reg, value)) { + ESP_LOGE(TAG, "Failed to write byte 0x%02X with value 0x%02X", reg, value); + } +} + +void BM8563::write_register_(uint8_t reg, const uint8_t *data, size_t len) { + if (auto error = this->write_register(reg, data, len); error != i2c::ErrorCode::NO_ERROR) { + ESP_LOGE(TAG, "Failed to write register 0x%02X with %zu bytes", reg, len); + } +} + +optional BM8563::read_register_(uint8_t reg) { + uint8_t data; + if (auto error = this->read_register(reg, &data, 1); error != i2c::ErrorCode::NO_ERROR) { + ESP_LOGE(TAG, "Failed to read register 0x%02X", reg); + return {}; + } + return data; +} + +void BM8563::set_timer_irq_(uint32_t duration_s) { + ESP_LOGI(TAG, "Timer Duration: %u s", duration_s); + + if (duration_s > MAX_TIMER_DURATION_S) { + ESP_LOGW(TAG, "Timer duration %u s exceeds maximum %u s", duration_s, MAX_TIMER_DURATION_S); + return; + } + + if (duration_s > 255) { + uint8_t duration_minutes = duration_s / 60; + this->write_byte_(TIMER_VALUE_REG, duration_minutes); + this->write_byte_(TIMER_CONTROL_REG, CLOCK_1_60_HZ); + } else { + this->write_byte_(TIMER_VALUE_REG, duration_s); + this->write_byte_(TIMER_CONTROL_REG, CLOCK_1_HZ); + } + + auto maybe_ctrl_status_2 = this->read_register_(CONTROL_STATUS_2_REG); + if (!maybe_ctrl_status_2.has_value()) { + ESP_LOGE(TAG, "Failed to read CONTROL_STATUS_2_REG"); + return; + } + uint8_t ctrl_status_2_reg_value = maybe_ctrl_status_2.value(); + ctrl_status_2_reg_value |= (1 << 0); + ctrl_status_2_reg_value &= ~(1 << 7); + this->write_byte_(CONTROL_STATUS_2_REG, ctrl_status_2_reg_value); +} + +void BM8563::clear_irq_() { + auto maybe_data = this->read_register_(CONTROL_STATUS_2_REG); + if (!maybe_data.has_value()) { + ESP_LOGE(TAG, "Failed to read CONTROL_STATUS_2_REG"); + return; + } + uint8_t data = maybe_data.value(); + this->write_byte_(CONTROL_STATUS_2_REG, data & 0xf3); +} + +void BM8563::disable_irq_() { + this->clear_irq_(); + auto maybe_data = this->read_register_(CONTROL_STATUS_2_REG); + if (!maybe_data.has_value()) { + ESP_LOGE(TAG, "Failed to read CONTROL_STATUS_2_REG"); + return; + } + uint8_t data = maybe_data.value(); + this->write_byte_(CONTROL_STATUS_2_REG, data & 0xfc); +} + +} // namespace esphome::bm8563 diff --git a/esphome/components/bm8563/bm8563.h b/esphome/components/bm8563/bm8563.h new file mode 100644 index 0000000000..eda2d1b3c0 --- /dev/null +++ b/esphome/components/bm8563/bm8563.h @@ -0,0 +1,57 @@ +#pragma once + +#include "esphome/components/i2c/i2c.h" +#include "esphome/components/time/real_time_clock.h" + +namespace esphome::bm8563 { + +class BM8563 : public time::RealTimeClock, public i2c::I2CDevice { + public: + void setup() override; + void update() override; + void dump_config() override; + + void write_time(); + void read_time(); + void start_timer(uint32_t duration_s); + + private: + void get_time_(ESPTime &time); + void get_date_(ESPTime &time); + + void set_time_(const ESPTime &time); + void set_date_(const ESPTime &time); + + void set_timer_irq_(uint32_t duration_s); + void clear_irq_(); + void disable_irq_(); + + void write_byte_(uint8_t reg, uint8_t value); + void write_register_(uint8_t reg, const uint8_t *data, size_t len); + optional read_register_(uint8_t reg); + + uint8_t bcd2_to_byte_(uint8_t value); + uint8_t byte_to_bcd2_(uint8_t value); +}; + +template class WriteAction : public Action, public Parented { + public: + void play(const Ts &...x) override { this->parent_->write_time(); } +}; + +template class ReadAction : public Action, public Parented { + public: + void play(const Ts &...x) override { this->parent_->read_time(); } +}; + +template class TimerAction : public Action, public Parented { + public: + TEMPLATABLE_VALUE(uint32_t, duration) + + void play(const Ts &...x) override { + auto duration = this->duration_.value(x...); + this->parent_->start_timer(duration); + } +}; + +} // namespace esphome::bm8563 diff --git a/esphome/components/bm8563/time.py b/esphome/components/bm8563/time.py new file mode 100644 index 0000000000..2785315af2 --- /dev/null +++ b/esphome/components/bm8563/time.py @@ -0,0 +1,80 @@ +from esphome import automation +import esphome.codegen as cg +from esphome.components import i2c, time +import esphome.config_validation as cv +from esphome.const import CONF_DURATION, CONF_ID + +DEPENDENCIES = ["i2c"] + +I2C_ADDR = 0x51 + +bm8563_ns = cg.esphome_ns.namespace("bm8563") +BM8563 = bm8563_ns.class_("BM8563", time.RealTimeClock, i2c.I2CDevice) +WriteAction = bm8563_ns.class_("WriteAction", automation.Action) +ReadAction = bm8563_ns.class_("ReadAction", automation.Action) +TimerAction = bm8563_ns.class_("TimerAction", automation.Action) + +CONFIG_SCHEMA = ( + time.TIME_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(BM8563), + } + ) + .extend(cv.COMPONENT_SCHEMA) + .extend(i2c.i2c_device_schema(I2C_ADDR)) +) + + +@automation.register_action( + "bm8563.write_time", + WriteAction, + automation.maybe_simple_id( + { + cv.GenerateID(): cv.use_id(BM8563), + } + ), +) +async def bm8563_write_time_to_code(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + await cg.register_parented(var, config[CONF_ID]) + return var + + +@automation.register_action( + "bm8563.start_timer", + TimerAction, + cv.Schema( + { + cv.GenerateID(): cv.use_id(BM8563), + cv.Required(CONF_DURATION): cv.templatable(cv.positive_time_period_seconds), + } + ), +) +async def bm8563_start_timer_to_code(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + await cg.register_parented(var, config[CONF_ID]) + template_ = await cg.templatable(config[CONF_DURATION], args, cg.uint32) + cg.add(var.set_duration(template_)) + return var + + +@automation.register_action( + "bm8563.read_time", + ReadAction, + automation.maybe_simple_id( + { + cv.GenerateID(): cv.use_id(BM8563), + } + ), +) +async def bm8563_read_time_to_code(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + await cg.register_parented(var, config[CONF_ID]) + return var + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await i2c.register_i2c_device(var, config) + await time.register_time(var, config) diff --git a/tests/components/bm8563/common.yaml b/tests/components/bm8563/common.yaml new file mode 100644 index 0000000000..ec3fdd1518 --- /dev/null +++ b/tests/components/bm8563/common.yaml @@ -0,0 +1,10 @@ +esphome: + on_boot: + - bm8563.read_time + - bm8563.write_time + - bm8563.start_timer: + duration: 300s + +time: + - platform: bm8563 + i2c_id: i2c_bus diff --git a/tests/components/bm8563/test.esp32-ard.yaml b/tests/components/bm8563/test.esp32-ard.yaml new file mode 100644 index 0000000000..7c503b0ccb --- /dev/null +++ b/tests/components/bm8563/test.esp32-ard.yaml @@ -0,0 +1,4 @@ +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-ard.yaml + +<<: !include common.yaml diff --git a/tests/components/bm8563/test.esp32-idf.yaml b/tests/components/bm8563/test.esp32-idf.yaml new file mode 100644 index 0000000000..b47e39c389 --- /dev/null +++ b/tests/components/bm8563/test.esp32-idf.yaml @@ -0,0 +1,4 @@ +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml + +<<: !include common.yaml diff --git a/tests/components/bm8563/test.esp8266-ard.yaml b/tests/components/bm8563/test.esp8266-ard.yaml new file mode 100644 index 0000000000..4a98b9388a --- /dev/null +++ b/tests/components/bm8563/test.esp8266-ard.yaml @@ -0,0 +1,4 @@ +packages: + i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml + +<<: !include common.yaml diff --git a/tests/components/bm8563/test.rp2040-ard.yaml b/tests/components/bm8563/test.rp2040-ard.yaml new file mode 100644 index 0000000000..319a7c71a6 --- /dev/null +++ b/tests/components/bm8563/test.rp2040-ard.yaml @@ -0,0 +1,4 @@ +packages: + i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml + +<<: !include common.yaml From 66d6c85aa76ab2bbbefa2ac3ace15eb08c064c58 Mon Sep 17 00:00:00 2001 From: kbx81 Date: Sun, 23 Nov 2025 02:05:41 -0600 Subject: [PATCH 194/415] preen --- esphome/components/wifi/automation.h | 10 +++++----- esphome/components/wifi_info/wifi_info_text_sensor.h | 2 -- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/esphome/components/wifi/automation.h b/esphome/components/wifi/automation.h index 0651eafca2..4c7545a445 100644 --- a/esphome/components/wifi/automation.h +++ b/esphome/components/wifi/automation.h @@ -9,22 +9,22 @@ namespace wifi { template class WiFiConnectedCondition : public Condition { public: - bool check(Ts... x) override { return global_wifi_component->is_connected(); } + bool check(const Ts &...x) override { return global_wifi_component->is_connected(); } }; template class WiFiEnabledCondition : public Condition { public: - bool check(Ts... x) override { return !global_wifi_component->is_disabled(); } + bool check(const Ts &...x) override { return !global_wifi_component->is_disabled(); } }; template class WiFiEnableAction : public Action { public: - void play(Ts... x) override { global_wifi_component->enable(); } + void play(const Ts &...x) override { global_wifi_component->enable(); } }; template class WiFiDisableAction : public Action { public: - void play(Ts... x) override { global_wifi_component->disable(); } + void play(const Ts &...x) override { global_wifi_component->disable(); } }; template class WiFiConfigureAction : public Action, public Component { @@ -34,7 +34,7 @@ template class WiFiConfigureAction : public Action, publi TEMPLATABLE_VALUE(bool, save) TEMPLATABLE_VALUE(uint32_t, connection_timeout) - void play(Ts... x) override { + void play(const Ts &...x) override { auto ssid = this->ssid_.value(x...); auto password = this->password_.value(x...); // Avoid multiple calls diff --git a/esphome/components/wifi_info/wifi_info_text_sensor.h b/esphome/components/wifi_info/wifi_info_text_sensor.h index 5358e3c6f3..65bf3da103 100644 --- a/esphome/components/wifi_info/wifi_info_text_sensor.h +++ b/esphome/components/wifi_info/wifi_info_text_sensor.h @@ -10,8 +10,6 @@ namespace esphome { namespace wifi_info { -static constexpr size_t MAX_STATE_LENGTH = 255; - class IPAddressWiFiInfo : public Component, public text_sensor::TextSensor { public: void setup() override; From a5751b294f06448375878dcb620ac6c4ec054893 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Mon, 24 Nov 2025 08:13:23 +1300 Subject: [PATCH 195/415] [api] Rename `USE_API_SERVICES` to `USE_API_USER_DEFINED_ACTIONS` (#12029) --- esphome/components/api/__init__.py | 4 ++-- esphome/components/api/api.proto | 8 ++++---- esphome/components/api/api_connection.cpp | 2 +- esphome/components/api/api_connection.h | 2 +- esphome/components/api/api_pb2.cpp | 2 +- esphome/components/api/api_pb2.h | 4 ++-- esphome/components/api/api_pb2_dump.cpp | 4 ++-- esphome/components/api/api_pb2_service.cpp | 4 ++-- esphome/components/api/api_pb2_service.h | 6 +++--- esphome/components/api/api_server.h | 8 ++++---- esphome/components/api/custom_api_device.h | 10 +++++----- esphome/components/api/list_entities.cpp | 2 +- esphome/components/api/list_entities.h | 2 +- esphome/components/api/user_services.h | 4 ++-- esphome/core/component_iterator.cpp | 6 +++--- esphome/core/component_iterator.h | 6 +++--- esphome/core/defines.h | 2 +- 17 files changed, 38 insertions(+), 38 deletions(-) diff --git a/esphome/components/api/__init__.py b/esphome/components/api/__init__.py index a9286c531f..7f84f2f247 100644 --- a/esphome/components/api/__init__.py +++ b/esphome/components/api/__init__.py @@ -260,9 +260,9 @@ async def to_code(config): cg.add(var.set_max_connections(config[CONF_MAX_CONNECTIONS])) cg.add_define("API_MAX_SEND_QUEUE", config[CONF_MAX_SEND_QUEUE]) - # Set USE_API_SERVICES if any services are enabled + # Set USE_API_USER_DEFINED_ACTIONS if any services are enabled if config.get(CONF_ACTIONS) or config[CONF_CUSTOM_SERVICES]: - cg.add_define("USE_API_SERVICES") + cg.add_define("USE_API_USER_DEFINED_ACTIONS") # Set USE_API_CUSTOM_SERVICES if external components need dynamic service registration if config[CONF_CUSTOM_SERVICES]: diff --git a/esphome/components/api/api.proto b/esphome/components/api/api.proto index e115e4630d..26d1fa6876 100644 --- a/esphome/components/api/api.proto +++ b/esphome/components/api/api.proto @@ -855,21 +855,21 @@ enum ServiceArgType { SERVICE_ARG_TYPE_STRING_ARRAY = 7; } message ListEntitiesServicesArgument { - option (ifdef) = "USE_API_SERVICES"; + option (ifdef) = "USE_API_USER_DEFINED_ACTIONS"; string name = 1; ServiceArgType type = 2; } message ListEntitiesServicesResponse { option (id) = 41; option (source) = SOURCE_SERVER; - option (ifdef) = "USE_API_SERVICES"; + option (ifdef) = "USE_API_USER_DEFINED_ACTIONS"; string name = 1; fixed32 key = 2; repeated ListEntitiesServicesArgument args = 3 [(fixed_vector) = true]; } message ExecuteServiceArgument { - option (ifdef) = "USE_API_SERVICES"; + option (ifdef) = "USE_API_USER_DEFINED_ACTIONS"; bool bool_ = 1; int32 legacy_int = 2; float float_ = 3; @@ -885,7 +885,7 @@ message ExecuteServiceRequest { option (id) = 42; option (source) = SOURCE_CLIENT; option (no_delay) = true; - option (ifdef) = "USE_API_SERVICES"; + option (ifdef) = "USE_API_USER_DEFINED_ACTIONS"; fixed32 key = 1; repeated ExecuteServiceArgument args = 2 [(fixed_vector) = true]; diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 4acd2fc15c..c60680ae43 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -1541,7 +1541,7 @@ void APIConnection::on_home_assistant_state_response(const HomeAssistantStateRes } } #endif -#ifdef USE_API_SERVICES +#ifdef USE_API_USER_DEFINED_ACTIONS void APIConnection::execute_service(const ExecuteServiceRequest &msg) { bool found = false; for (auto *service : this->parent_->get_user_services()) { diff --git a/esphome/components/api/api_connection.h b/esphome/components/api/api_connection.h index 6cfd108927..af3a19909f 100644 --- a/esphome/components/api/api_connection.h +++ b/esphome/components/api/api_connection.h @@ -221,7 +221,7 @@ class APIConnection final : public APIServerConnection { #ifdef USE_API_HOMEASSISTANT_STATES void subscribe_home_assistant_states(const SubscribeHomeAssistantStatesRequest &msg) override; #endif -#ifdef USE_API_SERVICES +#ifdef USE_API_USER_DEFINED_ACTIONS void execute_service(const ExecuteServiceRequest &msg) override; #endif #ifdef USE_API_NOISE diff --git a/esphome/components/api/api_pb2.cpp b/esphome/components/api/api_pb2.cpp index 0a073fb662..d52135a566 100644 --- a/esphome/components/api/api_pb2.cpp +++ b/esphome/components/api/api_pb2.cpp @@ -995,7 +995,7 @@ bool GetTimeResponse::decode_32bit(uint32_t field_id, Proto32Bit value) { } return true; } -#ifdef USE_API_SERVICES +#ifdef USE_API_USER_DEFINED_ACTIONS void ListEntitiesServicesArgument::encode(ProtoWriteBuffer buffer) const { buffer.encode_string(1, this->name_ref_); buffer.encode_uint32(2, static_cast(this->type)); diff --git a/esphome/components/api/api_pb2.h b/esphome/components/api/api_pb2.h index 358049026e..b19e92d4ff 100644 --- a/esphome/components/api/api_pb2.h +++ b/esphome/components/api/api_pb2.h @@ -63,7 +63,7 @@ enum LogLevel : uint32_t { LOG_LEVEL_VERBOSE = 6, LOG_LEVEL_VERY_VERBOSE = 7, }; -#ifdef USE_API_SERVICES +#ifdef USE_API_USER_DEFINED_ACTIONS enum ServiceArgType : uint32_t { SERVICE_ARG_TYPE_BOOL = 0, SERVICE_ARG_TYPE_INT = 1, @@ -1239,7 +1239,7 @@ class GetTimeResponse final : public ProtoDecodableMessage { bool decode_32bit(uint32_t field_id, Proto32Bit value) override; bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; }; -#ifdef USE_API_SERVICES +#ifdef USE_API_USER_DEFINED_ACTIONS class ListEntitiesServicesArgument final : public ProtoMessage { public: StringRef name_ref_{}; diff --git a/esphome/components/api/api_pb2_dump.cpp b/esphome/components/api/api_pb2_dump.cpp index 127ef44cd8..ea752ba3ba 100644 --- a/esphome/components/api/api_pb2_dump.cpp +++ b/esphome/components/api/api_pb2_dump.cpp @@ -206,7 +206,7 @@ template<> const char *proto_enum_to_string(enums::LogLevel val return "UNKNOWN"; } } -#ifdef USE_API_SERVICES +#ifdef USE_API_USER_DEFINED_ACTIONS template<> const char *proto_enum_to_string(enums::ServiceArgType value) { switch (value) { case enums::SERVICE_ARG_TYPE_BOOL: @@ -1177,7 +1177,7 @@ void GetTimeResponse::dump_to(std::string &out) const { out.append(format_hex_pretty(this->timezone, this->timezone_len)); out.append("\n"); } -#ifdef USE_API_SERVICES +#ifdef USE_API_USER_DEFINED_ACTIONS void ListEntitiesServicesArgument::dump_to(std::string &out) const { MessageDumpHelper helper(out, "ListEntitiesServicesArgument"); dump_field(out, "name", this->name_ref_); diff --git a/esphome/components/api/api_pb2_service.cpp b/esphome/components/api/api_pb2_service.cpp index 9d227af0a3..3d28a137c8 100644 --- a/esphome/components/api/api_pb2_service.cpp +++ b/esphome/components/api/api_pb2_service.cpp @@ -193,7 +193,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, break; } #endif -#ifdef USE_API_SERVICES +#ifdef USE_API_USER_DEFINED_ACTIONS case ExecuteServiceRequest::MESSAGE_TYPE: { ExecuteServiceRequest msg; msg.decode(msg_data, msg_size); @@ -670,7 +670,7 @@ void APIServerConnection::on_subscribe_home_assistant_states_request(const Subsc this->subscribe_home_assistant_states(msg); } #endif -#ifdef USE_API_SERVICES +#ifdef USE_API_USER_DEFINED_ACTIONS void APIServerConnection::on_execute_service_request(const ExecuteServiceRequest &msg) { this->execute_service(msg); } #endif #ifdef USE_API_NOISE diff --git a/esphome/components/api/api_pb2_service.h b/esphome/components/api/api_pb2_service.h index 549b00ee6a..827b89e23c 100644 --- a/esphome/components/api/api_pb2_service.h +++ b/esphome/components/api/api_pb2_service.h @@ -79,7 +79,7 @@ class APIServerConnectionBase : public ProtoService { virtual void on_get_time_response(const GetTimeResponse &value){}; -#ifdef USE_API_SERVICES +#ifdef USE_API_USER_DEFINED_ACTIONS virtual void on_execute_service_request(const ExecuteServiceRequest &value){}; #endif @@ -239,7 +239,7 @@ class APIServerConnection : public APIServerConnectionBase { #ifdef USE_API_HOMEASSISTANT_STATES virtual void subscribe_home_assistant_states(const SubscribeHomeAssistantStatesRequest &msg) = 0; #endif -#ifdef USE_API_SERVICES +#ifdef USE_API_USER_DEFINED_ACTIONS virtual void execute_service(const ExecuteServiceRequest &msg) = 0; #endif #ifdef USE_API_NOISE @@ -368,7 +368,7 @@ class APIServerConnection : public APIServerConnectionBase { #ifdef USE_API_HOMEASSISTANT_STATES void on_subscribe_home_assistant_states_request(const SubscribeHomeAssistantStatesRequest &msg) override; #endif -#ifdef USE_API_SERVICES +#ifdef USE_API_USER_DEFINED_ACTIONS void on_execute_service_request(const ExecuteServiceRequest &msg) override; #endif #ifdef USE_API_NOISE diff --git a/esphome/components/api/api_server.h b/esphome/components/api/api_server.h index 2d58063d6c..a3a082e165 100644 --- a/esphome/components/api/api_server.h +++ b/esphome/components/api/api_server.h @@ -12,7 +12,7 @@ #include "esphome/core/log.h" #include "list_entities.h" #include "subscribe_state.h" -#ifdef USE_API_SERVICES +#ifdef USE_API_USER_DEFINED_ACTIONS #include "user_services.h" #endif @@ -124,7 +124,7 @@ class APIServer : public Component, public Controller { #endif // USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON #endif // USE_API_HOMEASSISTANT_ACTION_RESPONSES #endif // USE_API_HOMEASSISTANT_SERVICES -#ifdef USE_API_SERVICES +#ifdef USE_API_USER_DEFINED_ACTIONS void initialize_user_services(std::initializer_list services) { this->user_services_.assign(services); } @@ -166,7 +166,7 @@ class APIServer : public Component, public Controller { std::function f); const std::vector &get_state_subs() const; #endif -#ifdef USE_API_SERVICES +#ifdef USE_API_USER_DEFINED_ACTIONS const std::vector &get_user_services() const { return this->user_services_; } #endif @@ -206,7 +206,7 @@ class APIServer : public Component, public Controller { #ifdef USE_API_HOMEASSISTANT_STATES std::vector state_subs_; #endif -#ifdef USE_API_SERVICES +#ifdef USE_API_USER_DEFINED_ACTIONS std::vector user_services_; #endif #ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES diff --git a/esphome/components/api/custom_api_device.h b/esphome/components/api/custom_api_device.h index 43ea644f0c..1006d07533 100644 --- a/esphome/components/api/custom_api_device.h +++ b/esphome/components/api/custom_api_device.h @@ -3,12 +3,12 @@ #include #include "api_server.h" #ifdef USE_API -#ifdef USE_API_SERVICES +#ifdef USE_API_USER_DEFINED_ACTIONS #include "user_services.h" #endif namespace esphome::api { -#ifdef USE_API_SERVICES +#ifdef USE_API_USER_DEFINED_ACTIONS template class CustomAPIDeviceService : public UserServiceDynamic { public: CustomAPIDeviceService(const std::string &name, const std::array &arg_names, T *obj, @@ -21,7 +21,7 @@ template class CustomAPIDeviceService : public UserS T *obj_; void (T::*callback_)(Ts...); }; -#endif // USE_API_SERVICES +#endif // USE_API_USER_DEFINED_ACTIONS class CustomAPIDevice { public: @@ -49,7 +49,7 @@ class CustomAPIDevice { * @param name The name of the service to register. * @param arg_names The name of the arguments for the service, must match the arguments of the function. */ -#ifdef USE_API_SERVICES +#ifdef USE_API_USER_DEFINED_ACTIONS template void register_service(void (T::*callback)(Ts...), const std::string &name, const std::array &arg_names) { @@ -90,7 +90,7 @@ class CustomAPIDevice { * @param callback The member function to call when the service is triggered. * @param name The name of the arguments for the service, must match the arguments of the function. */ -#ifdef USE_API_SERVICES +#ifdef USE_API_USER_DEFINED_ACTIONS template void register_service(void (T::*callback)(), const std::string &name) { #ifdef USE_API_CUSTOM_SERVICES auto *service = new CustomAPIDeviceService(name, {}, (T *) this, callback); // NOLINT diff --git a/esphome/components/api/list_entities.cpp b/esphome/components/api/list_entities.cpp index da4800a45e..e18fc17801 100644 --- a/esphome/components/api/list_entities.cpp +++ b/esphome/components/api/list_entities.cpp @@ -82,7 +82,7 @@ bool ListEntitiesIterator::on_end() { return this->client_->send_list_info_done( ListEntitiesIterator::ListEntitiesIterator(APIConnection *client) : client_(client) {} -#ifdef USE_API_SERVICES +#ifdef USE_API_USER_DEFINED_ACTIONS bool ListEntitiesIterator::on_service(UserServiceDescriptor *service) { auto resp = service->encode_list_service_response(); return this->client_->send_message(resp, ListEntitiesServicesResponse::MESSAGE_TYPE); diff --git a/esphome/components/api/list_entities.h b/esphome/components/api/list_entities.h index 769d7b9b6e..4c90dbbad8 100644 --- a/esphome/components/api/list_entities.h +++ b/esphome/components/api/list_entities.h @@ -43,7 +43,7 @@ class ListEntitiesIterator : public ComponentIterator { #ifdef USE_TEXT_SENSOR bool on_text_sensor(text_sensor::TextSensor *entity) override; #endif -#ifdef USE_API_SERVICES +#ifdef USE_API_USER_DEFINED_ACTIONS bool on_service(UserServiceDescriptor *service) override; #endif #ifdef USE_CAMERA diff --git a/esphome/components/api/user_services.h b/esphome/components/api/user_services.h index 2a887fc52d..501b702e6b 100644 --- a/esphome/components/api/user_services.h +++ b/esphome/components/api/user_services.h @@ -7,7 +7,7 @@ #include "esphome/core/automation.h" #include "api_pb2.h" -#ifdef USE_API_SERVICES +#ifdef USE_API_USER_DEFINED_ACTIONS namespace esphome::api { class UserServiceDescriptor { @@ -122,4 +122,4 @@ template class UserServiceTrigger : public UserServiceBaseprocess_platform_item_(api::global_api_server->get_user_services(), &ComponentIterator::on_service); break; @@ -185,7 +185,7 @@ void ComponentIterator::advance() { bool ComponentIterator::on_end() { return true; } bool ComponentIterator::on_begin() { return true; } -#ifdef USE_API_SERVICES +#ifdef USE_API_USER_DEFINED_ACTIONS bool ComponentIterator::on_service(api::UserServiceDescriptor *service) { return true; } #endif #ifdef USE_CAMERA diff --git a/esphome/core/component_iterator.h b/esphome/core/component_iterator.h index 641d42898a..1b1bd80ac5 100644 --- a/esphome/core/component_iterator.h +++ b/esphome/core/component_iterator.h @@ -10,7 +10,7 @@ namespace esphome { -#ifdef USE_API_SERVICES +#ifdef USE_API_USER_DEFINED_ACTIONS namespace api { class UserServiceDescriptor; } // namespace api @@ -45,7 +45,7 @@ class ComponentIterator { #ifdef USE_TEXT_SENSOR virtual bool on_text_sensor(text_sensor::TextSensor *text_sensor) = 0; #endif -#ifdef USE_API_SERVICES +#ifdef USE_API_USER_DEFINED_ACTIONS virtual bool on_service(api::UserServiceDescriptor *service); #endif #ifdef USE_CAMERA @@ -122,7 +122,7 @@ class ComponentIterator { #ifdef USE_TEXT_SENSOR TEXT_SENSOR, #endif -#ifdef USE_API_SERVICES +#ifdef USE_API_USER_DEFINED_ACTIONS SERVICE, #endif #ifdef USE_CAMERA diff --git a/esphome/core/defines.h b/esphome/core/defines.h index 41f4b28cd5..03362ce07a 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -124,7 +124,7 @@ #define USE_API_HOMEASSISTANT_STATES #define USE_API_NOISE #define USE_API_PLAINTEXT -#define USE_API_SERVICES +#define USE_API_USER_DEFINED_ACTIONS #define USE_API_CUSTOM_SERVICES #define API_MAX_SEND_QUEUE 8 #define USE_MD5 From f42b806889f51f81f31db8810443b5462c58e8e6 Mon Sep 17 00:00:00 2001 From: Javier Peletier Date: Sun, 23 Nov 2025 22:03:13 +0100 Subject: [PATCH 196/415] [core] Fix error on invalid id extend/remove (#12064) --- esphome/config.py | 2 ++ .../fixtures/substitutions/05-extend-remove.approved.yaml | 6 ++++++ .../fixtures/substitutions/05-extend-remove.input.yaml | 4 ++++ 3 files changed, 12 insertions(+) diff --git a/esphome/config.py b/esphome/config.py index 4c8019de75..1c4cdd93c6 100644 --- a/esphome/config.py +++ b/esphome/config.py @@ -355,6 +355,8 @@ def _get_item_id(item: Any) -> str | Extend | Remove | None: if isinstance(item_id, Extend): # Remove instances of Extend so they don't overwrite the original item when merging: del item[CONF_ID] + elif not isinstance(item_id, (str, Remove)): + return None return item_id diff --git a/tests/unit_tests/fixtures/substitutions/05-extend-remove.approved.yaml b/tests/unit_tests/fixtures/substitutions/05-extend-remove.approved.yaml index 35e3e6258f..773a124f25 100644 --- a/tests/unit_tests/fixtures/substitutions/05-extend-remove.approved.yaml +++ b/tests/unit_tests/fixtures/substitutions/05-extend-remove.approved.yaml @@ -31,3 +31,9 @@ lvgl: id: object5 x: 10 y: 11 + - obj: + id: + - Invalid ID + - obj: + id: + invalid: id diff --git a/tests/unit_tests/fixtures/substitutions/05-extend-remove.input.yaml b/tests/unit_tests/fixtures/substitutions/05-extend-remove.input.yaml index 617f09c31c..e6d46d6dc4 100644 --- a/tests/unit_tests/fixtures/substitutions/05-extend-remove.input.yaml +++ b/tests/unit_tests/fixtures/substitutions/05-extend-remove.input.yaml @@ -37,6 +37,10 @@ packages: id: object5 x: 10 y: 11 + - obj: + id: ["Invalid ID"] + - obj: + id: {"invalid": "id"} some_component: - id: !extend ${A} From c91a9495e611d4d1b94cffc6e88bd40f1d1843fd Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Sun, 23 Nov 2025 16:19:26 -0500 Subject: [PATCH 197/415] [ci] Fix filename (#12065) --- .../stts22h/{test.nrf52.yaml => test.nrf52-adafruit.yaml} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename tests/components/stts22h/{test.nrf52.yaml => test.nrf52-adafruit.yaml} (100%) diff --git a/tests/components/stts22h/test.nrf52.yaml b/tests/components/stts22h/test.nrf52-adafruit.yaml similarity index 100% rename from tests/components/stts22h/test.nrf52.yaml rename to tests/components/stts22h/test.nrf52-adafruit.yaml From 4795ac7b1b2a3a5f2c354ed503bf73ff2fa3dd32 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 23 Nov 2025 20:03:25 -0600 Subject: [PATCH 198/415] [esp32_ble_client] Replace std::string with char[18] for BLE address storage --- esphome/components/ble_client/automation.h | 2 +- esphome/components/ble_client/ble_client.cpp | 2 +- .../ble_client/output/ble_binary_output.cpp | 4 +- .../ble_client/sensor/ble_rssi_sensor.cpp | 6 +- .../ble_client/sensor/ble_sensor.cpp | 2 +- .../text_sensor/ble_text_sensor.cpp | 2 +- .../bluetooth_proxy/bluetooth_connection.cpp | 41 +++++----- .../bluetooth_proxy/bluetooth_proxy.cpp | 11 ++- .../esp32_ble_client/ble_characteristic.cpp | 6 +- .../esp32_ble_client/ble_client_base.cpp | 75 +++++++++---------- .../esp32_ble_client/ble_client_base.h | 16 ++-- .../esp32_ble_client/ble_service.cpp | 4 +- 12 files changed, 80 insertions(+), 91 deletions(-) diff --git a/esphome/components/ble_client/automation.h b/esphome/components/ble_client/automation.h index 9c5646b3d1..98ddaf5245 100644 --- a/esphome/components/ble_client/automation.h +++ b/esphome/components/ble_client/automation.h @@ -193,7 +193,7 @@ template class BLEClientWriteAction : public Action, publ } this->node_state = espbt::ClientState::ESTABLISHED; esph_log_d(Automation::TAG, "Found characteristic %s on device %s", this->char_uuid_.to_string().c_str(), - ble_client_->address_str().c_str()); + ble_client_->address_str()); break; } default: diff --git a/esphome/components/ble_client/ble_client.cpp b/esphome/components/ble_client/ble_client.cpp index 5cf096c9d4..b8968fe4ba 100644 --- a/esphome/components/ble_client/ble_client.cpp +++ b/esphome/components/ble_client/ble_client.cpp @@ -39,7 +39,7 @@ void BLEClient::set_enabled(bool enabled) { return; this->enabled = enabled; if (!enabled) { - ESP_LOGI(TAG, "[%s] Disabling BLE client.", this->address_str().c_str()); + ESP_LOGI(TAG, "[%s] Disabling BLE client.", this->address_str()); this->disconnect(); } } diff --git a/esphome/components/ble_client/output/ble_binary_output.cpp b/esphome/components/ble_client/output/ble_binary_output.cpp index ce67193be7..84558717f8 100644 --- a/esphome/components/ble_client/output/ble_binary_output.cpp +++ b/esphome/components/ble_client/output/ble_binary_output.cpp @@ -14,7 +14,7 @@ void BLEBinaryOutput::dump_config() { " MAC address : %s\n" " Service UUID : %s\n" " Characteristic UUID: %s", - this->parent_->address_str().c_str(), this->service_uuid_.to_string().c_str(), + this->parent_->address_str(), this->service_uuid_.to_string().c_str(), this->char_uuid_.to_string().c_str()); LOG_BINARY_OUTPUT(this); } @@ -44,7 +44,7 @@ void BLEBinaryOutput::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_i } this->node_state = espbt::ClientState::ESTABLISHED; ESP_LOGD(TAG, "Found characteristic %s on device %s", this->char_uuid_.to_string().c_str(), - this->parent()->address_str().c_str()); + this->parent()->address_str()); this->node_state = espbt::ClientState::ESTABLISHED; break; } diff --git a/esphome/components/ble_client/sensor/ble_rssi_sensor.cpp b/esphome/components/ble_client/sensor/ble_rssi_sensor.cpp index 663c52ac10..4edcbd3877 100644 --- a/esphome/components/ble_client/sensor/ble_rssi_sensor.cpp +++ b/esphome/components/ble_client/sensor/ble_rssi_sensor.cpp @@ -19,7 +19,7 @@ void BLEClientRSSISensor::loop() { void BLEClientRSSISensor::dump_config() { LOG_SENSOR("", "BLE Client RSSI Sensor", this); - ESP_LOGCONFIG(TAG, " MAC address : %s", this->parent()->address_str().c_str()); + ESP_LOGCONFIG(TAG, " MAC address : %s", this->parent()->address_str()); LOG_UPDATE_INTERVAL(this); } @@ -69,10 +69,10 @@ void BLEClientRSSISensor::update() { this->get_rssi_(); } void BLEClientRSSISensor::get_rssi_() { - ESP_LOGV(TAG, "requesting rssi from %s", this->parent()->address_str().c_str()); + ESP_LOGV(TAG, "requesting rssi from %s", this->parent()->address_str()); auto status = esp_ble_gap_read_rssi(this->parent()->get_remote_bda()); if (status != ESP_OK) { - ESP_LOGW(TAG, "esp_ble_gap_read_rssi error, address=%s, status=%d", this->parent()->address_str().c_str(), status); + ESP_LOGW(TAG, "esp_ble_gap_read_rssi error, address=%s, status=%d", this->parent()->address_str(), status); this->status_set_warning(); this->publish_state(NAN); } diff --git a/esphome/components/ble_client/sensor/ble_sensor.cpp b/esphome/components/ble_client/sensor/ble_sensor.cpp index 61685c0566..8e3e483003 100644 --- a/esphome/components/ble_client/sensor/ble_sensor.cpp +++ b/esphome/components/ble_client/sensor/ble_sensor.cpp @@ -25,7 +25,7 @@ void BLESensor::dump_config() { " Characteristic UUID: %s\n" " Descriptor UUID : %s\n" " Notifications : %s", - this->parent()->address_str().c_str(), this->service_uuid_.to_string().c_str(), + this->parent()->address_str(), this->service_uuid_.to_string().c_str(), this->char_uuid_.to_string().c_str(), this->descr_uuid_.to_string().c_str(), YESNO(this->notify_)); LOG_UPDATE_INTERVAL(this); } diff --git a/esphome/components/ble_client/text_sensor/ble_text_sensor.cpp b/esphome/components/ble_client/text_sensor/ble_text_sensor.cpp index b7a6d154db..bb771aed99 100644 --- a/esphome/components/ble_client/text_sensor/ble_text_sensor.cpp +++ b/esphome/components/ble_client/text_sensor/ble_text_sensor.cpp @@ -28,7 +28,7 @@ void BLETextSensor::dump_config() { " Characteristic UUID: %s\n" " Descriptor UUID : %s\n" " Notifications : %s", - this->parent()->address_str().c_str(), this->service_uuid_.to_string().c_str(), + this->parent()->address_str(), this->service_uuid_.to_string().c_str(), this->char_uuid_.to_string().c_str(), this->descr_uuid_.to_string().c_str(), YESNO(this->notify_)); LOG_UPDATE_INTERVAL(this); } diff --git a/esphome/components/bluetooth_proxy/bluetooth_connection.cpp b/esphome/components/bluetooth_proxy/bluetooth_connection.cpp index fcc344dda9..1d6f7e23b3 100644 --- a/esphome/components/bluetooth_proxy/bluetooth_connection.cpp +++ b/esphome/components/bluetooth_proxy/bluetooth_connection.cpp @@ -196,8 +196,8 @@ void BluetoothConnection::send_service_for_discovery_() { if (service_status != ESP_GATT_OK || service_count == 0) { ESP_LOGE(TAG, "[%d] [%s] esp_ble_gattc_get_service %s, status=%d, service_count=%d, offset=%d", - this->connection_index_, this->address_str().c_str(), - service_status != ESP_GATT_OK ? "error" : "missing", service_status, service_count, this->send_service_); + this->connection_index_, this->address_str(), service_status != ESP_GATT_OK ? "error" : "missing", + service_status, service_count, this->send_service_); this->send_service_ = DONE_SENDING_SERVICES; return; } @@ -312,13 +312,13 @@ void BluetoothConnection::send_service_for_discovery_() { if (resp.services.size() > 1) { resp.services.pop_back(); ESP_LOGD(TAG, "[%d] [%s] Service %d would exceed limit (current: %d + service: %d > %d), sending current batch", - this->connection_index_, this->address_str().c_str(), this->send_service_, current_size, service_size, + this->connection_index_, this->address_str(), this->send_service_, current_size, service_size, MAX_PACKET_SIZE); // Don't increment send_service_ - we'll retry this service in next batch } else { // This single service is too large, but we have to send it anyway ESP_LOGV(TAG, "[%d] [%s] Service %d is too large (%d bytes) but sending anyway", this->connection_index_, - this->address_str().c_str(), this->send_service_, service_size); + this->address_str(), this->send_service_, service_size); // Increment so we don't get stuck this->send_service_++; } @@ -337,21 +337,20 @@ void BluetoothConnection::send_service_for_discovery_() { } void BluetoothConnection::log_connection_error_(const char *operation, esp_gatt_status_t status) { - ESP_LOGE(TAG, "[%d] [%s] %s error, status=%d", this->connection_index_, this->address_str().c_str(), operation, - status); + ESP_LOGE(TAG, "[%d] [%s] %s error, status=%d", this->connection_index_, this->address_str(), operation, status); } void BluetoothConnection::log_connection_warning_(const char *operation, esp_err_t err) { - ESP_LOGW(TAG, "[%d] [%s] %s failed, err=%d", this->connection_index_, this->address_str().c_str(), operation, err); + ESP_LOGW(TAG, "[%d] [%s] %s failed, err=%d", this->connection_index_, this->address_str(), operation, err); } void BluetoothConnection::log_gatt_not_connected_(const char *action, const char *type) { - ESP_LOGW(TAG, "[%d] [%s] Cannot %s GATT %s, not connected.", this->connection_index_, this->address_str().c_str(), - action, type); + ESP_LOGW(TAG, "[%d] [%s] Cannot %s GATT %s, not connected.", this->connection_index_, this->address_str(), action, + type); } void BluetoothConnection::log_gatt_operation_error_(const char *operation, uint16_t handle, esp_gatt_status_t status) { - ESP_LOGW(TAG, "[%d] [%s] Error %s for handle 0x%2X, status=%d", this->connection_index_, this->address_str().c_str(), + ESP_LOGW(TAG, "[%d] [%s] Error %s for handle 0x%2X, status=%d", this->connection_index_, this->address_str(), operation, handle, status); } @@ -372,14 +371,14 @@ bool BluetoothConnection::gattc_event_handler(esp_gattc_cb_event_t event, esp_ga case ESP_GATTC_DISCONNECT_EVT: { // Don't reset connection yet - wait for CLOSE_EVT to ensure controller has freed resources // This prevents race condition where we mark slot as free before controller cleanup is complete - ESP_LOGD(TAG, "[%d] [%s] Disconnect, reason=0x%02x", this->connection_index_, this->address_str_.c_str(), + ESP_LOGD(TAG, "[%d] [%s] Disconnect, reason=0x%02x", this->connection_index_, this->address_str_, param->disconnect.reason); // Send disconnection notification but don't free the slot yet this->proxy_->send_device_connection(this->address_, false, 0, param->disconnect.reason); break; } case ESP_GATTC_CLOSE_EVT: { - ESP_LOGD(TAG, "[%d] [%s] Close, reason=0x%02x, freeing slot", this->connection_index_, this->address_str_.c_str(), + ESP_LOGD(TAG, "[%d] [%s] Close, reason=0x%02x, freeing slot", this->connection_index_, this->address_str_, param->close.reason); // Now the GATT connection is fully closed and controller resources are freed // Safe to mark the connection slot as available @@ -463,7 +462,7 @@ bool BluetoothConnection::gattc_event_handler(esp_gattc_cb_event_t event, esp_ga break; } case ESP_GATTC_NOTIFY_EVT: { - ESP_LOGV(TAG, "[%d] [%s] ESP_GATTC_NOTIFY_EVT: handle=0x%2X", this->connection_index_, this->address_str_.c_str(), + ESP_LOGV(TAG, "[%d] [%s] ESP_GATTC_NOTIFY_EVT: handle=0x%2X", this->connection_index_, this->address_str_, param->notify.handle); api::BluetoothGATTNotifyDataResponse resp; resp.address = this->address_; @@ -502,8 +501,7 @@ esp_err_t BluetoothConnection::read_characteristic(uint16_t handle) { return ESP_GATT_NOT_CONNECTED; } - ESP_LOGV(TAG, "[%d] [%s] Reading GATT characteristic handle %d", this->connection_index_, this->address_str_.c_str(), - handle); + ESP_LOGV(TAG, "[%d] [%s] Reading GATT characteristic handle %d", this->connection_index_, this->address_str_, handle); esp_err_t err = esp_ble_gattc_read_char(this->gattc_if_, this->conn_id_, handle, ESP_GATT_AUTH_REQ_NONE); return this->check_and_log_error_("esp_ble_gattc_read_char", err); @@ -515,8 +513,7 @@ esp_err_t BluetoothConnection::write_characteristic(uint16_t handle, const uint8 this->log_gatt_not_connected_("write", "characteristic"); return ESP_GATT_NOT_CONNECTED; } - ESP_LOGV(TAG, "[%d] [%s] Writing GATT characteristic handle %d", this->connection_index_, this->address_str_.c_str(), - handle); + ESP_LOGV(TAG, "[%d] [%s] Writing GATT characteristic handle %d", this->connection_index_, this->address_str_, handle); // ESP-IDF's API requires a non-const uint8_t* but it doesn't modify the data // The BTC layer immediately copies the data to its own buffer (see btc_gattc.c) @@ -532,8 +529,7 @@ esp_err_t BluetoothConnection::read_descriptor(uint16_t handle) { this->log_gatt_not_connected_("read", "descriptor"); return ESP_GATT_NOT_CONNECTED; } - ESP_LOGV(TAG, "[%d] [%s] Reading GATT descriptor handle %d", this->connection_index_, this->address_str_.c_str(), - handle); + ESP_LOGV(TAG, "[%d] [%s] Reading GATT descriptor handle %d", this->connection_index_, this->address_str_, handle); esp_err_t err = esp_ble_gattc_read_char_descr(this->gattc_if_, this->conn_id_, handle, ESP_GATT_AUTH_REQ_NONE); return this->check_and_log_error_("esp_ble_gattc_read_char_descr", err); @@ -544,8 +540,7 @@ esp_err_t BluetoothConnection::write_descriptor(uint16_t handle, const uint8_t * this->log_gatt_not_connected_("write", "descriptor"); return ESP_GATT_NOT_CONNECTED; } - ESP_LOGV(TAG, "[%d] [%s] Writing GATT descriptor handle %d", this->connection_index_, this->address_str_.c_str(), - handle); + ESP_LOGV(TAG, "[%d] [%s] Writing GATT descriptor handle %d", this->connection_index_, this->address_str_, handle); // ESP-IDF's API requires a non-const uint8_t* but it doesn't modify the data // The BTC layer immediately copies the data to its own buffer (see btc_gattc.c) @@ -564,13 +559,13 @@ esp_err_t BluetoothConnection::notify_characteristic(uint16_t handle, bool enabl if (enable) { ESP_LOGV(TAG, "[%d] [%s] Registering for GATT characteristic notifications handle %d", this->connection_index_, - this->address_str_.c_str(), handle); + this->address_str_, handle); esp_err_t err = esp_ble_gattc_register_for_notify(this->gattc_if_, this->remote_bda_, handle); return this->check_and_log_error_("esp_ble_gattc_register_for_notify", err); } ESP_LOGV(TAG, "[%d] [%s] Unregistering for GATT characteristic notifications handle %d", this->connection_index_, - this->address_str_.c_str(), handle); + this->address_str_, handle); esp_err_t err = esp_ble_gattc_unregister_for_notify(this->gattc_if_, this->remote_bda_, handle); return this->check_and_log_error_("esp_ble_gattc_unregister_for_notify", err); } diff --git a/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp b/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp index 34e0aa93a3..71f8da75a7 100644 --- a/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp +++ b/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp @@ -47,12 +47,11 @@ void BluetoothProxy::send_bluetooth_scanner_state_(esp32_ble_tracker::ScannerSta void BluetoothProxy::log_connection_request_ignored_(BluetoothConnection *connection, espbt::ClientState state) { ESP_LOGW(TAG, "[%d] [%s] Connection request ignored, state: %s", connection->get_connection_index(), - connection->address_str().c_str(), espbt::client_state_to_string(state)); + connection->address_str(), espbt::client_state_to_string(state)); } void BluetoothProxy::log_connection_info_(BluetoothConnection *connection, const char *message) { - ESP_LOGI(TAG, "[%d] [%s] Connecting %s", connection->get_connection_index(), connection->address_str().c_str(), - message); + ESP_LOGI(TAG, "[%d] [%s] Connecting %s", connection->get_connection_index(), connection->address_str(), message); } void BluetoothProxy::log_not_connected_gatt_(const char *action, const char *type) { @@ -186,7 +185,7 @@ void BluetoothProxy::bluetooth_device_request(const api::BluetoothDeviceRequest } if (!msg.has_address_type) { ESP_LOGE(TAG, "[%d] [%s] Missing address type in connect request", connection->get_connection_index(), - connection->address_str().c_str()); + connection->address_str()); this->send_device_connection(msg.address, false); return; } @@ -199,7 +198,7 @@ void BluetoothProxy::bluetooth_device_request(const api::BluetoothDeviceRequest } else if (connection->state() == espbt::ClientState::CONNECTING) { if (connection->disconnect_pending()) { ESP_LOGW(TAG, "[%d] [%s] Connection request while pending disconnect, cancelling pending disconnect", - connection->get_connection_index(), connection->address_str().c_str()); + connection->get_connection_index(), connection->address_str()); connection->cancel_pending_disconnect(); return; } @@ -339,7 +338,7 @@ void BluetoothProxy::bluetooth_gatt_send_services(const api::BluetoothGATTGetSer return; } if (!connection->service_count_) { - ESP_LOGW(TAG, "[%d] [%s] No GATT services found", connection->connection_index_, connection->address_str().c_str()); + ESP_LOGW(TAG, "[%d] [%s] No GATT services found", connection->connection_index_, connection->address_str()); this->send_gatt_services_done(msg.address); return; } diff --git a/esphome/components/esp32_ble_client/ble_characteristic.cpp b/esphome/components/esp32_ble_client/ble_characteristic.cpp index 36229c23c3..e0d0174c57 100644 --- a/esphome/components/esp32_ble_client/ble_characteristic.cpp +++ b/esphome/components/esp32_ble_client/ble_characteristic.cpp @@ -38,7 +38,7 @@ void BLECharacteristic::parse_descriptors() { } if (status != ESP_GATT_OK) { ESP_LOGW(TAG, "[%d] [%s] esp_ble_gattc_get_all_descr error, status=%d", - this->service->client->get_connection_index(), this->service->client->address_str().c_str(), status); + this->service->client->get_connection_index(), this->service->client->address_str(), status); break; } if (count == 0) { @@ -51,7 +51,7 @@ void BLECharacteristic::parse_descriptors() { desc->characteristic = this; this->descriptors.push_back(desc); ESP_LOGV(TAG, "[%d] [%s] descriptor %s, handle 0x%x", this->service->client->get_connection_index(), - this->service->client->address_str().c_str(), desc->uuid.to_string().c_str(), desc->handle); + this->service->client->address_str(), desc->uuid.to_string().c_str(), desc->handle); offset++; } } @@ -84,7 +84,7 @@ esp_err_t BLECharacteristic::write_value(uint8_t *new_val, int16_t new_val_size, new_val, write_type, ESP_GATT_AUTH_REQ_NONE); if (status) { ESP_LOGW(TAG, "[%d] [%s] Error sending write value to BLE gattc server, status=%d", - this->service->client->get_connection_index(), this->service->client->address_str().c_str(), status); + this->service->client->get_connection_index(), this->service->client->address_str(), status); } return status; } diff --git a/esphome/components/esp32_ble_client/ble_client_base.cpp b/esphome/components/esp32_ble_client/ble_client_base.cpp index 18321ef91c..07e88c7528 100644 --- a/esphome/components/esp32_ble_client/ble_client_base.cpp +++ b/esphome/components/esp32_ble_client/ble_client_base.cpp @@ -41,7 +41,7 @@ void BLEClientBase::setup() { } void BLEClientBase::set_state(espbt::ClientState st) { - ESP_LOGV(TAG, "[%d] [%s] Set state %d", this->connection_index_, this->address_str_.c_str(), (int) st); + ESP_LOGV(TAG, "[%d] [%s] Set state %d", this->connection_index_, this->address_str_, (int) st); ESPBTClient::set_state(st); } @@ -71,7 +71,7 @@ void BLEClientBase::dump_config() { ESP_LOGCONFIG(TAG, " Address: %s\n" " Auto-Connect: %s", - this->address_str().c_str(), TRUEFALSE(this->auto_connect_)); + this->address_str(), TRUEFALSE(this->auto_connect_)); ESP_LOGCONFIG(TAG, " State: %s", espbt::client_state_to_string(this->state())); if (this->status_ == ESP_GATT_NO_RESOURCES) { ESP_LOGE(TAG, " Failed due to no resources. Try to reduce number of BLE clients in config."); @@ -104,12 +104,11 @@ void BLEClientBase::connect() { // Prevent duplicate connection attempts if (this->state_ == espbt::ClientState::CONNECTING || this->state_ == espbt::ClientState::CONNECTED || this->state_ == espbt::ClientState::ESTABLISHED) { - ESP_LOGW(TAG, "[%d] [%s] Connection already in progress, state=%s", this->connection_index_, - this->address_str_.c_str(), espbt::client_state_to_string(this->state_)); + ESP_LOGW(TAG, "[%d] [%s] Connection already in progress, state=%s", this->connection_index_, this->address_str_, + espbt::client_state_to_string(this->state_)); return; } - ESP_LOGI(TAG, "[%d] [%s] 0x%02x Connecting", this->connection_index_, this->address_str_.c_str(), - this->remote_addr_type_); + ESP_LOGI(TAG, "[%d] [%s] 0x%02x Connecting", this->connection_index_, this->address_str_, this->remote_addr_type_); this->paired_ = false; // Enable loop for state processing this->enable_loop(); @@ -135,13 +134,13 @@ esp_err_t BLEClientBase::pair() { return esp_ble_set_encryption(this->remote_bda void BLEClientBase::disconnect() { if (this->state_ == espbt::ClientState::IDLE || this->state_ == espbt::ClientState::DISCONNECTING) { - ESP_LOGI(TAG, "[%d] [%s] Disconnect requested, but already %s", this->connection_index_, this->address_str_.c_str(), + ESP_LOGI(TAG, "[%d] [%s] Disconnect requested, but already %s", this->connection_index_, this->address_str_, espbt::client_state_to_string(this->state_)); return; } if (this->state_ == espbt::ClientState::CONNECTING || this->conn_id_ == UNSET_CONN_ID) { ESP_LOGD(TAG, "[%d] [%s] Disconnect before connected, disconnect scheduled", this->connection_index_, - this->address_str_.c_str()); + this->address_str_); this->want_disconnect_ = true; return; } @@ -150,8 +149,7 @@ void BLEClientBase::disconnect() { void BLEClientBase::unconditional_disconnect() { // Disconnect without checking the state. - ESP_LOGI(TAG, "[%d] [%s] Disconnecting (conn_id: %d).", this->connection_index_, this->address_str_.c_str(), - this->conn_id_); + ESP_LOGI(TAG, "[%d] [%s] Disconnecting (conn_id: %d).", this->connection_index_, this->address_str_, this->conn_id_); if (this->state_ == espbt::ClientState::DISCONNECTING) { this->log_error_("Already disconnecting"); return; @@ -192,24 +190,23 @@ void BLEClientBase::release_services() { } void BLEClientBase::log_event_(const char *name) { - ESP_LOGD(TAG, "[%d] [%s] %s", this->connection_index_, this->address_str_.c_str(), name); + ESP_LOGD(TAG, "[%d] [%s] %s", this->connection_index_, this->address_str_, name); } void BLEClientBase::log_gattc_event_(const char *name) { - ESP_LOGD(TAG, "[%d] [%s] ESP_GATTC_%s_EVT", this->connection_index_, this->address_str_.c_str(), name); + ESP_LOGD(TAG, "[%d] [%s] ESP_GATTC_%s_EVT", this->connection_index_, this->address_str_, name); } void BLEClientBase::log_gattc_warning_(const char *operation, esp_gatt_status_t status) { - ESP_LOGW(TAG, "[%d] [%s] %s error, status=%d", this->connection_index_, this->address_str_.c_str(), operation, - status); + ESP_LOGW(TAG, "[%d] [%s] %s error, status=%d", this->connection_index_, this->address_str_, operation, status); } void BLEClientBase::log_gattc_warning_(const char *operation, esp_err_t err) { - ESP_LOGW(TAG, "[%d] [%s] %s error, status=%d", this->connection_index_, this->address_str_.c_str(), operation, err); + ESP_LOGW(TAG, "[%d] [%s] %s error, status=%d", this->connection_index_, this->address_str_, operation, err); } void BLEClientBase::log_connection_params_(const char *param_type) { - ESP_LOGD(TAG, "[%d] [%s] %s conn params", this->connection_index_, this->address_str_.c_str(), param_type); + ESP_LOGD(TAG, "[%d] [%s] %s conn params", this->connection_index_, this->address_str_, param_type); } void BLEClientBase::handle_connection_result_(esp_err_t ret) { @@ -220,15 +217,15 @@ void BLEClientBase::handle_connection_result_(esp_err_t ret) { } void BLEClientBase::log_error_(const char *message) { - ESP_LOGE(TAG, "[%d] [%s] %s", this->connection_index_, this->address_str_.c_str(), message); + ESP_LOGE(TAG, "[%d] [%s] %s", this->connection_index_, this->address_str_, message); } void BLEClientBase::log_error_(const char *message, int code) { - ESP_LOGE(TAG, "[%d] [%s] %s=%d", this->connection_index_, this->address_str_.c_str(), message, code); + ESP_LOGE(TAG, "[%d] [%s] %s=%d", this->connection_index_, this->address_str_, message, code); } void BLEClientBase::log_warning_(const char *message) { - ESP_LOGW(TAG, "[%d] [%s] %s", this->connection_index_, this->address_str_.c_str(), message); + ESP_LOGW(TAG, "[%d] [%s] %s", this->connection_index_, this->address_str_, message); } void BLEClientBase::update_conn_params_(uint16_t min_interval, uint16_t max_interval, uint16_t latency, @@ -264,13 +261,13 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_ if (event != ESP_GATTC_REG_EVT && esp_gattc_if != ESP_GATT_IF_NONE && esp_gattc_if != this->gattc_if_) return false; - ESP_LOGV(TAG, "[%d] [%s] gattc_event_handler: event=%d gattc_if=%d", this->connection_index_, - this->address_str_.c_str(), event, esp_gattc_if); + ESP_LOGV(TAG, "[%d] [%s] gattc_event_handler: event=%d gattc_if=%d", this->connection_index_, this->address_str_, + event, esp_gattc_if); switch (event) { case ESP_GATTC_REG_EVT: { if (param->reg.status == ESP_GATT_OK) { - ESP_LOGV(TAG, "[%d] [%s] gattc registered app id %d", this->connection_index_, this->address_str_.c_str(), + ESP_LOGV(TAG, "[%d] [%s] gattc registered app id %d", this->connection_index_, this->address_str_, this->app_id); this->gattc_if_ = esp_gattc_if; } else { @@ -292,7 +289,7 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_ // arriving after we've already transitioned to IDLE state. if (this->state_ == espbt::ClientState::IDLE) { ESP_LOGD(TAG, "[%d] [%s] ESP_GATTC_OPEN_EVT in IDLE state (status=%d), ignoring", this->connection_index_, - this->address_str_.c_str(), param->open.status); + this->address_str_, param->open.status); break; } @@ -301,7 +298,7 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_ // because it means we have a bad assumption about how the // ESP BT stack works. ESP_LOGE(TAG, "[%d] [%s] ESP_GATTC_OPEN_EVT in %s state (status=%d)", this->connection_index_, - this->address_str_.c_str(), espbt::client_state_to_string(this->state_), param->open.status); + this->address_str_, espbt::client_state_to_string(this->state_), param->open.status); } if (param->open.status != ESP_GATT_OK && param->open.status != ESP_GATT_ALREADY_OPEN) { this->log_gattc_warning_("Connection open", param->open.status); @@ -318,7 +315,7 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_ } // MTU negotiation already started in ESP_GATTC_CONNECT_EVT this->set_state(espbt::ClientState::CONNECTED); - ESP_LOGI(TAG, "[%d] [%s] Connection open", this->connection_index_, this->address_str_.c_str()); + ESP_LOGI(TAG, "[%d] [%s] Connection open", this->connection_index_, this->address_str_); if (this->connection_type_ == espbt::ConnectionType::V3_WITH_CACHE) { // Cached connections already connected with medium parameters, no update needed // only set our state, subclients might have more stuff to do yet. @@ -354,8 +351,8 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_ this->state_ == espbt::ClientState::CONNECTED) { this->log_warning_("Remote closed during discovery"); } else { - ESP_LOGD(TAG, "[%d] [%s] ESP_GATTC_DISCONNECT_EVT, reason 0x%02x", this->connection_index_, - this->address_str_.c_str(), param->disconnect.reason); + ESP_LOGD(TAG, "[%d] [%s] ESP_GATTC_DISCONNECT_EVT, reason 0x%02x", this->connection_index_, this->address_str_, + param->disconnect.reason); } this->release_services(); this->set_state(espbt::ClientState::IDLE); @@ -366,12 +363,12 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_ if (this->conn_id_ != param->cfg_mtu.conn_id) return false; if (param->cfg_mtu.status != ESP_GATT_OK) { - ESP_LOGW(TAG, "[%d] [%s] cfg_mtu failed, mtu %d, status %d", this->connection_index_, - this->address_str_.c_str(), param->cfg_mtu.mtu, param->cfg_mtu.status); + ESP_LOGW(TAG, "[%d] [%s] cfg_mtu failed, mtu %d, status %d", this->connection_index_, this->address_str_, + param->cfg_mtu.mtu, param->cfg_mtu.status); // No state change required here - disconnect event will follow if needed. break; } - ESP_LOGD(TAG, "[%d] [%s] cfg_mtu status %d, mtu %d", this->connection_index_, this->address_str_.c_str(), + ESP_LOGD(TAG, "[%d] [%s] cfg_mtu status %d, mtu %d", this->connection_index_, this->address_str_, param->cfg_mtu.status, param->cfg_mtu.mtu); this->mtu_ = param->cfg_mtu.mtu; break; @@ -415,14 +412,14 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_ } else if (this->connection_type_ != espbt::ConnectionType::V3_WITH_CACHE) { #ifdef USE_ESP32_BLE_DEVICE for (auto &svc : this->services_) { - ESP_LOGV(TAG, "[%d] [%s] Service UUID: %s", this->connection_index_, this->address_str_.c_str(), + ESP_LOGV(TAG, "[%d] [%s] Service UUID: %s", this->connection_index_, this->address_str_, svc->uuid.to_string().c_str()); - ESP_LOGV(TAG, "[%d] [%s] start_handle: 0x%x end_handle: 0x%x", this->connection_index_, - this->address_str_.c_str(), svc->start_handle, svc->end_handle); + ESP_LOGV(TAG, "[%d] [%s] start_handle: 0x%x end_handle: 0x%x", this->connection_index_, this->address_str_, + svc->start_handle, svc->end_handle); } #endif } - ESP_LOGI(TAG, "[%d] [%s] Service discovery complete", this->connection_index_, this->address_str_.c_str()); + ESP_LOGI(TAG, "[%d] [%s] Service discovery complete", this->connection_index_, this->address_str_); this->state_ = espbt::ClientState::ESTABLISHED; break; } @@ -503,7 +500,7 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_ default: // ideally would check all other events for matching conn_id - ESP_LOGD(TAG, "[%d] [%s] Event %d", this->connection_index_, this->address_str_.c_str(), event); + ESP_LOGD(TAG, "[%d] [%s] Event %d", this->connection_index_, this->address_str_, event); break; } return true; @@ -520,7 +517,7 @@ void BLEClientBase::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_ case ESP_GAP_BLE_SEC_REQ_EVT: if (!this->check_addr(param->ble_security.auth_cmpl.bd_addr)) return; - ESP_LOGV(TAG, "[%d] [%s] ESP_GAP_BLE_SEC_REQ_EVT %x", this->connection_index_, this->address_str_.c_str(), event); + ESP_LOGV(TAG, "[%d] [%s] ESP_GAP_BLE_SEC_REQ_EVT %x", this->connection_index_, this->address_str_, event); esp_ble_gap_security_rsp(param->ble_security.ble_req.bd_addr, true); break; // This event is sent once authentication has completed @@ -529,13 +526,13 @@ void BLEClientBase::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_ return; esp_bd_addr_t bd_addr; memcpy(bd_addr, param->ble_security.auth_cmpl.bd_addr, sizeof(esp_bd_addr_t)); - ESP_LOGI(TAG, "[%d] [%s] auth complete addr: %s", this->connection_index_, this->address_str_.c_str(), + ESP_LOGI(TAG, "[%d] [%s] auth complete addr: %s", this->connection_index_, this->address_str_, format_hex(bd_addr, 6).c_str()); if (!param->ble_security.auth_cmpl.success) { this->log_error_("auth fail reason", param->ble_security.auth_cmpl.fail_reason); } else { this->paired_ = true; - ESP_LOGD(TAG, "[%d] [%s] auth success type = %d mode = %d", this->connection_index_, this->address_str_.c_str(), + ESP_LOGD(TAG, "[%d] [%s] auth success type = %d mode = %d", this->connection_index_, this->address_str_, param->ble_security.auth_cmpl.addr_type, param->ble_security.auth_cmpl.auth_mode); } break; @@ -598,7 +595,7 @@ float BLEClientBase::parse_char_value(uint8_t *value, uint16_t length) { } } ESP_LOGW(TAG, "[%d] [%s] Cannot parse characteristic value of type 0x%x length %d", this->connection_index_, - this->address_str_.c_str(), value[0], length); + this->address_str_, value[0], length); return NAN; } diff --git a/esphome/components/esp32_ble_client/ble_client_base.h b/esphome/components/esp32_ble_client/ble_client_base.h index 7f0ae3b83e..7786495915 100644 --- a/esphome/components/esp32_ble_client/ble_client_base.h +++ b/esphome/components/esp32_ble_client/ble_client_base.h @@ -10,7 +10,6 @@ #endif #include -#include #include #include @@ -23,6 +22,7 @@ namespace esphome::esp32_ble_client { namespace espbt = esphome::esp32_ble_tracker; static const int UNSET_CONN_ID = 0xFFFF; +static constexpr size_t MAC_ADDR_STR_LEN = 18; // "AA:BB:CC:DD:EE:FF\0" class BLEClientBase : public espbt::ESPBTClient, public Component { public: @@ -58,14 +58,12 @@ class BLEClientBase : public espbt::ESPBTClient, public Component { this->remote_bda_[4] = (address >> 8) & 0xFF; this->remote_bda_[5] = (address >> 0) & 0xFF; if (address == 0) { - this->address_str_ = ""; + this->address_str_[0] = '\0'; } else { - char buf[18]; - format_mac_addr_upper(this->remote_bda_, buf); - this->address_str_ = buf; + format_mac_addr_upper(this->remote_bda_, this->address_str_); } } - const std::string &address_str() const { return this->address_str_; } + const char *address_str() const { return this->address_str_; } #ifdef USE_ESP32_BLE_DEVICE BLEService *get_service(espbt::ESPBTUUID uuid); @@ -104,7 +102,6 @@ class BLEClientBase : public espbt::ESPBTClient, public Component { uint64_t address_{0}; // Group 2: Container types (grouped for memory optimization) - std::string address_str_{}; #ifdef USE_ESP32_BLE_DEVICE std::vector services_; #endif @@ -113,8 +110,9 @@ class BLEClientBase : public espbt::ESPBTClient, public Component { int gattc_if_; esp_gatt_status_t status_{ESP_GATT_OK}; - // Group 4: Arrays (6 bytes) - esp_bd_addr_t remote_bda_; + // Group 4: Arrays + char address_str_[MAC_ADDR_STR_LEN]{}; // 18 bytes: "AA:BB:CC:DD:EE:FF\0" + esp_bd_addr_t remote_bda_; // 6 bytes // Group 5: 2-byte types uint16_t conn_id_{UNSET_CONN_ID}; diff --git a/esphome/components/esp32_ble_client/ble_service.cpp b/esphome/components/esp32_ble_client/ble_service.cpp index accaad15e1..deaaa3de02 100644 --- a/esphome/components/esp32_ble_client/ble_service.cpp +++ b/esphome/components/esp32_ble_client/ble_service.cpp @@ -51,7 +51,7 @@ void BLEService::parse_characteristics() { } if (status != ESP_GATT_OK) { ESP_LOGW(TAG, "[%d] [%s] esp_ble_gattc_get_all_char error, status=%d", this->client->get_connection_index(), - this->client->address_str().c_str(), status); + this->client->address_str(), status); break; } if (count == 0) { @@ -65,7 +65,7 @@ void BLEService::parse_characteristics() { characteristic->service = this; this->characteristics.push_back(characteristic); ESP_LOGV(TAG, "[%d] [%s] characteristic %s, handle 0x%x, properties 0x%x", this->client->get_connection_index(), - this->client->address_str().c_str(), characteristic->uuid.to_string().c_str(), characteristic->handle, + this->client->address_str(), characteristic->uuid.to_string().c_str(), characteristic->handle, characteristic->properties); offset++; } From 173912c68b20a917a29cf3afb3a392cc1374cc8d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 23 Nov 2025 20:10:44 -0600 Subject: [PATCH 199/415] more fixes --- esphome/components/alpha3/alpha3.cpp | 18 +++++----- .../components/am43/sensor/am43_sensor.cpp | 13 +++---- esphome/components/anova/anova.cpp | 9 +++-- .../display/pvvx_display.cpp | 36 +++++++++---------- 4 files changed, 35 insertions(+), 41 deletions(-) diff --git a/esphome/components/alpha3/alpha3.cpp b/esphome/components/alpha3/alpha3.cpp index 344f2d5a03..55fd196822 100644 --- a/esphome/components/alpha3/alpha3.cpp +++ b/esphome/components/alpha3/alpha3.cpp @@ -56,13 +56,13 @@ bool Alpha3::is_current_response_type_(const uint8_t *response_type) { void Alpha3::handle_geni_response_(const uint8_t *response, uint16_t length) { if (this->response_offset_ >= this->response_length_) { - ESP_LOGD(TAG, "[%s] GENI response begin", this->parent_->address_str().c_str()); + ESP_LOGD(TAG, "[%s] GENI response begin", this->parent_->address_str()); if (length < GENI_RESPONSE_HEADER_LENGTH) { - ESP_LOGW(TAG, "[%s] response to short", this->parent_->address_str().c_str()); + ESP_LOGW(TAG, "[%s] response to short", this->parent_->address_str()); return; } if (response[0] != 36 || response[2] != 248 || response[3] != 231 || response[4] != 10) { - ESP_LOGW(TAG, "[%s] response bytes %d %d %d %d %d don't match GENI HEADER", this->parent_->address_str().c_str(), + ESP_LOGW(TAG, "[%s] response bytes %d %d %d %d %d don't match GENI HEADER", this->parent_->address_str(), response[0], response[1], response[2], response[3], response[4]); return; } @@ -77,11 +77,11 @@ void Alpha3::handle_geni_response_(const uint8_t *response, uint16_t length) { }; if (this->is_current_response_type_(GENI_RESPONSE_TYPE_FLOW_HEAD)) { - ESP_LOGD(TAG, "[%s] FLOW HEAD Response", this->parent_->address_str().c_str()); + ESP_LOGD(TAG, "[%s] FLOW HEAD Response", this->parent_->address_str()); extract_publish_sensor_value(GENI_RESPONSE_FLOW_OFFSET, this->flow_sensor_, 3600.0F); extract_publish_sensor_value(GENI_RESPONSE_HEAD_OFFSET, this->head_sensor_, .0001F); } else if (this->is_current_response_type_(GENI_RESPONSE_TYPE_POWER)) { - ESP_LOGD(TAG, "[%s] POWER Response", this->parent_->address_str().c_str()); + ESP_LOGD(TAG, "[%s] POWER Response", this->parent_->address_str()); extract_publish_sensor_value(GENI_RESPONSE_POWER_OFFSET, this->power_sensor_, 1.0F); extract_publish_sensor_value(GENI_RESPONSE_CURRENT_OFFSET, this->current_sensor_, 1.0F); extract_publish_sensor_value(GENI_RESPONSE_MOTOR_SPEED_OFFSET, this->speed_sensor_, 1.0F); @@ -100,7 +100,7 @@ void Alpha3::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc if (param->open.status == ESP_GATT_OK) { this->response_offset_ = 0; this->response_length_ = 0; - ESP_LOGI(TAG, "[%s] connection open", this->parent_->address_str().c_str()); + ESP_LOGI(TAG, "[%s] connection open", this->parent_->address_str()); } break; } @@ -132,7 +132,7 @@ void Alpha3::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc case ESP_GATTC_SEARCH_CMPL_EVT: { auto *chr = this->parent_->get_characteristic(ALPHA3_GENI_SERVICE_UUID, ALPHA3_GENI_CHARACTERISTIC_UUID); if (chr == nullptr) { - ESP_LOGE(TAG, "[%s] No GENI service found at device, not an Alpha3..?", this->parent_->address_str().c_str()); + ESP_LOGE(TAG, "[%s] No GENI service found at device, not an Alpha3..?", this->parent_->address_str()); break; } auto status = esp_ble_gattc_register_for_notify(this->parent_->get_gattc_if(), this->parent_->get_remote_bda(), @@ -164,12 +164,12 @@ void Alpha3::send_request_(uint8_t *request, size_t len) { esp_ble_gattc_write_char(this->parent_->get_gattc_if(), this->parent_->get_conn_id(), this->geni_handle_, len, request, ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE); if (status) - ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str().c_str(), status); + ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str(), status); } void Alpha3::update() { if (this->node_state != espbt::ClientState::ESTABLISHED) { - ESP_LOGW(TAG, "[%s] Cannot poll, not connected", this->parent_->address_str().c_str()); + ESP_LOGW(TAG, "[%s] Cannot poll, not connected", this->parent_->address_str()); return; } diff --git a/esphome/components/am43/sensor/am43_sensor.cpp b/esphome/components/am43/sensor/am43_sensor.cpp index 4cc99001ae..b2bc3254e2 100644 --- a/esphome/components/am43/sensor/am43_sensor.cpp +++ b/esphome/components/am43/sensor/am43_sensor.cpp @@ -44,11 +44,9 @@ void Am43::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_i auto *chr = this->parent_->get_characteristic(AM43_SERVICE_UUID, AM43_CHARACTERISTIC_UUID); if (chr == nullptr) { if (this->parent_->get_characteristic(AM43_TUYA_SERVICE_UUID, AM43_TUYA_CHARACTERISTIC_UUID) != nullptr) { - ESP_LOGE(TAG, "[%s] Detected a Tuya AM43 which is not supported, sorry.", - this->parent_->address_str().c_str()); + ESP_LOGE(TAG, "[%s] Detected a Tuya AM43 which is not supported, sorry.", this->parent_->address_str()); } else { - ESP_LOGE(TAG, "[%s] No control service found at device, not an AM43..?", - this->parent_->address_str().c_str()); + ESP_LOGE(TAG, "[%s] No control service found at device, not an AM43..?", this->parent_->address_str()); } break; } @@ -82,8 +80,7 @@ void Am43::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_i this->char_handle_, packet->length, packet->data, ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE); if (status) { - ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str().c_str(), - status); + ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str(), status); } } this->current_sensor_ = 0; @@ -97,7 +94,7 @@ void Am43::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_i void Am43::update() { if (this->node_state != espbt::ClientState::ESTABLISHED) { - ESP_LOGW(TAG, "[%s] Cannot poll, not connected", this->parent_->address_str().c_str()); + ESP_LOGW(TAG, "[%s] Cannot poll, not connected", this->parent_->address_str()); return; } if (this->current_sensor_ == 0) { @@ -107,7 +104,7 @@ void Am43::update() { esp_ble_gattc_write_char(this->parent_->get_gattc_if(), this->parent_->get_conn_id(), this->char_handle_, packet->length, packet->data, ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE); if (status) { - ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str().c_str(), status); + ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str(), status); } } this->current_sensor_++; diff --git a/esphome/components/anova/anova.cpp b/esphome/components/anova/anova.cpp index d0e8f6827f..2693224a97 100644 --- a/esphome/components/anova/anova.cpp +++ b/esphome/components/anova/anova.cpp @@ -42,7 +42,7 @@ void Anova::control(const ClimateCall &call) { esp_ble_gattc_write_char(this->parent_->get_gattc_if(), this->parent_->get_conn_id(), this->char_handle_, pkt->length, pkt->data, ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE); if (status) { - ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str().c_str(), status); + ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str(), status); } } if (call.get_target_temperature().has_value()) { @@ -51,7 +51,7 @@ void Anova::control(const ClimateCall &call) { esp_ble_gattc_write_char(this->parent_->get_gattc_if(), this->parent_->get_conn_id(), this->char_handle_, pkt->length, pkt->data, ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE); if (status) { - ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str().c_str(), status); + ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str(), status); } } } @@ -124,8 +124,7 @@ void Anova::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_ esp_ble_gattc_write_char(this->parent_->get_gattc_if(), this->parent_->get_conn_id(), this->char_handle_, pkt->length, pkt->data, ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE); if (status) { - ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str().c_str(), - status); + ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str(), status); } } } @@ -150,7 +149,7 @@ void Anova::update() { esp_ble_gattc_write_char(this->parent_->get_gattc_if(), this->parent_->get_conn_id(), this->char_handle_, pkt->length, pkt->data, ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE); if (status) { - ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str().c_str(), status); + ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str(), status); } this->current_request_++; } diff --git a/esphome/components/pvvx_mithermometer/display/pvvx_display.cpp b/esphome/components/pvvx_mithermometer/display/pvvx_display.cpp index b6916ad68f..8436633619 100644 --- a/esphome/components/pvvx_mithermometer/display/pvvx_display.cpp +++ b/esphome/components/pvvx_mithermometer/display/pvvx_display.cpp @@ -14,7 +14,7 @@ void PVVXDisplay::dump_config() { " Service UUID : %s\n" " Characteristic UUID : %s\n" " Auto clear : %s", - this->parent_->address_str().c_str(), this->service_uuid_.to_string().c_str(), + this->parent_->address_str(), this->service_uuid_.to_string().c_str(), this->char_uuid_.to_string().c_str(), YESNO(this->auto_clear_enabled_)); #ifdef USE_TIME ESP_LOGCONFIG(TAG, " Set time on connection: %s", YESNO(this->time_ != nullptr)); @@ -28,12 +28,12 @@ void PVVXDisplay::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t switch (event) { case ESP_GATTC_OPEN_EVT: if (param->open.status == ESP_GATT_OK) { - ESP_LOGV(TAG, "[%s] Connected successfully!", this->parent_->address_str().c_str()); + ESP_LOGV(TAG, "[%s] Connected successfully!", this->parent_->address_str()); this->delayed_disconnect_(); } break; case ESP_GATTC_DISCONNECT_EVT: - ESP_LOGV(TAG, "[%s] Disconnected", this->parent_->address_str().c_str()); + ESP_LOGV(TAG, "[%s] Disconnected", this->parent_->address_str()); this->connection_established_ = false; this->cancel_timeout("disconnect"); this->char_handle_ = 0; @@ -41,7 +41,7 @@ void PVVXDisplay::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t case ESP_GATTC_SEARCH_CMPL_EVT: { auto *chr = this->parent_->get_characteristic(this->service_uuid_, this->char_uuid_); if (chr == nullptr) { - ESP_LOGW(TAG, "[%s] Characteristic not found.", this->parent_->address_str().c_str()); + ESP_LOGW(TAG, "[%s] Characteristic not found.", this->parent_->address_str()); break; } this->connection_established_ = true; @@ -66,11 +66,11 @@ void PVVXDisplay::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb return; if (param->ble_security.auth_cmpl.success) { - ESP_LOGD(TAG, "[%s] Authentication successful, performing writes.", this->parent_->address_str().c_str()); + ESP_LOGD(TAG, "[%s] Authentication successful, performing writes.", this->parent_->address_str()); // Now that pairing is complete, perform the pending writes this->sync_time_and_display_(); } else { - ESP_LOGW(TAG, "[%s] Authentication failed.", this->parent_->address_str().c_str()); + ESP_LOGW(TAG, "[%s] Authentication failed.", this->parent_->address_str()); } break; } @@ -89,22 +89,20 @@ void PVVXDisplay::update() { void PVVXDisplay::display() { if (!this->parent_->enabled) { - ESP_LOGD(TAG, "[%s] BLE client not enabled. Init connection.", this->parent_->address_str().c_str()); + ESP_LOGD(TAG, "[%s] BLE client not enabled. Init connection.", this->parent_->address_str()); this->parent_->set_enabled(true); return; } if (!this->connection_established_) { - ESP_LOGW(TAG, "[%s] Not connected to BLE client. State update can not be written.", - this->parent_->address_str().c_str()); + ESP_LOGW(TAG, "[%s] Not connected to BLE client. State update can not be written.", this->parent_->address_str()); return; } if (!this->char_handle_) { - ESP_LOGW(TAG, "[%s] No ble handle to BLE client. State update can not be written.", - this->parent_->address_str().c_str()); + ESP_LOGW(TAG, "[%s] No ble handle to BLE client. State update can not be written.", this->parent_->address_str()); return; } ESP_LOGD(TAG, "[%s] Send to display: bignum %d, smallnum: %d, cfg: 0x%02x, validity period: %u.", - this->parent_->address_str().c_str(), this->bignum_, this->smallnum_, this->cfg_, this->validity_period_); + this->parent_->address_str(), this->bignum_, this->smallnum_, this->cfg_, this->validity_period_); uint8_t blk[8] = {}; blk[0] = 0x22; blk[1] = this->bignum_ & 0xff; @@ -128,16 +126,16 @@ void PVVXDisplay::setcfgbit_(uint8_t bit, bool value) { void PVVXDisplay::send_to_setup_char_(uint8_t *blk, size_t size) { if (!this->connection_established_) { - ESP_LOGW(TAG, "[%s] Not connected to BLE client.", this->parent_->address_str().c_str()); + ESP_LOGW(TAG, "[%s] Not connected to BLE client.", this->parent_->address_str()); return; } auto status = esp_ble_gattc_write_char(this->parent_->get_gattc_if(), this->parent_->get_conn_id(), this->char_handle_, size, blk, ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE); if (status) { - ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str().c_str(), status); + ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str(), status); } else { - ESP_LOGV(TAG, "[%s] send %u bytes", this->parent_->address_str().c_str(), size); + ESP_LOGV(TAG, "[%s] send %u bytes", this->parent_->address_str(), size); this->delayed_disconnect_(); } } @@ -161,21 +159,21 @@ void PVVXDisplay::sync_time_() { if (this->time_ == nullptr) return; if (!this->connection_established_) { - ESP_LOGW(TAG, "[%s] Not connected to BLE client. Time can not be synced.", this->parent_->address_str().c_str()); + ESP_LOGW(TAG, "[%s] Not connected to BLE client. Time can not be synced.", this->parent_->address_str()); return; } if (!this->char_handle_) { - ESP_LOGW(TAG, "[%s] No ble handle to BLE client. Time can not be synced.", this->parent_->address_str().c_str()); + ESP_LOGW(TAG, "[%s] No ble handle to BLE client. Time can not be synced.", this->parent_->address_str()); return; } auto time = this->time_->now(); if (!time.is_valid()) { - ESP_LOGW(TAG, "[%s] Time is not yet valid. Time can not be synced.", this->parent_->address_str().c_str()); + ESP_LOGW(TAG, "[%s] Time is not yet valid. Time can not be synced.", this->parent_->address_str()); return; } time.recalc_timestamp_utc(true); // calculate timestamp of local time uint8_t blk[5] = {}; - ESP_LOGD(TAG, "[%s] Sync time with timestamp %" PRIu64 ".", this->parent_->address_str().c_str(), time.timestamp); + ESP_LOGD(TAG, "[%s] Sync time with timestamp %" PRIu64 ".", this->parent_->address_str(), time.timestamp); blk[0] = 0x23; blk[1] = time.timestamp & 0xff; blk[2] = (time.timestamp >> 8) & 0xff; From e7d09c0f622289bfb617e01b89fd09fc3fc5332d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 23 Nov 2025 20:36:54 -0600 Subject: [PATCH 200/415] [esp32_ble] Store device name in flash to reduce RAM usage --- esphome/components/esp32_ble/ble.cpp | 4 ++-- esphome/components/esp32_ble/ble.h | 5 ++--- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/esphome/components/esp32_ble/ble.cpp b/esphome/components/esp32_ble/ble.cpp index d0bfb6f843..787b01295b 100644 --- a/esphome/components/esp32_ble/ble.cpp +++ b/esphome/components/esp32_ble/ble.cpp @@ -257,8 +257,8 @@ bool ESP32BLE::ble_setup_() { #endif std::string name; - if (this->name_.has_value()) { - name = this->name_.value(); + if (this->name_ != nullptr) { + name = this->name_; if (App.is_name_add_mac_suffix_enabled()) { // MAC address suffix length (last 6 characters of 12-char MAC address string) constexpr size_t mac_address_suffix_len = 6; diff --git a/esphome/components/esp32_ble/ble.h b/esphome/components/esp32_ble/ble.h index 2fb60bb822..55aec3dc0c 100644 --- a/esphome/components/esp32_ble/ble.h +++ b/esphome/components/esp32_ble/ble.h @@ -112,7 +112,7 @@ class ESP32BLE : public Component { void loop() override; void dump_config() override; float get_setup_priority() const override; - void set_name(const std::string &name) { this->name_ = name; } + void set_name(const char *name) { this->name_ = name; } #ifdef USE_ESP32_BLE_ADVERTISING void advertising_start(); @@ -191,8 +191,7 @@ class ESP32BLE : public Component { esphome::LockFreeQueue ble_events_; esphome::EventPool ble_event_pool_; - // optional (typically 16+ bytes on 32-bit, aligned to 4 bytes) - optional name_; + const char *name_{nullptr}; // 4-byte aligned members #ifdef USE_ESP32_BLE_ADVERTISING From 10cc0c3bffd52c6b2b1baa0767559e577383d6f6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 23 Nov 2025 20:43:51 -0600 Subject: [PATCH 201/415] overload --- esphome/core/helpers.cpp | 13 +++++++------ esphome/core/helpers.h | 9 +++++++++ 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/esphome/core/helpers.cpp b/esphome/core/helpers.cpp index 50af71649c..6d8f2f02ec 100644 --- a/esphome/core/helpers.cpp +++ b/esphome/core/helpers.cpp @@ -238,27 +238,28 @@ std::string str_sprintf(const char *fmt, ...) { // Maximum size for name with suffix: 120 (max friendly name) + 1 (separator) + 6 (MAC suffix) + 1 (null term) static constexpr size_t MAX_NAME_WITH_SUFFIX_SIZE = 128; -std::string make_name_with_suffix(const std::string &name, char sep, const char *suffix_ptr, size_t suffix_len) { +std::string make_name_with_suffix(const char *name, char sep, const char *suffix_ptr, size_t suffix_len) { char buffer[MAX_NAME_WITH_SUFFIX_SIZE]; - size_t name_len = name.size(); + size_t name_len = strlen(name); size_t total_len = name_len + 1 + suffix_len; // Silently truncate if needed: prioritize keeping the full suffix if (total_len >= MAX_NAME_WITH_SUFFIX_SIZE) { - // NOTE: This calculation could underflow if suffix_len >= MAX_NAME_WITH_SUFFIX_SIZE - 2, - // but this is safe because this helper is only called with small suffixes: - // MAC suffixes (6-12 bytes), ".local" (5 bytes), etc. name_len = MAX_NAME_WITH_SUFFIX_SIZE - suffix_len - 2; // -2 for separator and null terminator total_len = name_len + 1 + suffix_len; } - memcpy(buffer, name.c_str(), name_len); + memcpy(buffer, name, name_len); buffer[name_len] = sep; memcpy(buffer + name_len + 1, suffix_ptr, suffix_len); buffer[total_len] = '\0'; return std::string(buffer, total_len); } +std::string make_name_with_suffix(const std::string &name, char sep, const char *suffix_ptr, size_t suffix_len) { + return make_name_with_suffix(name.c_str(), sep, suffix_ptr, suffix_len); +} + // Parsing & formatting size_t parse_hex(const char *str, size_t length, uint8_t *data, size_t count) { diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index d8c1f4647e..810b486011 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -512,6 +512,15 @@ std::string __attribute__((format(printf, 1, 2))) str_sprintf(const char *fmt, . /// @return The concatenated string: name + sep + suffix std::string make_name_with_suffix(const std::string &name, char sep, const char *suffix_ptr, size_t suffix_len); +/// Optimized string concatenation: name + separator + suffix (const char* overload) +/// Uses a fixed stack buffer to avoid heap allocations. +/// @param name The base name (null-terminated string) +/// @param sep Single character separator +/// @param suffix_ptr Pointer to the suffix characters +/// @param suffix_len Length of the suffix +/// @return The concatenated string: name + sep + suffix +std::string make_name_with_suffix(const char *name, char sep, const char *suffix_ptr, size_t suffix_len); + ///@} /// @name Parsing & formatting From cd9323ce704a94d58e82b5d81bb086897b8e06f3 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 23 Nov 2025 20:45:31 -0600 Subject: [PATCH 202/415] overload --- esphome/components/esp32_ble/ble.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/esphome/components/esp32_ble/ble.cpp b/esphome/components/esp32_ble/ble.cpp index 787b01295b..b0683b9e65 100644 --- a/esphome/components/esp32_ble/ble.cpp +++ b/esphome/components/esp32_ble/ble.cpp @@ -258,13 +258,14 @@ bool ESP32BLE::ble_setup_() { std::string name; if (this->name_ != nullptr) { - name = this->name_; if (App.is_name_add_mac_suffix_enabled()) { // MAC address suffix length (last 6 characters of 12-char MAC address string) constexpr size_t mac_address_suffix_len = 6; const std::string mac_addr = get_mac_address(); const char *mac_suffix_ptr = mac_addr.c_str() + mac_address_suffix_len; - name = make_name_with_suffix(name, '-', mac_suffix_ptr, mac_address_suffix_len); + name = make_name_with_suffix(this->name_, '-', mac_suffix_ptr, mac_address_suffix_len); + } else { + name = this->name_; } } else { name = App.get_name(); From b432c056dc58454f21c2fcdf76fb2e8d2f52ddec Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 23 Nov 2025 20:48:27 -0600 Subject: [PATCH 203/415] [esp32_ble] Store device name in flash to reduce RAM usage --- esphome/components/esp32_ble/ble.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/esphome/components/esp32_ble/ble.cpp b/esphome/components/esp32_ble/ble.cpp index b0683b9e65..86c750e8f8 100644 --- a/esphome/components/esp32_ble/ble.cpp +++ b/esphome/components/esp32_ble/ble.cpp @@ -261,8 +261,9 @@ bool ESP32BLE::ble_setup_() { if (App.is_name_add_mac_suffix_enabled()) { // MAC address suffix length (last 6 characters of 12-char MAC address string) constexpr size_t mac_address_suffix_len = 6; - const std::string mac_addr = get_mac_address(); - const char *mac_suffix_ptr = mac_addr.c_str() + mac_address_suffix_len; + char mac_addr[13]; + get_mac_address_into_buffer(mac_addr); + const char *mac_suffix_ptr = mac_addr + mac_address_suffix_len; name = make_name_with_suffix(this->name_, '-', mac_suffix_ptr, mac_address_suffix_len); } else { name = this->name_; From 531af6a277929c77f1615e6c18ef10c9db2add2c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 23 Nov 2025 20:50:31 -0600 Subject: [PATCH 204/415] [esp32_ble] Store device name in flash to reduce RAM usage --- esphome/components/esp32_ble/ble.cpp | 2 +- esphome/core/helpers.cpp | 9 ++++++--- esphome/core/helpers.h | 6 ++++-- 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/esphome/components/esp32_ble/ble.cpp b/esphome/components/esp32_ble/ble.cpp index 86c750e8f8..226ff1952e 100644 --- a/esphome/components/esp32_ble/ble.cpp +++ b/esphome/components/esp32_ble/ble.cpp @@ -264,7 +264,7 @@ bool ESP32BLE::ble_setup_() { char mac_addr[13]; get_mac_address_into_buffer(mac_addr); const char *mac_suffix_ptr = mac_addr + mac_address_suffix_len; - name = make_name_with_suffix(this->name_, '-', mac_suffix_ptr, mac_address_suffix_len); + name = make_name_with_suffix(this->name_, strlen(this->name_), '-', mac_suffix_ptr, mac_address_suffix_len); } else { name = this->name_; } diff --git a/esphome/core/helpers.cpp b/esphome/core/helpers.cpp index 6d8f2f02ec..1f675563c7 100644 --- a/esphome/core/helpers.cpp +++ b/esphome/core/helpers.cpp @@ -238,13 +238,16 @@ std::string str_sprintf(const char *fmt, ...) { // Maximum size for name with suffix: 120 (max friendly name) + 1 (separator) + 6 (MAC suffix) + 1 (null term) static constexpr size_t MAX_NAME_WITH_SUFFIX_SIZE = 128; -std::string make_name_with_suffix(const char *name, char sep, const char *suffix_ptr, size_t suffix_len) { +std::string make_name_with_suffix(const char *name, size_t name_len, char sep, const char *suffix_ptr, + size_t suffix_len) { char buffer[MAX_NAME_WITH_SUFFIX_SIZE]; - size_t name_len = strlen(name); size_t total_len = name_len + 1 + suffix_len; // Silently truncate if needed: prioritize keeping the full suffix if (total_len >= MAX_NAME_WITH_SUFFIX_SIZE) { + // NOTE: This calculation could underflow if suffix_len >= MAX_NAME_WITH_SUFFIX_SIZE - 2, + // but this is safe because this helper is only called with small suffixes: + // MAC suffixes (6-12 bytes), ".local" (5 bytes), etc. name_len = MAX_NAME_WITH_SUFFIX_SIZE - suffix_len - 2; // -2 for separator and null terminator total_len = name_len + 1 + suffix_len; } @@ -257,7 +260,7 @@ std::string make_name_with_suffix(const char *name, char sep, const char *suffix } std::string make_name_with_suffix(const std::string &name, char sep, const char *suffix_ptr, size_t suffix_len) { - return make_name_with_suffix(name.c_str(), sep, suffix_ptr, suffix_len); + return make_name_with_suffix(name.c_str(), name.size(), sep, suffix_ptr, suffix_len); } // Parsing & formatting diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index 810b486011..a43c55e06b 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -514,12 +514,14 @@ std::string make_name_with_suffix(const std::string &name, char sep, const char /// Optimized string concatenation: name + separator + suffix (const char* overload) /// Uses a fixed stack buffer to avoid heap allocations. -/// @param name The base name (null-terminated string) +/// @param name The base name string +/// @param name_len Length of the name /// @param sep Single character separator /// @param suffix_ptr Pointer to the suffix characters /// @param suffix_len Length of the suffix /// @return The concatenated string: name + sep + suffix -std::string make_name_with_suffix(const char *name, char sep, const char *suffix_ptr, size_t suffix_len); +std::string make_name_with_suffix(const char *name, size_t name_len, char sep, const char *suffix_ptr, + size_t suffix_len); ///@} From 1e886b88851cfb47a9a2d6dffbe54b4eca570be8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 23 Nov 2025 20:56:27 -0600 Subject: [PATCH 205/415] [esp32_ble] Store device name in flash to reduce RAM usage --- esphome/components/esp32_ble/ble.cpp | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/esphome/components/esp32_ble/ble.cpp b/esphome/components/esp32_ble/ble.cpp index 226ff1952e..2e1aaadeb7 100644 --- a/esphome/components/esp32_ble/ble.cpp +++ b/esphome/components/esp32_ble/ble.cpp @@ -256,7 +256,9 @@ bool ESP32BLE::ble_setup_() { } #endif - std::string name; + const char *device_name; + std::string name_with_suffix; + if (this->name_ != nullptr) { if (App.is_name_add_mac_suffix_enabled()) { // MAC address suffix length (last 6 characters of 12-char MAC address string) @@ -264,23 +266,26 @@ bool ESP32BLE::ble_setup_() { char mac_addr[13]; get_mac_address_into_buffer(mac_addr); const char *mac_suffix_ptr = mac_addr + mac_address_suffix_len; - name = make_name_with_suffix(this->name_, strlen(this->name_), '-', mac_suffix_ptr, mac_address_suffix_len); + name_with_suffix = + make_name_with_suffix(this->name_, strlen(this->name_), '-', mac_suffix_ptr, mac_address_suffix_len); + device_name = name_with_suffix.c_str(); } else { - name = this->name_; + device_name = this->name_; } } else { - name = App.get_name(); - if (name.length() > 20) { + name_with_suffix = App.get_name(); + if (name_with_suffix.length() > 20) { if (App.is_name_add_mac_suffix_enabled()) { // Keep first 13 chars and last 7 chars (MAC suffix), remove middle - name.erase(13, name.length() - 20); + name_with_suffix.erase(13, name_with_suffix.length() - 20); } else { - name.resize(20); + name_with_suffix.resize(20); } } + device_name = name_with_suffix.c_str(); } - err = esp_ble_gap_set_device_name(name.c_str()); + err = esp_ble_gap_set_device_name(device_name); if (err != ESP_OK) { ESP_LOGE(TAG, "esp_ble_gap_set_device_name failed: %d", err); return false; From 268780dbeb7c6a0a40c68ebcb2b5d60335d35035 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 23 Nov 2025 21:13:27 -0600 Subject: [PATCH 206/415] [api] Use stack buffer for MAC address in Noise handshake --- esphome/components/api/api_frame_helper_noise.cpp | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/esphome/components/api/api_frame_helper_noise.cpp b/esphome/components/api/api_frame_helper_noise.cpp index 633b07a7fa..8bcec0f9f3 100644 --- a/esphome/components/api/api_frame_helper_noise.cpp +++ b/esphome/components/api/api_frame_helper_noise.cpp @@ -239,12 +239,13 @@ APIError APINoiseFrameHelper::state_action_() { } if (state_ == State::SERVER_HELLO) { // send server hello + constexpr size_t mac_len = 13; // 12 hex chars + null terminator const std::string &name = App.get_name(); - const std::string &mac = get_mac_address(); + char mac[mac_len]; + get_mac_address_into_buffer(mac); // Calculate positions and sizes size_t name_len = name.size() + 1; // including null terminator - size_t mac_len = mac.size() + 1; // including null terminator size_t name_offset = 1; size_t mac_offset = name_offset + name_len; size_t total_size = 1 + name_len + mac_len; @@ -257,7 +258,7 @@ APIError APINoiseFrameHelper::state_action_() { // node name, terminated by null byte std::memcpy(msg.get() + name_offset, name.c_str(), name_len); // node mac, terminated by null byte - std::memcpy(msg.get() + mac_offset, mac.c_str(), mac_len); + std::memcpy(msg.get() + mac_offset, mac, mac_len); aerr = write_frame_(msg.get(), total_size); if (aerr != APIError::OK) From 48f0e52f9d6194cc2220f61c0c314091251f5d19 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 23 Nov 2025 21:20:23 -0600 Subject: [PATCH 207/415] one more --- esphome/core/application.h | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/esphome/core/application.h b/esphome/core/application.h index dae44d8902..14e800342e 100644 --- a/esphome/core/application.h +++ b/esphome/core/application.h @@ -105,11 +105,13 @@ class Application { arch_init(); this->name_add_mac_suffix_ = name_add_mac_suffix; if (name_add_mac_suffix) { + // MAC address length: 12 hex chars + null terminator + constexpr size_t mac_address_len = 13; // MAC address suffix length (last 6 characters of 12-char MAC address string) constexpr size_t mac_address_suffix_len = 6; - const std::string mac_addr = get_mac_address(); - // Use pointer + offset to avoid substr() allocation - const char *mac_suffix_ptr = mac_addr.c_str() + mac_address_suffix_len; + char mac_addr[mac_address_len]; + get_mac_address_into_buffer(mac_addr); + const char *mac_suffix_ptr = mac_addr + mac_address_suffix_len; this->name_ = make_name_with_suffix(name, '-', mac_suffix_ptr, mac_address_suffix_len); if (!friendly_name.empty()) { this->friendly_name_ = make_name_with_suffix(friendly_name, ' ', mac_suffix_ptr, mac_address_suffix_len); From ff4940d3b59e307d4bace65a0849ab84ff04c5de Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 23 Nov 2025 21:21:02 -0600 Subject: [PATCH 208/415] one more --- esphome/components/esp32_ble/ble.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/esphome/components/esp32_ble/ble.cpp b/esphome/components/esp32_ble/ble.cpp index 2e1aaadeb7..a0ed9ee90c 100644 --- a/esphome/components/esp32_ble/ble.cpp +++ b/esphome/components/esp32_ble/ble.cpp @@ -261,9 +261,11 @@ bool ESP32BLE::ble_setup_() { if (this->name_ != nullptr) { if (App.is_name_add_mac_suffix_enabled()) { + // MAC address length: 12 hex chars + null terminator + constexpr size_t mac_address_len = 13; // MAC address suffix length (last 6 characters of 12-char MAC address string) constexpr size_t mac_address_suffix_len = 6; - char mac_addr[13]; + char mac_addr[mac_address_len]; get_mac_address_into_buffer(mac_addr); const char *mac_suffix_ptr = mac_addr + mac_address_suffix_len; name_with_suffix = From 2cd71bf273512b5fef55b51f444386b84a80bbe9 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 23 Nov 2025 21:22:26 -0600 Subject: [PATCH 209/415] one more --- esphome/components/esp32_ble/ble.h | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/esphome/components/esp32_ble/ble.h b/esphome/components/esp32_ble/ble.h index 55aec3dc0c..393ec2e911 100644 --- a/esphome/components/esp32_ble/ble.h +++ b/esphome/components/esp32_ble/ble.h @@ -191,12 +191,11 @@ class ESP32BLE : public Component { esphome::LockFreeQueue ble_events_; esphome::EventPool ble_event_pool_; - const char *name_{nullptr}; - // 4-byte aligned members #ifdef USE_ESP32_BLE_ADVERTISING BLEAdvertising *advertising_{}; // 4 bytes (pointer) #endif + const char *name_{nullptr}; // 4 bytes (pointer to string literal in flash) esp_ble_io_cap_t io_cap_{ESP_IO_CAP_NONE}; // 4 bytes (enum) uint32_t advertising_cycle_time_{}; // 4 bytes From 5750f7fccbcf22c1df62d363cd0adb7d173be55d Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Sun, 23 Nov 2025 22:25:24 -0500 Subject: [PATCH 210/415] [ci] Fix test grouping (#12067) --- tests/components/stts22h/common.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/components/stts22h/common.yaml b/tests/components/stts22h/common.yaml index 802afe2065..2e332f9276 100644 --- a/tests/components/stts22h/common.yaml +++ b/tests/components/stts22h/common.yaml @@ -1,4 +1,5 @@ sensor: - platform: stts22h + i2c_id: i2c_bus name: Temperature update_interval: 15s From fa299eed588d48f785b4782decf1fb322f32ff33 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 23 Nov 2025 21:45:28 -0600 Subject: [PATCH 211/415] [mdns] Store MAC address in fixed buffer to reduce RAM usage --- esphome/components/mdns/__init__.py | 10 ++++----- esphome/components/mdns/mdns_component.cpp | 2 +- esphome/components/mdns/mdns_component.h | 24 ++++++++++++++++++++-- esphome/components/mdns/mdns_esp32.cpp | 3 ++- esphome/components/mdns/mdns_esp8266.cpp | 3 ++- esphome/components/mdns/mdns_host.cpp | 1 + esphome/components/mdns/mdns_libretiny.cpp | 3 ++- esphome/components/mdns/mdns_rp2040.cpp | 3 ++- esphome/core/defines.h | 3 ++- 9 files changed, 39 insertions(+), 13 deletions(-) diff --git a/esphome/components/mdns/__init__.py b/esphome/components/mdns/__init__.py index 4776bef22f..49ba00e5a6 100644 --- a/esphome/components/mdns/__init__.py +++ b/esphome/components/mdns/__init__.py @@ -184,10 +184,8 @@ async def to_code(config): # Calculate compile-time dynamic TXT value count # Dynamic values are those that cannot be stored in flash at compile time + # Note: MAC address is now stored in a fixed char[13] buffer, not dynamic storage dynamic_txt_count = 0 - if "api" in CORE.config: - # Always: get_mac_address() - dynamic_txt_count += 1 # User-provided templatable TXT values (only lambdas, not static strings) dynamic_txt_count += sum( 1 @@ -196,8 +194,10 @@ async def to_code(config): if cg.is_template(txt_value) ) - # Ensure at least 1 to avoid zero-size array - cg.add_define("MDNS_DYNAMIC_TXT_COUNT", max(1, dynamic_txt_count)) + # Only add define if we actually need dynamic storage + if dynamic_txt_count > 0: + cg.add_define("USE_MDNS_DYNAMIC_TXT") + cg.add_define("MDNS_DYNAMIC_TXT_COUNT", dynamic_txt_count) # Enable storage if verbose logging is enabled (for dump_config) if get_logger_level() in ("VERBOSE", "VERY_VERBOSE"): diff --git a/esphome/components/mdns/mdns_component.cpp b/esphome/components/mdns/mdns_component.cpp index c81defd19f..208f4e9cd4 100644 --- a/esphome/components/mdns/mdns_component.cpp +++ b/esphome/components/mdns/mdns_component.cpp @@ -86,7 +86,7 @@ void MDNSComponent::compile_records_(StaticVectoradd_dynamic_txt_value(get_mac_address()))}); + txt_records.push_back({MDNS_STR(TXT_MAC), MDNS_STR(this->mac_address_)}); #ifdef USE_ESP8266 MDNS_STATIC_CONST_CHAR(PLATFORM_ESP8266, "ESP8266"); diff --git a/esphome/components/mdns/mdns_component.h b/esphome/components/mdns/mdns_component.h index 691c45b7df..ce4f0e6817 100644 --- a/esphome/components/mdns/mdns_component.h +++ b/esphome/components/mdns/mdns_component.h @@ -60,18 +60,38 @@ class MDNSComponent : public Component { void on_shutdown() override; +#ifdef USE_MDNS_DYNAMIC_TXT /// Add a dynamic TXT value and return pointer to it for use in MDNSTXTRecord const char *add_dynamic_txt_value(const std::string &value) { this->dynamic_txt_values_.push_back(value); return this->dynamic_txt_values_[this->dynamic_txt_values_.size() - 1].c_str(); } +#endif - /// Storage for runtime-generated TXT values (MAC address, user lambdas) + protected: + /// Common setup logic called by all platform-specific setup() implementations + void on_setup() { +#ifdef USE_API + // Populate MAC address buffer once during setup + get_mac_address_into_buffer(std::span(this->mac_address_)); +#endif + +#ifdef USE_MDNS_STORE_SERVICES + this->compile_records_(this->services_); +#endif + } + +#ifdef USE_MDNS_DYNAMIC_TXT + /// Storage for runtime-generated TXT values from user lambdas /// Pre-sized at compile time via MDNS_DYNAMIC_TXT_COUNT to avoid heap allocations. /// Static/compile-time values (version, board, etc.) are stored directly in flash and don't use this. StaticVector dynamic_txt_values_; +#endif - protected: +#ifdef USE_API + /// Fixed buffer for MAC address (populated once in setup()) + char mac_address_[13]; // 12 hex chars + null terminator +#endif #ifdef USE_MDNS_STORE_SERVICES StaticVector services_{}; #endif diff --git a/esphome/components/mdns/mdns_esp32.cpp b/esphome/components/mdns/mdns_esp32.cpp index 5547a2524b..b96abb1b3f 100644 --- a/esphome/components/mdns/mdns_esp32.cpp +++ b/esphome/components/mdns/mdns_esp32.cpp @@ -12,8 +12,9 @@ namespace esphome::mdns { static const char *const TAG = "mdns"; void MDNSComponent::setup() { + this->on_setup(); + #ifdef USE_MDNS_STORE_SERVICES - this->compile_records_(this->services_); const auto &services = this->services_; #else StaticVector services; diff --git a/esphome/components/mdns/mdns_esp8266.cpp b/esphome/components/mdns/mdns_esp8266.cpp index 06f905884c..3388d1d9ab 100644 --- a/esphome/components/mdns/mdns_esp8266.cpp +++ b/esphome/components/mdns/mdns_esp8266.cpp @@ -12,8 +12,9 @@ namespace esphome::mdns { void MDNSComponent::setup() { + this->on_setup(); + #ifdef USE_MDNS_STORE_SERVICES - this->compile_records_(this->services_); const auto &services = this->services_; #else StaticVector services; diff --git a/esphome/components/mdns/mdns_host.cpp b/esphome/components/mdns/mdns_host.cpp index 64b8c8f54b..c46e170181 100644 --- a/esphome/components/mdns/mdns_host.cpp +++ b/esphome/components/mdns/mdns_host.cpp @@ -9,6 +9,7 @@ namespace esphome::mdns { void MDNSComponent::setup() { + this->on_setup(); // Host platform doesn't have actual mDNS implementation } diff --git a/esphome/components/mdns/mdns_libretiny.cpp b/esphome/components/mdns/mdns_libretiny.cpp index a049fe2109..945510e051 100644 --- a/esphome/components/mdns/mdns_libretiny.cpp +++ b/esphome/components/mdns/mdns_libretiny.cpp @@ -12,8 +12,9 @@ namespace esphome::mdns { void MDNSComponent::setup() { + this->on_setup(); + #ifdef USE_MDNS_STORE_SERVICES - this->compile_records_(this->services_); const auto &services = this->services_; #else StaticVector services; diff --git a/esphome/components/mdns/mdns_rp2040.cpp b/esphome/components/mdns/mdns_rp2040.cpp index a102e0b6c3..3b7c2e8254 100644 --- a/esphome/components/mdns/mdns_rp2040.cpp +++ b/esphome/components/mdns/mdns_rp2040.cpp @@ -12,8 +12,9 @@ namespace esphome::mdns { void MDNSComponent::setup() { + this->on_setup(); + #ifdef USE_MDNS_STORE_SERVICES - this->compile_records_(this->services_); const auto &services = this->services_; #else StaticVector services; diff --git a/esphome/core/defines.h b/esphome/core/defines.h index 03362ce07a..4ef8bc2b6f 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -88,7 +88,8 @@ #define USE_MDNS #define USE_MDNS_STORE_SERVICES #define MDNS_SERVICE_COUNT 3 -#define MDNS_DYNAMIC_TXT_COUNT 3 +#define USE_MDNS_DYNAMIC_TXT +#define MDNS_DYNAMIC_TXT_COUNT 2 #define SNTP_SERVER_COUNT 3 #define USE_MEDIA_PLAYER #define USE_NEXTION_TFT_UPLOAD From 792a2b1ee124ec800fa0048b55d084e1ff56096a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 23 Nov 2025 21:52:02 -0600 Subject: [PATCH 212/415] tidy --- esphome/components/mdns/mdns_component.h | 2 +- esphome/components/mdns/mdns_esp32.cpp | 2 +- esphome/components/mdns/mdns_esp8266.cpp | 2 +- esphome/components/mdns/mdns_host.cpp | 2 +- esphome/components/mdns/mdns_libretiny.cpp | 2 +- esphome/components/mdns/mdns_rp2040.cpp | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/esphome/components/mdns/mdns_component.h b/esphome/components/mdns/mdns_component.h index ce4f0e6817..198d1f23ae 100644 --- a/esphome/components/mdns/mdns_component.h +++ b/esphome/components/mdns/mdns_component.h @@ -70,7 +70,7 @@ class MDNSComponent : public Component { protected: /// Common setup logic called by all platform-specific setup() implementations - void on_setup() { + void on_setup_() { #ifdef USE_API // Populate MAC address buffer once during setup get_mac_address_into_buffer(std::span(this->mac_address_)); diff --git a/esphome/components/mdns/mdns_esp32.cpp b/esphome/components/mdns/mdns_esp32.cpp index b96abb1b3f..1090ef53b5 100644 --- a/esphome/components/mdns/mdns_esp32.cpp +++ b/esphome/components/mdns/mdns_esp32.cpp @@ -12,7 +12,7 @@ namespace esphome::mdns { static const char *const TAG = "mdns"; void MDNSComponent::setup() { - this->on_setup(); + this->on_setup_(); #ifdef USE_MDNS_STORE_SERVICES const auto &services = this->services_; diff --git a/esphome/components/mdns/mdns_esp8266.cpp b/esphome/components/mdns/mdns_esp8266.cpp index 3388d1d9ab..14ba355f4a 100644 --- a/esphome/components/mdns/mdns_esp8266.cpp +++ b/esphome/components/mdns/mdns_esp8266.cpp @@ -12,7 +12,7 @@ namespace esphome::mdns { void MDNSComponent::setup() { - this->on_setup(); + this->on_setup_(); #ifdef USE_MDNS_STORE_SERVICES const auto &services = this->services_; diff --git a/esphome/components/mdns/mdns_host.cpp b/esphome/components/mdns/mdns_host.cpp index c46e170181..c154b44049 100644 --- a/esphome/components/mdns/mdns_host.cpp +++ b/esphome/components/mdns/mdns_host.cpp @@ -9,7 +9,7 @@ namespace esphome::mdns { void MDNSComponent::setup() { - this->on_setup(); + this->on_setup_(); // Host platform doesn't have actual mDNS implementation } diff --git a/esphome/components/mdns/mdns_libretiny.cpp b/esphome/components/mdns/mdns_libretiny.cpp index 945510e051..2ee162ac2e 100644 --- a/esphome/components/mdns/mdns_libretiny.cpp +++ b/esphome/components/mdns/mdns_libretiny.cpp @@ -12,7 +12,7 @@ namespace esphome::mdns { void MDNSComponent::setup() { - this->on_setup(); + this->on_setup_(); #ifdef USE_MDNS_STORE_SERVICES const auto &services = this->services_; diff --git a/esphome/components/mdns/mdns_rp2040.cpp b/esphome/components/mdns/mdns_rp2040.cpp index 3b7c2e8254..102e9eb3c2 100644 --- a/esphome/components/mdns/mdns_rp2040.cpp +++ b/esphome/components/mdns/mdns_rp2040.cpp @@ -12,7 +12,7 @@ namespace esphome::mdns { void MDNSComponent::setup() { - this->on_setup(); + this->on_setup_(); #ifdef USE_MDNS_STORE_SERVICES const auto &services = this->services_; From 60d687c2c6c9bb8961763958d1b0ad78fee2b772 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Sun, 23 Nov 2025 23:31:14 -0500 Subject: [PATCH 213/415] [esp32] Fix C2 builds (#12050) --- esphome/components/esp32/__init__.py | 6 ++++++ esphome/components/esp32/pre_build.py.script | 9 +++++++++ 2 files changed, 15 insertions(+) create mode 100644 esphome/components/esp32/pre_build.py.script diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index 6f577d2926..59c6029334 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -883,6 +883,12 @@ async def to_code(config): CORE.relative_internal_path(".espressif") ) + add_extra_script( + "pre", + "pre_build.py", + Path(__file__).parent / "pre_build.py.script", + ) + add_extra_script( "post", "post_build.py", diff --git a/esphome/components/esp32/pre_build.py.script b/esphome/components/esp32/pre_build.py.script new file mode 100644 index 0000000000..af12275a0b --- /dev/null +++ b/esphome/components/esp32/pre_build.py.script @@ -0,0 +1,9 @@ +Import("env") # noqa: F821 + +# Remove custom_sdkconfig from the board config as it causes +# pioarduino to enable some strange hybrid build mode that breaks IDF +board = env.BoardConfig() +if "espidf.custom_sdkconfig" in board: + del board._manifest["espidf"]["custom_sdkconfig"] + if not board._manifest["espidf"]: + del board._manifest["espidf"] From 39a4a0bf10842c23f9b1786424d3da52437d9996 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 23 Nov 2025 22:35:13 -0600 Subject: [PATCH 214/415] no dupe storage --- esphome/components/mdns/mdns_component.cpp | 6 ++++-- esphome/components/mdns/mdns_component.h | 18 +++++++++--------- esphome/components/mdns/mdns_esp32.cpp | 11 +++++++++-- esphome/components/mdns/mdns_esp8266.cpp | 11 +++++++++-- esphome/components/mdns/mdns_host.cpp | 9 ++++++++- esphome/components/mdns/mdns_libretiny.cpp | 11 +++++++++-- esphome/components/mdns/mdns_rp2040.cpp | 11 +++++++++-- esphome/core/helpers.cpp | 6 +++--- esphome/core/helpers.h | 15 +++++++++++---- 9 files changed, 71 insertions(+), 27 deletions(-) diff --git a/esphome/components/mdns/mdns_component.cpp b/esphome/components/mdns/mdns_component.cpp index 208f4e9cd4..109b1a75ad 100644 --- a/esphome/components/mdns/mdns_component.cpp +++ b/esphome/components/mdns/mdns_component.cpp @@ -35,7 +35,7 @@ MDNS_STATIC_CONST_CHAR(SERVICE_TCP, "_tcp"); // Wrap build-time defines into flash storage MDNS_STATIC_CONST_CHAR(VALUE_VERSION, ESPHOME_VERSION); -void MDNSComponent::compile_records_(StaticVector &services) { +void MDNSComponent::compile_records_(StaticVector &services, char *mac_address_buf) { // IMPORTANT: The #ifdef blocks below must match COMPONENTS_WITH_MDNS_SERVICES // in mdns/__init__.py. If you add a new service here, update both locations. @@ -86,7 +86,9 @@ void MDNSComponent::compile_records_(StaticVectormac_address_)}); + + // MAC address: passed as parameter from setup(), lives in caller's stack frame + txt_records.push_back({MDNS_STR(TXT_MAC), MDNS_STR(mac_address_buf)}); #ifdef USE_ESP8266 MDNS_STATIC_CONST_CHAR(PLATFORM_ESP8266, "ESP8266"); diff --git a/esphome/components/mdns/mdns_component.h b/esphome/components/mdns/mdns_component.h index 198d1f23ae..6d48d6382a 100644 --- a/esphome/components/mdns/mdns_component.h +++ b/esphome/components/mdns/mdns_component.h @@ -70,14 +70,14 @@ class MDNSComponent : public Component { protected: /// Common setup logic called by all platform-specific setup() implementations - void on_setup_() { -#ifdef USE_API - // Populate MAC address buffer once during setup - get_mac_address_into_buffer(std::span(this->mac_address_)); + void on_setup_(char *mac_address_buf) { +#if defined(USE_API) && defined(USE_MDNS_STORE_SERVICES) + // Populate MAC address buffer once during setup (only needed if storing services) + get_mac_address_into_buffer(std::span(this->mac_address_)); #endif #ifdef USE_MDNS_STORE_SERVICES - this->compile_records_(this->services_); + this->compile_records_(this->services_, this->mac_address_); #endif } @@ -88,14 +88,14 @@ class MDNSComponent : public Component { StaticVector dynamic_txt_values_; #endif -#ifdef USE_API - /// Fixed buffer for MAC address (populated once in setup()) - char mac_address_[13]; // 12 hex chars + null terminator +#if defined(USE_API) && defined(USE_MDNS_STORE_SERVICES) + /// Fixed buffer for MAC address (only needed when services are stored) + char mac_address_[MAC_ADDRESS_BUFFER_SIZE]; #endif #ifdef USE_MDNS_STORE_SERVICES StaticVector services_{}; #endif - void compile_records_(StaticVector &services); + void compile_records_(StaticVector &services, char *mac_address_buf); }; } // namespace esphome::mdns diff --git a/esphome/components/mdns/mdns_esp32.cpp b/esphome/components/mdns/mdns_esp32.cpp index 1090ef53b5..4dfb416249 100644 --- a/esphome/components/mdns/mdns_esp32.cpp +++ b/esphome/components/mdns/mdns_esp32.cpp @@ -12,13 +12,20 @@ namespace esphome::mdns { static const char *const TAG = "mdns"; void MDNSComponent::setup() { - this->on_setup_(); +#ifdef USE_API + char mac_address[MAC_ADDRESS_BUFFER_SIZE]; + get_mac_address_into_buffer(std::span(mac_address)); +#else + char *mac_address = nullptr; +#endif + + this->on_setup_(mac_address); #ifdef USE_MDNS_STORE_SERVICES const auto &services = this->services_; #else StaticVector services; - this->compile_records_(services); + this->compile_records_(services, mac_address); #endif esp_err_t err = mdns_init(); diff --git a/esphome/components/mdns/mdns_esp8266.cpp b/esphome/components/mdns/mdns_esp8266.cpp index 14ba355f4a..e70028d075 100644 --- a/esphome/components/mdns/mdns_esp8266.cpp +++ b/esphome/components/mdns/mdns_esp8266.cpp @@ -12,13 +12,20 @@ namespace esphome::mdns { void MDNSComponent::setup() { - this->on_setup_(); +#ifdef USE_API + char mac_address[MAC_ADDRESS_BUFFER_SIZE]; + get_mac_address_into_buffer(std::span(mac_address)); +#else + char *mac_address = nullptr; +#endif + + this->on_setup_(mac_address); #ifdef USE_MDNS_STORE_SERVICES const auto &services = this->services_; #else StaticVector services; - this->compile_records_(services); + this->compile_records_(services, mac_address); #endif MDNS.begin(App.get_name().c_str()); diff --git a/esphome/components/mdns/mdns_host.cpp b/esphome/components/mdns/mdns_host.cpp index c154b44049..1be0b721c5 100644 --- a/esphome/components/mdns/mdns_host.cpp +++ b/esphome/components/mdns/mdns_host.cpp @@ -9,7 +9,14 @@ namespace esphome::mdns { void MDNSComponent::setup() { - this->on_setup_(); +#ifdef USE_API + char mac_address[MAC_ADDRESS_BUFFER_SIZE]; + get_mac_address_into_buffer(std::span(mac_address)); +#else + char *mac_address = nullptr; +#endif + + this->on_setup_(mac_address); // Host platform doesn't have actual mDNS implementation } diff --git a/esphome/components/mdns/mdns_libretiny.cpp b/esphome/components/mdns/mdns_libretiny.cpp index 2ee162ac2e..04c6f8f89d 100644 --- a/esphome/components/mdns/mdns_libretiny.cpp +++ b/esphome/components/mdns/mdns_libretiny.cpp @@ -12,13 +12,20 @@ namespace esphome::mdns { void MDNSComponent::setup() { - this->on_setup_(); +#ifdef USE_API + char mac_address[MAC_ADDRESS_BUFFER_SIZE]; + get_mac_address_into_buffer(std::span(mac_address)); +#else + char *mac_address = nullptr; +#endif + + this->on_setup_(mac_address); #ifdef USE_MDNS_STORE_SERVICES const auto &services = this->services_; #else StaticVector services; - this->compile_records_(services); + this->compile_records_(services, mac_address); #endif MDNS.begin(App.get_name().c_str()); diff --git a/esphome/components/mdns/mdns_rp2040.cpp b/esphome/components/mdns/mdns_rp2040.cpp index 102e9eb3c2..10677b49ed 100644 --- a/esphome/components/mdns/mdns_rp2040.cpp +++ b/esphome/components/mdns/mdns_rp2040.cpp @@ -12,13 +12,20 @@ namespace esphome::mdns { void MDNSComponent::setup() { - this->on_setup_(); +#ifdef USE_API + char mac_address[MAC_ADDRESS_BUFFER_SIZE]; + get_mac_address_into_buffer(std::span(mac_address)); +#else + char *mac_address = nullptr; +#endif + + this->on_setup_(mac_address); #ifdef USE_MDNS_STORE_SERVICES const auto &services = this->services_; #else StaticVector services; - this->compile_records_(services); + this->compile_records_(services, mac_address); #endif MDNS.begin(App.get_name().c_str()); diff --git a/esphome/core/helpers.cpp b/esphome/core/helpers.cpp index 50af71649c..17bab6b041 100644 --- a/esphome/core/helpers.cpp +++ b/esphome/core/helpers.cpp @@ -638,17 +638,17 @@ std::string get_mac_address() { } std::string get_mac_address_pretty() { - char buf[18]; + char buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE]; return std::string(get_mac_address_pretty_into_buffer(buf)); } -void get_mac_address_into_buffer(std::span buf) { +void get_mac_address_into_buffer(std::span buf) { uint8_t mac[6]; get_mac_address_raw(mac); format_mac_addr_lower_no_sep(mac, buf.data()); } -const char *get_mac_address_pretty_into_buffer(std::span buf) { +const char *get_mac_address_pretty_into_buffer(std::span buf) { uint8_t mac[6]; get_mac_address_raw(mac); format_mac_addr_upper(mac, buf.data()); diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index d8c1f4647e..27e127b1de 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -1042,6 +1042,12 @@ class HighFrequencyLoopRequester { /// Get the device MAC address as raw bytes, written into the provided byte array (6 bytes). void get_mac_address_raw(uint8_t *mac); // NOLINT(readability-non-const-parameter) +/// Buffer size for MAC address in lowercase hex notation (12 hex chars + null terminator) +constexpr size_t MAC_ADDRESS_BUFFER_SIZE = 13; + +/// Buffer size for MAC address in colon-separated uppercase hex notation (17 chars + null terminator) +constexpr size_t MAC_ADDRESS_PRETTY_BUFFER_SIZE = 18; + /// Get the device MAC address as a string, in lowercase hex notation. std::string get_mac_address(); @@ -1049,13 +1055,14 @@ std::string get_mac_address(); std::string get_mac_address_pretty(); /// Get the device MAC address into the given buffer, in lowercase hex notation. -/// Assumes buffer length is 13 (12 digits for hexadecimal representation followed by null terminator). -void get_mac_address_into_buffer(std::span buf); +/// Assumes buffer length is MAC_ADDRESS_BUFFER_SIZE (12 digits for hexadecimal representation followed by null +/// terminator). +void get_mac_address_into_buffer(std::span buf); /// Get the device MAC address into the given buffer, in colon-separated uppercase hex notation. -/// Buffer must be exactly 18 bytes (17 for "XX:XX:XX:XX:XX:XX" + null terminator). +/// Buffer must be exactly MAC_ADDRESS_PRETTY_BUFFER_SIZE bytes (17 for "XX:XX:XX:XX:XX:XX" + null terminator). /// Returns pointer to the buffer for convenience. -const char *get_mac_address_pretty_into_buffer(std::span buf); +const char *get_mac_address_pretty_into_buffer(std::span buf); #ifdef USE_ESP32 /// Set the MAC address to use from the provided byte array (6 bytes). From b8719319feb67e0060492be4b7c5e5495b4ee111 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 23 Nov 2025 22:42:07 -0600 Subject: [PATCH 215/415] cleanup --- esphome/components/mdns/mdns_component.cpp | 2 +- esphome/components/mdns/mdns_component.h | 11 ++++++----- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/esphome/components/mdns/mdns_component.cpp b/esphome/components/mdns/mdns_component.cpp index 109b1a75ad..3232cd3cac 100644 --- a/esphome/components/mdns/mdns_component.cpp +++ b/esphome/components/mdns/mdns_component.cpp @@ -87,7 +87,7 @@ void MDNSComponent::compile_records_(StaticVector(this->mac_address_)); -#endif - #ifdef USE_MDNS_STORE_SERVICES +#ifdef USE_API + // Copy to member buffer for storage + std::memcpy(this->mac_address_, mac_address_buf, MAC_ADDRESS_BUFFER_SIZE); this->compile_records_(this->services_, this->mac_address_); +#else + this->compile_records_(this->services_, mac_address_buf); +#endif #endif } From deb8ffafa8898a1819e07c4e8f9aa19ecfe75e82 Mon Sep 17 00:00:00 2001 From: kbx81 Date: Mon, 24 Nov 2025 01:30:29 -0600 Subject: [PATCH 216/415] pico_w --- .../components/wifi/wifi_component_pico_w.cpp | 43 ++++++++++++++++++- 1 file changed, 42 insertions(+), 1 deletion(-) diff --git a/esphome/components/wifi/wifi_component_pico_w.cpp b/esphome/components/wifi/wifi_component_pico_w.cpp index 76f2250a5a..f120254924 100644 --- a/esphome/components/wifi/wifi_component_pico_w.cpp +++ b/esphome/components/wifi/wifi_component_pico_w.cpp @@ -1,4 +1,3 @@ - #include "wifi_component.h" #ifdef USE_WIFI @@ -20,6 +19,10 @@ namespace wifi { static const char *const TAG = "wifi_pico_w"; +// Track previous state for detecting changes +static bool s_sta_was_connected = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) +static bool s_sta_had_ip = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) + bool WiFiComponent::wifi_mode_(optional sta, optional ap) { if (sta.has_value()) { if (sta.value()) { @@ -219,11 +222,49 @@ network::IPAddress WiFiComponent::wifi_dns_ip_(int num) { } void WiFiComponent::wifi_loop_() { + // Handle scan completion if (this->state_ == WIFI_COMPONENT_STATE_STA_SCANNING && !cyw43_wifi_scan_active(&cyw43_state)) { this->scan_done_ = true; ESP_LOGV(TAG, "Scan done"); this->wifi_scan_state_callback_.call(this->scan_result_); } + + // Poll for connection state changes + // The arduino-pico WiFi library doesn't have event callbacks like ESP8266/ESP32, + // so we need to poll the link status to detect state changes + auto status = cyw43_tcpip_link_status(&cyw43_state, CYW43_ITF_STA); + bool is_connected = (status == CYW43_LINK_UP); + + // Detect connection state change + if (is_connected && !s_sta_was_connected) { + // Just connected + s_sta_was_connected = true; + ESP_LOGV(TAG, "Connected"); + this->wifi_connect_state_callback_.call(this->wifi_ssid(), this->wifi_bssid()); + } else if (!is_connected && s_sta_was_connected) { + // Just disconnected + s_sta_was_connected = false; + s_sta_had_ip = false; + ESP_LOGV(TAG, "Disconnected"); + this->wifi_connect_state_callback_.call("", bssid_t({0, 0, 0, 0, 0, 0})); + } + + // Detect IP address changes (only when connected) + if (is_connected) { + bool has_ip = false; + // Check for any IP address (IPv4 or IPv6) + for (auto addr : addrList) { + has_ip = true; + break; + } + + if (has_ip && !s_sta_had_ip) { + // Just got IP address + s_sta_had_ip = true; + ESP_LOGV(TAG, "Got IP address"); + this->ip_state_callback_.call(this->wifi_sta_ip_addresses(), this->get_dns_address(0), this->get_dns_address(1)); + } + } } void WiFiComponent::wifi_pre_setup_() {} From c1bc0358c3323bf693c374a0f82509755b49345d Mon Sep 17 00:00:00 2001 From: kbx81 Date: Mon, 24 Nov 2025 01:33:32 -0600 Subject: [PATCH 217/415] preen --- esphome/components/wifi/automation.h | 6 ++---- esphome/components/wifi/wifi_component.cpp | 6 ++---- esphome/components/wifi/wifi_component.h | 6 ++---- esphome/components/wifi/wifi_component_esp8266.cpp | 7 ++----- esphome/components/wifi/wifi_component_esp_idf.cpp | 7 ++----- esphome/components/wifi/wifi_component_pico_w.cpp | 7 ++----- 6 files changed, 12 insertions(+), 27 deletions(-) diff --git a/esphome/components/wifi/automation.h b/esphome/components/wifi/automation.h index 4c7545a445..dfeb2d8f25 100644 --- a/esphome/components/wifi/automation.h +++ b/esphome/components/wifi/automation.h @@ -4,8 +4,7 @@ #ifdef USE_WIFI #include "wifi_component.h" -namespace esphome { -namespace wifi { +namespace esphome::wifi { template class WiFiConnectedCondition : public Condition { public: @@ -108,6 +107,5 @@ template class WiFiConfigureAction : public Action, publi Trigger<> *error_trigger_{new Trigger<>()}; }; -} // namespace wifi -} // namespace esphome +} // namespace esphome::wifi #endif diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index 6f698bc2a8..d8287c3bb7 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -37,8 +37,7 @@ #include "esphome/components/esp32_improv/esp32_improv_component.h" #endif -namespace esphome { -namespace wifi { +namespace esphome::wifi { static const char *const TAG = "wifi"; @@ -1724,6 +1723,5 @@ bool WiFiScanResult::operator==(const WiFiScanResult &rhs) const { return this-> WiFiComponent *global_wifi_component; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) -} // namespace wifi -} // namespace esphome +} // namespace esphome::wifi #endif diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index b50ed210b5..93ede38503 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -49,8 +49,7 @@ extern "C" { #include #endif -namespace esphome { -namespace wifi { +namespace esphome::wifi { /// Sentinel value for RSSI when WiFi is not connected static constexpr int8_t WIFI_RSSI_DISCONNECTED = -127; @@ -569,6 +568,5 @@ class WiFiComponent : public Component { extern WiFiComponent *global_wifi_component; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) -} // namespace wifi -} // namespace esphome +} // namespace esphome::wifi #endif diff --git a/esphome/components/wifi/wifi_component_esp8266.cpp b/esphome/components/wifi/wifi_component_esp8266.cpp index e20db1ee97..28dc597620 100644 --- a/esphome/components/wifi/wifi_component_esp8266.cpp +++ b/esphome/components/wifi/wifi_component_esp8266.cpp @@ -38,8 +38,7 @@ extern "C" { #include "esphome/core/log.h" #include "esphome/core/util.h" -namespace esphome { -namespace wifi { +namespace esphome::wifi { static const char *const TAG = "wifi_esp8266"; @@ -892,8 +891,6 @@ network::IPAddress WiFiComponent::wifi_gateway_ip_() { return {(const ip_addr_t network::IPAddress WiFiComponent::wifi_dns_ip_(int num) { return {(const ip_addr_t *) WiFi.dnsIP(num)}; } void WiFiComponent::wifi_loop_() {} -} // namespace wifi -} // namespace esphome - +} // namespace esphome::wifi #endif #endif diff --git a/esphome/components/wifi/wifi_component_esp_idf.cpp b/esphome/components/wifi/wifi_component_esp_idf.cpp index e72f78a8c6..88052cb66d 100644 --- a/esphome/components/wifi/wifi_component_esp_idf.cpp +++ b/esphome/components/wifi/wifi_component_esp_idf.cpp @@ -41,8 +41,7 @@ #include "esphome/core/log.h" #include "esphome/core/util.h" -namespace esphome { -namespace wifi { +namespace esphome::wifi { static const char *const TAG = "wifi_esp32"; @@ -1098,8 +1097,6 @@ network::IPAddress WiFiComponent::wifi_dns_ip_(int num) { return network::IPAddress(dns_ip); } -} // namespace wifi -} // namespace esphome - +} // namespace esphome::wifi #endif // USE_ESP32 #endif diff --git a/esphome/components/wifi/wifi_component_pico_w.cpp b/esphome/components/wifi/wifi_component_pico_w.cpp index f120254924..2cc7bd2567 100644 --- a/esphome/components/wifi/wifi_component_pico_w.cpp +++ b/esphome/components/wifi/wifi_component_pico_w.cpp @@ -14,8 +14,7 @@ #include "esphome/core/log.h" #include "esphome/core/util.h" -namespace esphome { -namespace wifi { +namespace esphome::wifi { static const char *const TAG = "wifi_pico_w"; @@ -269,8 +268,6 @@ void WiFiComponent::wifi_loop_() { void WiFiComponent::wifi_pre_setup_() {} -} // namespace wifi -} // namespace esphome - +} // namespace esphome::wifi #endif #endif From 84f9cbca58029a301596e3e88970d3139f5fc2fd Mon Sep 17 00:00:00 2001 From: kbx81 Date: Mon, 24 Nov 2025 02:21:34 -0600 Subject: [PATCH 218/415] preen --- esphome/components/wifi_info/wifi_info_text_sensor.cpp | 6 ++---- esphome/components/wifi_info/wifi_info_text_sensor.h | 6 ++---- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/esphome/components/wifi_info/wifi_info_text_sensor.cpp b/esphome/components/wifi_info/wifi_info_text_sensor.cpp index a1d15cfead..bbf375970e 100644 --- a/esphome/components/wifi_info/wifi_info_text_sensor.cpp +++ b/esphome/components/wifi_info/wifi_info_text_sensor.cpp @@ -2,8 +2,7 @@ #ifdef USE_WIFI #include "esphome/core/log.h" -namespace esphome { -namespace wifi_info { +namespace esphome::wifi_info { static const char *const TAG = "wifi_info"; @@ -114,6 +113,5 @@ void BSSIDWiFiInfo::state_callback_(wifi::bssid_t bssid) { void MacAddressWifiInfo::dump_config() { LOG_TEXT_SENSOR("", "MAC Address", this); } -} // namespace wifi_info -} // namespace esphome +} // namespace esphome::wifi_info #endif diff --git a/esphome/components/wifi_info/wifi_info_text_sensor.h b/esphome/components/wifi_info/wifi_info_text_sensor.h index 65bf3da103..4daae00e9c 100644 --- a/esphome/components/wifi_info/wifi_info_text_sensor.h +++ b/esphome/components/wifi_info/wifi_info_text_sensor.h @@ -7,8 +7,7 @@ #ifdef USE_WIFI #include -namespace esphome { -namespace wifi_info { +namespace esphome::wifi_info { class IPAddressWiFiInfo : public Component, public text_sensor::TextSensor { public: @@ -67,6 +66,5 @@ class MacAddressWifiInfo : public Component, public text_sensor::TextSensor { void dump_config() override; }; -} // namespace wifi_info -} // namespace esphome +} // namespace esphome::wifi_info #endif From 5b23b471bba0d5046b780e3d965c5c72fa08fe69 Mon Sep 17 00:00:00 2001 From: kbx81 Date: Mon, 24 Nov 2025 02:34:46 -0600 Subject: [PATCH 219/415] preen --- esphome/components/wifi/wifi_component_libretiny.cpp | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/esphome/components/wifi/wifi_component_libretiny.cpp b/esphome/components/wifi/wifi_component_libretiny.cpp index 8f3a97675c..bf1d7a5408 100644 --- a/esphome/components/wifi/wifi_component_libretiny.cpp +++ b/esphome/components/wifi/wifi_component_libretiny.cpp @@ -15,8 +15,7 @@ #include "esphome/core/log.h" #include "esphome/core/util.h" -namespace esphome { -namespace wifi { +namespace esphome::wifi { static const char *const TAG = "wifi_lt"; @@ -497,8 +496,6 @@ network::IPAddress WiFiComponent::wifi_gateway_ip_() { return {WiFi.gatewayIP()} network::IPAddress WiFiComponent::wifi_dns_ip_(int num) { return {WiFi.dnsIP(num)}; } void WiFiComponent::wifi_loop_() {} -} // namespace wifi -} // namespace esphome - +} // namespace esphome::wifi #endif // USE_LIBRETINY #endif From b4b98505baed1fea37c1e2e5da11c2c8ea7d2e26 Mon Sep 17 00:00:00 2001 From: James <23900@qq.com> Date: Mon, 24 Nov 2025 23:05:02 +1300 Subject: [PATCH 220/415] [mipi_dsi] add guition JC4880P443 display (#12068) --- esphome/components/mipi_dsi/models/guition.py | 67 +++++++++++++++++++ 1 file changed, 67 insertions(+) diff --git a/esphome/components/mipi_dsi/models/guition.py b/esphome/components/mipi_dsi/models/guition.py index 5f7db4ebda..cd566633f9 100644 --- a/esphome/components/mipi_dsi/models/guition.py +++ b/esphome/components/mipi_dsi/models/guition.py @@ -35,3 +35,70 @@ DriverChip( (0x10, 0x0C), (0x11, 0x0C), (0x12, 0x0C), (0x13, 0x0C), (0x30, 0x00), ], ) + + +# JC4880P443 Driver Configuration (ST7701) +# Using parameters from esp_lcd_st7701.h and the working full init sequence +# ---------------------------------------------------------------------------------------------------------------------- +# * Resolution: 480x800 +# * PCLK Frequency: 34 MHz +# * DSI Lane Bit Rate: 500 Mbps (using 2-Lane DSI configuration) +# * Horizontal Timing (hsync_pulse_width=12, hsync_back_porch=42, hsync_front_porch=42) +# * Vertical Timing (vsync_pulse_width=2, vsync_back_porch=8, vsync_front_porch=166) +# ---------------------------------------------------------------------------------------------------------------------- +DriverChip( + "JC4880P443", + width=480, + height=800, + hsync_back_porch=42, + hsync_pulse_width=12, + hsync_front_porch=42, + vsync_back_porch=8, + vsync_pulse_width=2, + vsync_front_porch=166, + pclk_frequency="34MHz", + lane_bit_rate="500Mbps", + swap_xy=cv.UNDEFINED, + color_order="RGB", + reset_pin=5, + initsequence=[ + (0xFF, 0x77, 0x01, 0x00, 0x00, 0x13), + (0xEF, 0x08), + (0xFF, 0x77, 0x01, 0x00, 0x00, 0x10), + (0xC0, 0x63, 0x00), + (0xC1, 0x0D, 0x02), + (0xC2, 0x10, 0x08), + (0xCC, 0x10), + (0xB0, 0x80, 0x09, 0x53, 0x0C, 0xD0, 0x07, 0x0C, 0x09, 0x09, 0x28, 0x06, 0xD4, 0x13, 0x69, 0x2B, 0x71), + (0xB1, 0x80, 0x94, 0x5A, 0x10, 0xD3, 0x06, 0x0A, 0x08, 0x08, 0x25, 0x03, 0xD3, 0x12, 0x66, 0x6A, 0x0D), + (0xFF, 0x77, 0x01, 0x00, 0x00, 0x11), + (0xB0, 0x5D), + (0xB1, 0x58), + (0xB2, 0x87), + (0xB3, 0x80), + (0xB5, 0x4E), + (0xB7, 0x85), + (0xB8, 0x21), + (0xB9, 0x10, 0x1F), + (0xBB, 0x03), + (0xBC, 0x00), + (0xC1, 0x78), + (0xC2, 0x78), + (0xD0, 0x88), + (0xE0, 0x00, 0x3A, 0x02), + (0xE1, 0x04, 0xA0, 0x00, 0xA0, 0x05, 0xA0, 0x00, 0xA0, 0x00, 0x40, 0x40), + (0xE2, 0x30, 0x00, 0x40, 0x40, 0x32, 0xA0, 0x00, 0xA0, 0x00, 0xA0, 0x00, 0xA0, 0x00), + (0xE3, 0x00, 0x00, 0x33, 0x33), + (0xE4, 0x44, 0x44), + (0xE5, 0x09, 0x2E, 0xA0, 0xA0, 0x0B, 0x30, 0xA0, 0xA0, 0x05, 0x2A, 0xA0, 0xA0, 0x07, 0x2C, 0xA0, 0xA0), + (0xE6, 0x00, 0x00, 0x33, 0x33), + (0xE7, 0x44, 0x44), + (0xE8, 0x08, 0x2D, 0xA0, 0xA0, 0x0A, 0x2F, 0xA0, 0xA0, 0x04, 0x29, 0xA0, 0xA0, 0x06, 0x2B, 0xA0, 0xA0), + (0xEB, 0x00, 0x00, 0x4E, 0x4E, 0x00, 0x00, 0x00), + (0xEC, 0x08, 0x01), + (0xED, 0xB0, 0x2B, 0x98, 0xA4, 0x56, 0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0xF7, 0x65, 0x4A, 0x89, 0xB2, 0x0B), + (0xEF, 0x08, 0x08, 0x08, 0x45, 0x3F, 0x54), + (0xFF, 0x77, 0x01, 0x00, 0x00, 0x00), + ] +) +# fmt: on From fd0a4e9111c268324ac199a0343a5d8beb92bb9f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 24 Nov 2025 08:33:47 -0600 Subject: [PATCH 221/415] dry --- esphome/components/mdns/mdns_component.h | 29 ++++++++++++++++------ esphome/components/mdns/mdns_esp32.cpp | 22 +++------------- esphome/components/mdns/mdns_esp8266.cpp | 20 +++------------ esphome/components/mdns/mdns_host.cpp | 11 ++++---- esphome/components/mdns/mdns_libretiny.cpp | 20 +++------------ esphome/components/mdns/mdns_rp2040.cpp | 20 +++------------ 6 files changed, 41 insertions(+), 81 deletions(-) diff --git a/esphome/components/mdns/mdns_component.h b/esphome/components/mdns/mdns_component.h index d0659abdbf..f696cfff1c 100644 --- a/esphome/components/mdns/mdns_component.h +++ b/esphome/components/mdns/mdns_component.h @@ -69,17 +69,32 @@ class MDNSComponent : public Component { #endif protected: - /// Common setup logic called by all platform-specific setup() implementations - void on_setup_(char *mac_address_buf) { + /// Helper to set up services and MAC buffers, then call platform-specific registration + using PlatformRegisterFn = void (*)(MDNSComponent *, StaticVector &); + + void setup_buffers_and_register_(PlatformRegisterFn platform_register) { #ifdef USE_MDNS_STORE_SERVICES -#ifdef USE_API - // Copy to member buffer for storage - std::memcpy(this->mac_address_, mac_address_buf, MAC_ADDRESS_BUFFER_SIZE); - this->compile_records_(this->services_, this->mac_address_); + auto &services = this->services_; #else - this->compile_records_(this->services_, mac_address_buf); + StaticVector services_storage; + auto &services = services_storage; #endif + +#ifdef USE_API +#ifdef USE_MDNS_STORE_SERVICES + get_mac_address_into_buffer(this->mac_address_); + char *mac_ptr = this->mac_address_; +#else + char mac_address[MAC_ADDRESS_BUFFER_SIZE]; + get_mac_address_into_buffer(mac_address); + char *mac_ptr = mac_address; #endif +#else + char *mac_ptr = nullptr; +#endif + + this->compile_records_(services, mac_ptr); + platform_register(this, services); } #ifdef USE_MDNS_DYNAMIC_TXT diff --git a/esphome/components/mdns/mdns_esp32.cpp b/esphome/components/mdns/mdns_esp32.cpp index 4dfb416249..e6b43e59cb 100644 --- a/esphome/components/mdns/mdns_esp32.cpp +++ b/esphome/components/mdns/mdns_esp32.cpp @@ -11,27 +11,11 @@ namespace esphome::mdns { static const char *const TAG = "mdns"; -void MDNSComponent::setup() { -#ifdef USE_API - char mac_address[MAC_ADDRESS_BUFFER_SIZE]; - get_mac_address_into_buffer(std::span(mac_address)); -#else - char *mac_address = nullptr; -#endif - - this->on_setup_(mac_address); - -#ifdef USE_MDNS_STORE_SERVICES - const auto &services = this->services_; -#else - StaticVector services; - this->compile_records_(services, mac_address); -#endif - +static void register_esp32(MDNSComponent *comp, StaticVector &services) { esp_err_t err = mdns_init(); if (err != ESP_OK) { ESP_LOGW(TAG, "Init failed: %s", esp_err_to_name(err)); - this->mark_failed(); + comp->mark_failed(); return; } @@ -58,6 +42,8 @@ void MDNSComponent::setup() { } } +void MDNSComponent::setup() { this->setup_buffers_and_register_(register_esp32); } + void MDNSComponent::on_shutdown() { mdns_free(); delay(40); // Allow the mdns packets announcing service removal to be sent diff --git a/esphome/components/mdns/mdns_esp8266.cpp b/esphome/components/mdns/mdns_esp8266.cpp index e70028d075..dcbe5ebd52 100644 --- a/esphome/components/mdns/mdns_esp8266.cpp +++ b/esphome/components/mdns/mdns_esp8266.cpp @@ -11,23 +11,7 @@ namespace esphome::mdns { -void MDNSComponent::setup() { -#ifdef USE_API - char mac_address[MAC_ADDRESS_BUFFER_SIZE]; - get_mac_address_into_buffer(std::span(mac_address)); -#else - char *mac_address = nullptr; -#endif - - this->on_setup_(mac_address); - -#ifdef USE_MDNS_STORE_SERVICES - const auto &services = this->services_; -#else - StaticVector services; - this->compile_records_(services, mac_address); -#endif - +static void register_esp8266(MDNSComponent *, StaticVector &services) { MDNS.begin(App.get_name().c_str()); for (const auto &service : services) { @@ -52,6 +36,8 @@ void MDNSComponent::setup() { } } +void MDNSComponent::setup() { this->setup_buffers_and_register_(register_esp8266); } + void MDNSComponent::loop() { MDNS.update(); } void MDNSComponent::on_shutdown() { diff --git a/esphome/components/mdns/mdns_host.cpp b/esphome/components/mdns/mdns_host.cpp index 1be0b721c5..4d902319b8 100644 --- a/esphome/components/mdns/mdns_host.cpp +++ b/esphome/components/mdns/mdns_host.cpp @@ -9,14 +9,15 @@ namespace esphome::mdns { void MDNSComponent::setup() { +#ifdef USE_MDNS_STORE_SERVICES #ifdef USE_API - char mac_address[MAC_ADDRESS_BUFFER_SIZE]; - get_mac_address_into_buffer(std::span(mac_address)); + get_mac_address_into_buffer(this->mac_address_); + char *mac_ptr = this->mac_address_; #else - char *mac_address = nullptr; + char *mac_ptr = nullptr; +#endif + this->compile_records_(this->services_, mac_ptr); #endif - - this->on_setup_(mac_address); // Host platform doesn't have actual mDNS implementation } diff --git a/esphome/components/mdns/mdns_libretiny.cpp b/esphome/components/mdns/mdns_libretiny.cpp index 04c6f8f89d..986099fa1f 100644 --- a/esphome/components/mdns/mdns_libretiny.cpp +++ b/esphome/components/mdns/mdns_libretiny.cpp @@ -11,23 +11,7 @@ namespace esphome::mdns { -void MDNSComponent::setup() { -#ifdef USE_API - char mac_address[MAC_ADDRESS_BUFFER_SIZE]; - get_mac_address_into_buffer(std::span(mac_address)); -#else - char *mac_address = nullptr; -#endif - - this->on_setup_(mac_address); - -#ifdef USE_MDNS_STORE_SERVICES - const auto &services = this->services_; -#else - StaticVector services; - this->compile_records_(services, mac_address); -#endif - +static void register_libretiny(MDNSComponent *, StaticVector &services) { MDNS.begin(App.get_name().c_str()); for (const auto &service : services) { @@ -51,6 +35,8 @@ void MDNSComponent::setup() { } } +void MDNSComponent::setup() { this->setup_buffers_and_register_(register_libretiny); } + void MDNSComponent::on_shutdown() {} } // namespace esphome::mdns diff --git a/esphome/components/mdns/mdns_rp2040.cpp b/esphome/components/mdns/mdns_rp2040.cpp index 10677b49ed..e4a9b60cdb 100644 --- a/esphome/components/mdns/mdns_rp2040.cpp +++ b/esphome/components/mdns/mdns_rp2040.cpp @@ -11,23 +11,7 @@ namespace esphome::mdns { -void MDNSComponent::setup() { -#ifdef USE_API - char mac_address[MAC_ADDRESS_BUFFER_SIZE]; - get_mac_address_into_buffer(std::span(mac_address)); -#else - char *mac_address = nullptr; -#endif - - this->on_setup_(mac_address); - -#ifdef USE_MDNS_STORE_SERVICES - const auto &services = this->services_; -#else - StaticVector services; - this->compile_records_(services, mac_address); -#endif - +static void register_rp2040(MDNSComponent *, StaticVector &services) { MDNS.begin(App.get_name().c_str()); for (const auto &service : services) { @@ -51,6 +35,8 @@ void MDNSComponent::setup() { } } +void MDNSComponent::setup() { this->setup_buffers_and_register_(register_rp2040); } + void MDNSComponent::loop() { MDNS.update(); } void MDNSComponent::on_shutdown() { From 8607a0881d4f3d3b6fe064287710f6af3f3a16b6 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Mon, 24 Nov 2025 10:10:24 -0500 Subject: [PATCH 222/415] [core] Add support for passing yaml files to clean-all (#12039) --- esphome/__main__.py | 2 +- esphome/writer.py | 8 +++++++- tests/unit_tests/test_writer.py | 31 +++++++++++++++++++++++++++++++ 3 files changed, 39 insertions(+), 2 deletions(-) diff --git a/esphome/__main__.py b/esphome/__main__.py index b0c081a34f..f8fb678cb2 100644 --- a/esphome/__main__.py +++ b/esphome/__main__.py @@ -1319,7 +1319,7 @@ def parse_args(argv): "clean-all", help="Clean all build and platform files." ) parser_clean_all.add_argument( - "configuration", help="Your YAML configuration directory.", nargs="*" + "configuration", help="Your YAML file or configuration directory.", nargs="*" ) parser_dashboard = subparsers.add_parser( diff --git a/esphome/writer.py b/esphome/writer.py index b866a804b3..3124e9e12c 100644 --- a/esphome/writer.py +++ b/esphome/writer.py @@ -343,7 +343,13 @@ def clean_build(clear_pio_cache: bool = True): def clean_all(configuration: list[str]): import shutil - data_dirs = [Path(dir) / ".esphome" for dir in configuration] + data_dirs = [] + for config in configuration: + item = Path(config) + if item.is_file() and item.suffix in (".yaml", ".yml"): + data_dirs.append(item.parent / ".esphome") + else: + data_dirs.append(item / ".esphome") if is_ha_addon(): data_dirs.append(Path("/data")) if "ESPHOME_DATA_DIR" in os.environ: diff --git a/tests/unit_tests/test_writer.py b/tests/unit_tests/test_writer.py index a4490fbbc0..a2a358f4d3 100644 --- a/tests/unit_tests/test_writer.py +++ b/tests/unit_tests/test_writer.py @@ -737,6 +737,37 @@ def test_write_cpp_with_duplicate_markers( write_cpp("// New code") +@patch("esphome.writer.CORE") +def test_clean_all_with_yaml_file( + mock_core: MagicMock, + tmp_path: Path, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test clean_all with a .yaml file uses parent directory.""" + # Create config directory with yaml file + config_dir = tmp_path / "config" + config_dir.mkdir() + yaml_file = config_dir / "test.yaml" + yaml_file.write_text("esphome:\n name: test\n") + + build_dir = config_dir / ".esphome" + build_dir.mkdir() + (build_dir / "dummy.txt").write_text("x") + + from esphome.writer import clean_all + + with caplog.at_level("INFO"): + clean_all([str(yaml_file)]) + + # Verify .esphome directory still exists but contents cleaned + assert build_dir.exists() + assert not (build_dir / "dummy.txt").exists() + + # Verify logging mentions the build dir + assert "Cleaning" in caplog.text + assert str(build_dir) in caplog.text + + @patch("esphome.writer.CORE") def test_clean_all( mock_core: MagicMock, From 1f0a5e1eeab2d86031934a3f9c9e182458d60a5e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 24 Nov 2025 10:21:32 -0600 Subject: [PATCH 223/415] [logger] Reduce UART overhead on ESP32/ESP8266 and fix buffer truncation (#11927) --- esphome/components/logger/__init__.py | 2 + esphome/components/logger/logger.cpp | 21 ++++---- esphome/components/logger/logger.h | 53 +++++++++++++++++-- esphome/components/logger/logger_esp32.cpp | 32 ++++++----- esphome/components/logger/logger_esp8266.cpp | 5 +- esphome/components/logger/logger_host.cpp | 2 +- .../components/logger/logger_libretiny.cpp | 2 +- esphome/components/logger/logger_rp2040.cpp | 2 +- esphome/components/logger/logger_zephyr.cpp | 2 +- 9 files changed, 84 insertions(+), 37 deletions(-) diff --git a/esphome/components/logger/__init__.py b/esphome/components/logger/__init__.py index cf78e6ae63..39877030e9 100644 --- a/esphome/components/logger/__init__.py +++ b/esphome/components/logger/__init__.py @@ -365,8 +365,10 @@ async def to_code(config): if CORE.is_esp32: if config[CONF_HARDWARE_UART] == USB_CDC: add_idf_sdkconfig_option("CONFIG_ESP_CONSOLE_USB_CDC", True) + cg.add_define("USE_LOGGER_UART_SELECTION_USB_CDC") elif config[CONF_HARDWARE_UART] == USB_SERIAL_JTAG: add_idf_sdkconfig_option("CONFIG_ESP_CONSOLE_USB_SERIAL_JTAG", True) + cg.add_define("USE_LOGGER_UART_SELECTION_USB_SERIAL_JTAG") try: uart_selection(USB_SERIAL_JTAG) cg.add_define("USE_LOGGER_USB_SERIAL_JTAG") diff --git a/esphome/components/logger/logger.cpp b/esphome/components/logger/logger.cpp index 9a9bf89fe3..9803bf528c 100644 --- a/esphome/components/logger/logger.cpp +++ b/esphome/components/logger/logger.cpp @@ -65,7 +65,9 @@ void HOT Logger::log_vprintf_(uint8_t level, const char *tag, int line, const ch uint16_t buffer_at = 0; // Initialize buffer position this->format_log_to_buffer_with_terminator_(level, tag, line, format, args, console_buffer, &buffer_at, MAX_CONSOLE_LOG_MSG_SIZE); - this->write_msg_(console_buffer); + // Add newline if platform needs it (ESP32 doesn't add via write_msg_) + this->add_newline_to_buffer_if_needed_(console_buffer, &buffer_at, MAX_CONSOLE_LOG_MSG_SIZE); + this->write_msg_(console_buffer, buffer_at); } // Reset the recursion guard for this task @@ -131,18 +133,19 @@ void Logger::log_vprintf_(uint8_t level, const char *tag, int line, const __Flas // Save the offset before calling format_log_to_buffer_with_terminator_ // since it will increment tx_buffer_at_ to the end of the formatted string - uint32_t msg_start = this->tx_buffer_at_; + uint16_t msg_start = this->tx_buffer_at_; this->format_log_to_buffer_with_terminator_(level, tag, line, this->tx_buffer_, args, this->tx_buffer_, &this->tx_buffer_at_, this->tx_buffer_size_); - // Write to console and send callback starting at the msg_start - if (this->baud_rate_ > 0) { - this->write_msg_(this->tx_buffer_ + msg_start); - } - size_t msg_length = + uint16_t msg_length = this->tx_buffer_at_ - msg_start; // Don't subtract 1 - tx_buffer_at_ is already at the null terminator position + + // Callbacks get message first (before console write) this->log_callback_.call(level, tag, this->tx_buffer_ + msg_start, msg_length); + // Write to console starting at the msg_start + this->write_tx_buffer_to_console_(msg_start, &msg_length); + global_recursion_guard_ = false; } #endif // USE_STORE_LOG_STR_IN_FLASH @@ -209,9 +212,7 @@ void Logger::process_messages_() { // This ensures all log messages appear on the console in a clean, serialized manner // Note: Messages may appear slightly out of order due to async processing, but // this is preferred over corrupted/interleaved console output - if (this->baud_rate_ > 0) { - this->write_msg_(this->tx_buffer_); - } + this->write_tx_buffer_to_console_(); } } else { // No messages to process, disable loop if appropriate diff --git a/esphome/components/logger/logger.h b/esphome/components/logger/logger.h index dc8e06e0c9..8ba3dacacb 100644 --- a/esphome/components/logger/logger.h +++ b/esphome/components/logger/logger.h @@ -71,6 +71,17 @@ static constexpr uint16_t MAX_HEADER_SIZE = 128; // "0x" + 2 hex digits per byte + '\0' static constexpr size_t MAX_POINTER_REPRESENTATION = 2 + sizeof(void *) * 2 + 1; +// Platform-specific: does write_msg_ add its own newline? +// false: Caller must add newline to buffer before calling write_msg_ (ESP32, ESP8266) +// Allows single write call with newline included for efficiency +// true: write_msg_ adds newline itself via puts()/println() (other platforms) +// Newline should NOT be added to buffer +#if defined(USE_ESP32) || defined(USE_ESP8266) +static constexpr bool WRITE_MSG_ADDS_NEWLINE = false; +#else +static constexpr bool WRITE_MSG_ADDS_NEWLINE = true; +#endif + #if defined(USE_ESP32) || defined(USE_ESP8266) || defined(USE_RP2040) || defined(USE_LIBRETINY) || defined(USE_ZEPHYR) /** Enum for logging UART selection * @@ -173,7 +184,7 @@ class Logger : public Component { protected: void process_messages_(); - void write_msg_(const char *msg); + void write_msg_(const char *msg, size_t len); // Format a log message with printf-style arguments and write it to a buffer with header, footer, and null terminator // It's the caller's responsibility to initialize buffer_at (typically to 0) @@ -200,6 +211,35 @@ class Logger : public Component { } } + // Helper to add newline to buffer for platforms that need it + // Modifies buffer_at to include the newline + inline void HOT add_newline_to_buffer_if_needed_(char *buffer, uint16_t *buffer_at, uint16_t buffer_size) { + if constexpr (!WRITE_MSG_ADDS_NEWLINE) { + // Add newline - don't need to maintain null termination + // write_msg_ now always receives explicit length, so we can safely overwrite the null terminator + // This is safe because: + // 1. Callbacks already received the message (before we add newline) + // 2. write_msg_ receives the length explicitly (doesn't need null terminator) + if (*buffer_at < buffer_size) { + buffer[(*buffer_at)++] = '\n'; + } else if (buffer_size > 0) { + // Buffer was full - replace last char with newline to ensure it's visible + buffer[buffer_size - 1] = '\n'; + *buffer_at = buffer_size; + } + } + } + + // Helper to write tx_buffer_ to console if logging is enabled + // INTERNAL USE ONLY - offset > 0 requires length parameter to be non-null + inline void HOT write_tx_buffer_to_console_(uint16_t offset = 0, uint16_t *length = nullptr) { + if (this->baud_rate_ > 0) { + uint16_t *len_ptr = length ? length : &this->tx_buffer_at_; + this->add_newline_to_buffer_if_needed_(this->tx_buffer_ + offset, len_ptr, this->tx_buffer_size_ - offset); + this->write_msg_(this->tx_buffer_ + offset, *len_ptr); + } + } + // Helper to format and send a log message to both console and callbacks inline void HOT log_message_to_buffer_and_send_(uint8_t level, const char *tag, int line, const char *format, va_list args) { @@ -208,10 +248,11 @@ class Logger : public Component { this->format_log_to_buffer_with_terminator_(level, tag, line, format, args, this->tx_buffer_, &this->tx_buffer_at_, this->tx_buffer_size_); - if (this->baud_rate_ > 0) { - this->write_msg_(this->tx_buffer_); // If logging is enabled, write to console - } + // Callbacks get message WITHOUT newline (for API/MQTT/syslog) this->log_callback_.call(level, tag, this->tx_buffer_, this->tx_buffer_at_); + + // Console gets message WITH newline (if platform needs it) + this->write_tx_buffer_to_console_(); } // Write the body of the log message to the buffer @@ -425,7 +466,9 @@ class Logger : public Component { } // Update buffer_at with the formatted length (handle truncation) - uint16_t formatted_len = (ret >= remaining) ? remaining : ret; + // When vsnprintf truncates (ret >= remaining), it writes (remaining - 1) chars + null terminator + // When it doesn't truncate (ret < remaining), it writes ret chars + null terminator + uint16_t formatted_len = (ret >= remaining) ? (remaining - 1) : ret; *buffer_at += formatted_len; // Remove all trailing newlines right after formatting diff --git a/esphome/components/logger/logger_esp32.cpp b/esphome/components/logger/logger_esp32.cpp index 7fc79e6f54..32ef752462 100644 --- a/esphome/components/logger/logger_esp32.cpp +++ b/esphome/components/logger/logger_esp32.cpp @@ -121,25 +121,23 @@ void Logger::pre_setup() { ESP_LOGI(TAG, "Log initialized"); } -void HOT Logger::write_msg_(const char *msg) { - if ( -#if defined(USE_LOGGER_USB_CDC) && !defined(USE_LOGGER_USB_SERIAL_JTAG) - this->uart_ == UART_SELECTION_USB_CDC -#elif defined(USE_LOGGER_USB_SERIAL_JTAG) && !defined(USE_LOGGER_USB_CDC) - this->uart_ == UART_SELECTION_USB_SERIAL_JTAG -#elif defined(USE_LOGGER_USB_CDC) && defined(USE_LOGGER_USB_SERIAL_JTAG) - this->uart_ == UART_SELECTION_USB_CDC || this->uart_ == UART_SELECTION_USB_SERIAL_JTAG +void HOT Logger::write_msg_(const char *msg, size_t len) { + // Length is now always passed explicitly - no strlen() fallback needed + +#if defined(USE_LOGGER_UART_SELECTION_USB_CDC) || defined(USE_LOGGER_UART_SELECTION_USB_SERIAL_JTAG) + // USB CDC/JTAG - single write including newline (already in buffer) + // Use fwrite to stdout which goes through VFS to USB console + // + // Note: These defines indicate the user's YAML configuration choice (hardware_uart: USB_CDC/USB_SERIAL_JTAG). + // They are ONLY defined when the user explicitly selects USB as the logger output in their config. + // This is compile-time selection, not runtime detection - if USB is configured, it's always used. + // There is no fallback to regular UART if "USB isn't connected" - that's the user's responsibility + // to configure correctly for their hardware. This approach eliminates runtime overhead. + fwrite(msg, 1, len, stdout); #else - /* DISABLES CODE */ (false) // NOLINT + // Regular UART - single write including newline (already in buffer) + uart_write_bytes(this->uart_num_, msg, len); #endif - ) { - puts(msg); - } else { - // Use tx_buffer_at_ if msg points to tx_buffer_, otherwise fall back to strlen - size_t len = (msg == this->tx_buffer_) ? this->tx_buffer_at_ : strlen(msg); - uart_write_bytes(this->uart_num_, msg, len); - uart_write_bytes(this->uart_num_, "\n", 1); - } } const LogString *Logger::get_uart_selection_() { diff --git a/esphome/components/logger/logger_esp8266.cpp b/esphome/components/logger/logger_esp8266.cpp index 5063d88b92..0fc73b747a 100644 --- a/esphome/components/logger/logger_esp8266.cpp +++ b/esphome/components/logger/logger_esp8266.cpp @@ -33,7 +33,10 @@ void Logger::pre_setup() { ESP_LOGI(TAG, "Log initialized"); } -void HOT Logger::write_msg_(const char *msg) { this->hw_serial_->println(msg); } +void HOT Logger::write_msg_(const char *msg, size_t len) { + // Single write with newline already in buffer (added by caller) + this->hw_serial_->write(msg, len); +} const LogString *Logger::get_uart_selection_() { switch (this->uart_) { diff --git a/esphome/components/logger/logger_host.cpp b/esphome/components/logger/logger_host.cpp index 4abe92286a..c5e1e6f865 100644 --- a/esphome/components/logger/logger_host.cpp +++ b/esphome/components/logger/logger_host.cpp @@ -3,7 +3,7 @@ namespace esphome::logger { -void HOT Logger::write_msg_(const char *msg) { +void HOT Logger::write_msg_(const char *msg, size_t) { time_t rawtime; struct tm *timeinfo; char buffer[80]; diff --git a/esphome/components/logger/logger_libretiny.cpp b/esphome/components/logger/logger_libretiny.cpp index 3edfa74480..b8017b841d 100644 --- a/esphome/components/logger/logger_libretiny.cpp +++ b/esphome/components/logger/logger_libretiny.cpp @@ -49,7 +49,7 @@ void Logger::pre_setup() { ESP_LOGI(TAG, "Log initialized"); } -void HOT Logger::write_msg_(const char *msg) { this->hw_serial_->println(msg); } +void HOT Logger::write_msg_(const char *msg, size_t) { this->hw_serial_->println(msg); } const LogString *Logger::get_uart_selection_() { switch (this->uart_) { diff --git a/esphome/components/logger/logger_rp2040.cpp b/esphome/components/logger/logger_rp2040.cpp index 63727c2cda..4a8535c8e4 100644 --- a/esphome/components/logger/logger_rp2040.cpp +++ b/esphome/components/logger/logger_rp2040.cpp @@ -27,7 +27,7 @@ void Logger::pre_setup() { ESP_LOGI(TAG, "Log initialized"); } -void HOT Logger::write_msg_(const char *msg) { this->hw_serial_->println(msg); } +void HOT Logger::write_msg_(const char *msg, size_t) { this->hw_serial_->println(msg); } const LogString *Logger::get_uart_selection_() { switch (this->uart_) { diff --git a/esphome/components/logger/logger_zephyr.cpp b/esphome/components/logger/logger_zephyr.cpp index fb0c7dcca3..ec2ff3013c 100644 --- a/esphome/components/logger/logger_zephyr.cpp +++ b/esphome/components/logger/logger_zephyr.cpp @@ -62,7 +62,7 @@ void Logger::pre_setup() { ESP_LOGI(TAG, "Log initialized"); } -void HOT Logger::write_msg_(const char *msg) { +void HOT Logger::write_msg_(const char *msg, size_t) { #ifdef CONFIG_PRINTK printk("%s\n", msg); #endif From 056b4375ebe238250675081613dcb00389e3254e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 24 Nov 2025 10:21:47 -0600 Subject: [PATCH 224/415] [api] Reduce heap allocations in DeviceInfoResponse (#11952) --- esphome/components/api/api_connection.cpp | 12 ++++++++---- esphome/components/bluetooth_proxy/bluetooth_proxy.h | 6 ++---- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index c60680ae43..04221a237b 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -1451,8 +1451,11 @@ bool APIConnection::send_device_info_response(const DeviceInfoRequest &msg) { #ifdef USE_AREAS resp.set_suggested_area(StringRef(App.get_area())); #endif - // mac_address must store temporary string - will be valid during send_message call - std::string mac_address = get_mac_address_pretty(); + // Stack buffer for MAC address (XX:XX:XX:XX:XX:XX\0 = 18 bytes) + char mac_address[18]; + uint8_t mac[6]; + get_mac_address_raw(mac); + format_mac_addr_upper(mac, mac_address); resp.set_mac_address(StringRef(mac_address)); resp.set_esphome_version(ESPHOME_VERSION_REF); @@ -1493,8 +1496,9 @@ bool APIConnection::send_device_info_response(const DeviceInfoRequest &msg) { #endif #ifdef USE_BLUETOOTH_PROXY resp.bluetooth_proxy_feature_flags = bluetooth_proxy::global_bluetooth_proxy->get_feature_flags(); - // bt_mac must store temporary string - will be valid during send_message call - std::string bluetooth_mac = bluetooth_proxy::global_bluetooth_proxy->get_bluetooth_mac_address_pretty(); + // Stack buffer for Bluetooth MAC address (XX:XX:XX:XX:XX:XX\0 = 18 bytes) + char bluetooth_mac[18]; + bluetooth_proxy::global_bluetooth_proxy->get_bluetooth_mac_address_pretty(bluetooth_mac); resp.set_bluetooth_mac_address(StringRef(bluetooth_mac)); #endif #ifdef USE_VOICE_ASSISTANT diff --git a/esphome/components/bluetooth_proxy/bluetooth_proxy.h b/esphome/components/bluetooth_proxy/bluetooth_proxy.h index a5f0fbe32f..4de541fac2 100644 --- a/esphome/components/bluetooth_proxy/bluetooth_proxy.h +++ b/esphome/components/bluetooth_proxy/bluetooth_proxy.h @@ -130,11 +130,9 @@ class BluetoothProxy final : public esp32_ble_tracker::ESPBTDeviceListener, publ return flags; } - std::string get_bluetooth_mac_address_pretty() { + void get_bluetooth_mac_address_pretty(std::span output) { const uint8_t *mac = esp_bt_dev_get_address(); - char buf[18]; - format_mac_addr_upper(mac, buf); - return std::string(buf); + format_mac_addr_upper(mac, output.data()); } protected: From 426734beef724112b37037641c7ea7c20f044082 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 24 Nov 2025 10:22:01 -0600 Subject: [PATCH 225/415] [web_server_base] Replace shared_ptr with unique_ptr for AsyncWebServer (#11984) --- esphome/components/web_server_base/web_server_base.h | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/esphome/components/web_server_base/web_server_base.h b/esphome/components/web_server_base/web_server_base.h index 039a452d64..fbf0d00c06 100644 --- a/esphome/components/web_server_base/web_server_base.h +++ b/esphome/components/web_server_base/web_server_base.h @@ -111,7 +111,7 @@ class WebServerBase : public Component { this->initialized_++; return; } - this->server_ = std::make_shared(this->port_); + this->server_ = std::make_unique(this->port_); // All content is controlled and created by user - so allowing all origins is fine here. DefaultHeaders::Instance().addHeader("Access-Control-Allow-Origin", "*"); this->server_->begin(); @@ -127,7 +127,7 @@ class WebServerBase : public Component { this->server_ = nullptr; } } - std::shared_ptr get_server() const { return server_; } + AsyncWebServer *get_server() const { return this->server_.get(); } float get_setup_priority() const override; #ifdef USE_WEBSERVER_AUTH @@ -143,7 +143,7 @@ class WebServerBase : public Component { protected: int initialized_{0}; uint16_t port_{80}; - std::shared_ptr server_{nullptr}; + std::unique_ptr server_{nullptr}; std::vector handlers_; #ifdef USE_WEBSERVER_AUTH internal::Credentials credentials_; From 3c48e13c9f5cf391e775174748c7da77928e9b8d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 24 Nov 2025 10:22:13 -0600 Subject: [PATCH 226/415] [ethernet] Conditionally compile manual_ip to save 24 bytes RAM (#11832) --- esphome/components/ethernet/__init__.py | 1 + esphome/components/ethernet/ethernet_component.cpp | 12 ++++++++++-- esphome/components/ethernet/ethernet_component.h | 4 ++++ esphome/core/defines.h | 1 + 4 files changed, 16 insertions(+), 2 deletions(-) diff --git a/esphome/components/ethernet/__init__.py b/esphome/components/ethernet/__init__.py index 2f02d227d7..b4d67635c1 100644 --- a/esphome/components/ethernet/__init__.py +++ b/esphome/components/ethernet/__init__.py @@ -383,6 +383,7 @@ 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 diff --git a/esphome/components/ethernet/ethernet_component.cpp b/esphome/components/ethernet/ethernet_component.cpp index cad963b299..9a46aa2687 100644 --- a/esphome/components/ethernet/ethernet_component.cpp +++ b/esphome/components/ethernet/ethernet_component.cpp @@ -553,11 +553,14 @@ 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 { + } else +#endif + { info.ip.addr = 0; info.gw.addr = 0; info.netmask.addr = 0; @@ -578,6 +581,7 @@ 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()) { @@ -590,7 +594,9 @@ void EthernetComponent::start_connect_() { d = this->manual_ip_->dns2; dns_setserver(1, &d); } - } else { + } else +#endif + { err = esp_netif_dhcpc_start(this->eth_netif_); if (err != ESP_ERR_ESP_NETIF_DHCP_ALREADY_STARTED) { ESPHL_ERROR_CHECK(err, "DHCPC start error"); @@ -688,7 +694,9 @@ 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. diff --git a/esphome/components/ethernet/ethernet_component.h b/esphome/components/ethernet/ethernet_component.h index f1f0ac9cb8..bffed4dc4a 100644 --- a/esphome/components/ethernet/ethernet_component.h +++ b/esphome/components/ethernet/ethernet_component.h @@ -82,7 +82,9 @@ 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 &mac) { this->fixed_mac_ = mac; } network::IPAddresses get_ip_addresses(); @@ -137,7 +139,9 @@ class EthernetComponent : public Component { uint8_t mdc_pin_{23}; uint8_t mdio_pin_{18}; #endif +#ifdef USE_ETHERNET_MANUAL_IP optional manual_ip_{}; +#endif uint32_t connect_begin_; // Group all uint8_t types together (enums and bools) diff --git a/esphome/core/defines.h b/esphome/core/defines.h index 03362ce07a..5e7f51e04c 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -216,6 +216,7 @@ #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 From 737f23a0bdb8a1c09bb0dee9709d09cb4c5403c4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 24 Nov 2025 10:23:11 -0600 Subject: [PATCH 227/415] [light] Dynamically disable loop when idle to reduce CPU overhead (#11881) --- esphome/components/light/light_state.cpp | 24 ++++++++++++++++++++++++ esphome/components/light/light_state.h | 3 +++ 2 files changed, 27 insertions(+) diff --git a/esphome/components/light/light_state.cpp b/esphome/components/light/light_state.cpp index 36b2af03a5..9cde9077da 100644 --- a/esphome/components/light/light_state.cpp +++ b/esphome/components/light/light_state.cpp @@ -23,6 +23,9 @@ void LightState::setup() { effect->init_internal(this); } + // Start with loop disabled if idle - respects any effects/transitions set up during initialization + this->disable_loop_if_idle_(); + // When supported color temperature range is known, initialize color temperature setting within bounds. auto traits = this->get_traits(); float min_mireds = traits.get_min_mireds(); @@ -125,6 +128,9 @@ void LightState::loop() { this->is_transformer_active_ = false; this->transformer_ = nullptr; this->target_state_reached_callback_.call(); + + // Disable loop if idle (no transformer and no effect) + this->disable_loop_if_idle_(); } } @@ -132,6 +138,8 @@ void LightState::loop() { if (this->next_write_) { this->next_write_ = false; this->output_->write_state(this); + // Disable loop if idle (no transformer and no effect) + this->disable_loop_if_idle_(); } } @@ -227,6 +235,8 @@ void LightState::start_effect_(uint32_t effect_index) { this->active_effect_index_ = effect_index; auto *effect = this->get_active_effect_(); effect->start_internal(); + // Enable loop while effect is active + this->enable_loop(); } LightEffect *LightState::get_active_effect_() { if (this->active_effect_index_ == 0) { @@ -241,6 +251,8 @@ void LightState::stop_effect_() { effect->stop(); } this->active_effect_index_ = 0; + // Disable loop if idle (no effect and no transformer) + this->disable_loop_if_idle_(); } void LightState::start_transition_(const LightColorValues &target, uint32_t length, bool set_remote_values) { @@ -250,6 +262,8 @@ void LightState::start_transition_(const LightColorValues &target, uint32_t leng if (set_remote_values) { this->remote_values = target; } + // Enable loop while transition is active + this->enable_loop(); } void LightState::start_flash_(const LightColorValues &target, uint32_t length, bool set_remote_values) { @@ -265,6 +279,8 @@ void LightState::start_flash_(const LightColorValues &target, uint32_t length, b if (set_remote_values) { this->remote_values = target; }; + // Enable loop while flash is active + this->enable_loop(); } void LightState::set_immediately_(const LightColorValues &target, bool set_remote_values) { @@ -276,6 +292,14 @@ void LightState::set_immediately_(const LightColorValues &target, bool set_remot } this->output_->update_state(this); this->next_write_ = true; + this->enable_loop(); +} + +void LightState::disable_loop_if_idle_() { + // Only disable loop if both transformer and effect are inactive, and no pending writes + if (this->transformer_ == nullptr && this->get_active_effect_() == nullptr && !this->next_write_) { + this->disable_loop(); + } } void LightState::save_remote_values_() { diff --git a/esphome/components/light/light_state.h b/esphome/components/light/light_state.h index 06519cdc14..ad8922b46f 100644 --- a/esphome/components/light/light_state.h +++ b/esphome/components/light/light_state.h @@ -255,6 +255,9 @@ class LightState : public EntityBase, public Component { /// Internal method to save the current remote_values to the preferences void save_remote_values_(); + /// Disable loop if neither transformer nor effect is active + void disable_loop_if_idle_(); + /// Store the output to allow effects to have more access. LightOutput *output_; /// The currently active transformer for this light (transition/flash). From 04ec6a69995adb0e6277e7abf2e49c05486a0e37 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 24 Nov 2025 10:23:31 -0600 Subject: [PATCH 228/415] [api] Use stack buffer for MAC address in Noise handshake (#12072) --- esphome/components/api/api_frame_helper_noise.cpp | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/esphome/components/api/api_frame_helper_noise.cpp b/esphome/components/api/api_frame_helper_noise.cpp index 633b07a7fa..8bcec0f9f3 100644 --- a/esphome/components/api/api_frame_helper_noise.cpp +++ b/esphome/components/api/api_frame_helper_noise.cpp @@ -239,12 +239,13 @@ APIError APINoiseFrameHelper::state_action_() { } if (state_ == State::SERVER_HELLO) { // send server hello + constexpr size_t mac_len = 13; // 12 hex chars + null terminator const std::string &name = App.get_name(); - const std::string &mac = get_mac_address(); + char mac[mac_len]; + get_mac_address_into_buffer(mac); // Calculate positions and sizes size_t name_len = name.size() + 1; // including null terminator - size_t mac_len = mac.size() + 1; // including null terminator size_t name_offset = 1; size_t mac_offset = name_offset + name_len; size_t total_size = 1 + name_len + mac_len; @@ -257,7 +258,7 @@ APIError APINoiseFrameHelper::state_action_() { // node name, terminated by null byte std::memcpy(msg.get() + name_offset, name.c_str(), name_len); // node mac, terminated by null byte - std::memcpy(msg.get() + mac_offset, mac.c_str(), mac_len); + std::memcpy(msg.get() + mac_offset, mac, mac_len); aerr = write_frame_(msg.get(), total_size); if (aerr != APIError::OK) From 5248e0139dde498f9d689d139324f7b85e5ca143 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 24 Nov 2025 10:25:15 -0600 Subject: [PATCH 229/415] handle mark_failed case --- esphome/components/bh1900nux/bh1900nux.cpp | 2 +- esphome/components/bme280_base/bme280_base.cpp | 18 +++++++++--------- esphome/components/bmp280_base/bmp280_base.cpp | 16 ++++++++-------- esphome/components/epaper_spi/epaper_spi.cpp | 4 ++-- esphome/components/esp_ldo/esp_ldo.cpp | 4 ++-- .../gt911/touchscreen/gt911_touchscreen.cpp | 4 ++-- esphome/components/mipi_dsi/mipi_dsi.cpp | 18 +++++++++--------- esphome/components/mipi_dsi/mipi_dsi.h | 6 +++--- esphome/components/mipi_rgb/mipi_rgb.cpp | 10 +++++----- esphome/components/mipi_spi/mipi_spi.h | 2 +- esphome/components/qmp6988/qmp6988.cpp | 2 +- esphome/components/stts22h/stts22h.cpp | 12 ++++++------ esphome/core/component.h | 7 +++++++ 13 files changed, 56 insertions(+), 49 deletions(-) diff --git a/esphome/components/bh1900nux/bh1900nux.cpp b/esphome/components/bh1900nux/bh1900nux.cpp index 96a06adaa0..0e71bd6532 100644 --- a/esphome/components/bh1900nux/bh1900nux.cpp +++ b/esphome/components/bh1900nux/bh1900nux.cpp @@ -23,7 +23,7 @@ void BH1900NUXSensor::setup() { i2c::ErrorCode result_code = this->write_register(SOFT_RESET_REG, &SOFT_RESET_PAYLOAD, 1); // Software Reset to check communication if (result_code != i2c::ERROR_OK) { - this->mark_failed(ESP_LOG_MSG_COMM_FAIL); + this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL)); return; } } diff --git a/esphome/components/bme280_base/bme280_base.cpp b/esphome/components/bme280_base/bme280_base.cpp index 86b65d361d..c5d4c9c0a5 100644 --- a/esphome/components/bme280_base/bme280_base.cpp +++ b/esphome/components/bme280_base/bme280_base.cpp @@ -100,18 +100,18 @@ void BME280Component::setup() { if (!this->read_byte(BME280_REGISTER_CHIPID, &chip_id)) { this->error_code_ = COMMUNICATION_FAILED; - this->mark_failed(ESP_LOG_MSG_COMM_FAIL); + this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL)); return; } if (chip_id != 0x60) { this->error_code_ = WRONG_CHIP_ID; - this->mark_failed(BME280_ERROR_WRONG_CHIP_ID); + this->mark_failed(LOG_STR(BME280_ERROR_WRONG_CHIP_ID)); return; } // Send a soft reset. if (!this->write_byte(BME280_REGISTER_RESET, BME280_SOFT_RESET)) { - this->mark_failed("Reset failed"); + this->mark_failed(LOG_STR("Reset failed")); return; } // Wait until the NVM data has finished loading. @@ -120,12 +120,12 @@ void BME280Component::setup() { do { // NOLINT delay(2); if (!this->read_byte(BME280_REGISTER_STATUS, &status)) { - this->mark_failed("Error reading status register"); + this->mark_failed(LOG_STR("Error reading status register")); return; } } while ((status & BME280_STATUS_IM_UPDATE) && (--retry)); if (status & BME280_STATUS_IM_UPDATE) { - this->mark_failed("Timeout loading NVM"); + this->mark_failed(LOG_STR("Timeout loading NVM")); return; } @@ -153,26 +153,26 @@ void BME280Component::setup() { uint8_t humid_control_val = 0; if (!this->read_byte(BME280_REGISTER_CONTROLHUMID, &humid_control_val)) { - this->mark_failed("Read humidity control"); + this->mark_failed(LOG_STR("Read humidity control")); return; } humid_control_val &= ~0b00000111; humid_control_val |= this->humidity_oversampling_ & 0b111; if (!this->write_byte(BME280_REGISTER_CONTROLHUMID, humid_control_val)) { - this->mark_failed("Write humidity control"); + this->mark_failed(LOG_STR("Write humidity control")); return; } uint8_t config_register = 0; if (!this->read_byte(BME280_REGISTER_CONFIG, &config_register)) { - this->mark_failed("Read config"); + this->mark_failed(LOG_STR("Read config")); return; } config_register &= ~0b11111100; config_register |= 0b101 << 5; // 1000 ms standby time config_register |= (this->iir_filter_ & 0b111) << 2; if (!this->write_byte(BME280_REGISTER_CONFIG, config_register)) { - this->mark_failed("Write config"); + this->mark_failed(LOG_STR("Write config")); return; } } diff --git a/esphome/components/bmp280_base/bmp280_base.cpp b/esphome/components/bmp280_base/bmp280_base.cpp index 39654f5875..728eead521 100644 --- a/esphome/components/bmp280_base/bmp280_base.cpp +++ b/esphome/components/bmp280_base/bmp280_base.cpp @@ -65,23 +65,23 @@ void BMP280Component::setup() { // https://community.st.com/t5/stm32-mcus-products/issue-with-reading-bmp280-chip-id-using-spi/td-p/691855 if (!this->bmp_read_byte(0xD0, &chip_id)) { this->error_code_ = COMMUNICATION_FAILED; - this->mark_failed(ESP_LOG_MSG_COMM_FAIL); + this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL)); return; } if (!this->bmp_read_byte(0xD0, &chip_id)) { this->error_code_ = COMMUNICATION_FAILED; - this->mark_failed(ESP_LOG_MSG_COMM_FAIL); + this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL)); return; } if (chip_id != 0x58) { this->error_code_ = WRONG_CHIP_ID; - this->mark_failed(BMP280_ERROR_WRONG_CHIP_ID); + this->mark_failed(LOG_STR(BMP280_ERROR_WRONG_CHIP_ID)); return; } // Send a soft reset. if (!this->bmp_write_byte(BMP280_REGISTER_RESET, BMP280_SOFT_RESET)) { - this->mark_failed("Reset failed"); + this->mark_failed(LOG_STR("Reset failed")); return; } // Wait until the NVM data has finished loading. @@ -90,12 +90,12 @@ void BMP280Component::setup() { do { delay(2); if (!this->bmp_read_byte(BMP280_REGISTER_STATUS, &status)) { - this->mark_failed("Error reading status register"); + this->mark_failed(LOG_STR("Error reading status register")); return; } } while ((status & BMP280_STATUS_IM_UPDATE) && (--retry)); if (status & BMP280_STATUS_IM_UPDATE) { - this->mark_failed("Timeout loading NVM"); + this->mark_failed(LOG_STR("Timeout loading NVM")); return; } @@ -116,14 +116,14 @@ void BMP280Component::setup() { uint8_t config_register = 0; if (!this->bmp_read_byte(BMP280_REGISTER_CONFIG, &config_register)) { - this->mark_failed("Read config"); + this->mark_failed(LOG_STR("Read config")); return; } config_register &= ~0b11111100; config_register |= 0b000 << 5; // 0.5 ms standby time config_register |= (this->iir_filter_ & 0b111) << 2; if (!this->bmp_write_byte(BMP280_REGISTER_CONFIG, config_register)) { - this->mark_failed("Write config"); + this->mark_failed(LOG_STR("Write config")); return; } } diff --git a/esphome/components/epaper_spi/epaper_spi.cpp b/esphome/components/epaper_spi/epaper_spi.cpp index cf6a0b0c3d..39959cd743 100644 --- a/esphome/components/epaper_spi/epaper_spi.cpp +++ b/esphome/components/epaper_spi/epaper_spi.cpp @@ -22,7 +22,7 @@ const char *EPaperBase::epaper_state_to_string_() { void EPaperBase::setup() { if (!this->init_buffer_(this->buffer_length_)) { - this->mark_failed("Failed to initialise buffer"); + this->mark_failed(LOG_STR("Failed to initialise buffer")); return; } this->setup_pins_(); @@ -246,7 +246,7 @@ void EPaperBase::initialise_() { auto length = this->init_sequence_length_; while (index != length) { if (length - index < 2) { - this->mark_failed("Malformed init sequence"); + this->mark_failed(LOG_STR("Malformed init sequence")); return; } const uint8_t cmd = sequence[index++]; diff --git a/esphome/components/esp_ldo/esp_ldo.cpp b/esphome/components/esp_ldo/esp_ldo.cpp index eb04670d7e..5e3d4159f3 100644 --- a/esphome/components/esp_ldo/esp_ldo.cpp +++ b/esphome/components/esp_ldo/esp_ldo.cpp @@ -14,8 +14,8 @@ void EspLdo::setup() { config.flags.adjustable = this->adjustable_; auto err = esp_ldo_acquire_channel(&config, &this->handle_); if (err != ESP_OK) { - auto msg = str_sprintf("Failed to acquire LDO channel %d with voltage %fV", this->channel_, this->voltage_); - this->mark_failed(msg.c_str()); + ESP_LOGE(TAG, "Failed to acquire LDO channel %d with voltage %fV", this->channel_, this->voltage_); + this->mark_failed(LOG_STR("Failed to acquire LDO channel")); } else { ESP_LOGD(TAG, "Acquired LDO channel %d with voltage %fV", this->channel_, this->voltage_); } diff --git a/esphome/components/gt911/touchscreen/gt911_touchscreen.cpp b/esphome/components/gt911/touchscreen/gt911_touchscreen.cpp index 992a86cc21..b11880a042 100644 --- a/esphome/components/gt911/touchscreen/gt911_touchscreen.cpp +++ b/esphome/components/gt911/touchscreen/gt911_touchscreen.cpp @@ -79,13 +79,13 @@ void GT911Touchscreen::setup_internal_() { } } if (err != i2c::ERROR_OK) { - this->mark_failed("Calibration error"); + this->mark_failed(LOG_STR("Calibration error")); return; } } if (err != i2c::ERROR_OK) { - this->mark_failed(ESP_LOG_MSG_COMM_FAIL); + this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL)); return; } this->setup_done_ = true; diff --git a/esphome/components/mipi_dsi/mipi_dsi.cpp b/esphome/components/mipi_dsi/mipi_dsi.cpp index fbe251de41..ec36447f09 100644 --- a/esphome/components/mipi_dsi/mipi_dsi.cpp +++ b/esphome/components/mipi_dsi/mipi_dsi.cpp @@ -31,7 +31,7 @@ void MIPI_DSI::setup() { }; auto err = esp_lcd_new_dsi_bus(&bus_config, &this->bus_handle_); if (err != ESP_OK) { - this->smark_failed("lcd_new_dsi_bus failed", err); + this->smark_failed(LOG_STR("lcd_new_dsi_bus failed"), err); return; } esp_lcd_dbi_io_config_t dbi_config = { @@ -41,7 +41,7 @@ void MIPI_DSI::setup() { }; err = esp_lcd_new_panel_io_dbi(this->bus_handle_, &dbi_config, &this->io_handle_); if (err != ESP_OK) { - this->smark_failed("new_panel_io_dbi failed", err); + this->smark_failed(LOG_STR("new_panel_io_dbi failed"), err); return; } auto pixel_format = LCD_COLOR_PIXEL_FORMAT_RGB565; @@ -69,7 +69,7 @@ void MIPI_DSI::setup() { }}; err = esp_lcd_new_panel_dpi(this->bus_handle_, &dpi_config, &this->handle_); if (err != ESP_OK) { - this->smark_failed("esp_lcd_new_panel_dpi failed", err); + this->smark_failed(LOG_STR("esp_lcd_new_panel_dpi failed"), err); return; } if (this->reset_pin_ != nullptr) { @@ -86,14 +86,14 @@ void MIPI_DSI::setup() { auto when = millis() + 120; err = esp_lcd_panel_init(this->handle_); if (err != ESP_OK) { - this->smark_failed("esp_lcd_init failed", err); + this->smark_failed(LOG_STR("esp_lcd_init failed"), err); return; } size_t index = 0; auto &vec = this->init_sequence_; while (index != vec.size()) { if (vec.size() - index < 2) { - this->mark_failed("Malformed init sequence"); + this->mark_failed(LOG_STR("Malformed init sequence")); return; } uint8_t cmd = vec[index++]; @@ -104,7 +104,7 @@ void MIPI_DSI::setup() { } else { uint8_t num_args = x & 0x7F; if (vec.size() - index < num_args) { - this->mark_failed("Malformed init sequence"); + this->mark_failed(LOG_STR("Malformed init sequence")); return; } if (cmd == SLEEP_OUT) { @@ -119,7 +119,7 @@ void MIPI_DSI::setup() { format_hex_pretty(ptr, num_args, '.', false).c_str()); err = esp_lcd_panel_io_tx_param(this->io_handle_, cmd, ptr, num_args); if (err != ESP_OK) { - this->smark_failed("lcd_panel_io_tx_param failed", err); + this->smark_failed(LOG_STR("lcd_panel_io_tx_param failed"), err); return; } index += num_args; @@ -134,7 +134,7 @@ void MIPI_DSI::setup() { err = (esp_lcd_dpi_panel_register_event_callbacks(this->handle_, &cbs, this->io_lock_)); if (err != ESP_OK) { - this->smark_failed("Failed to register callbacks", err); + this->smark_failed(LOG_STR("Failed to register callbacks"), err); return; } @@ -216,7 +216,7 @@ bool MIPI_DSI::check_buffer_() { RAMAllocator allocator; this->buffer_ = allocator.allocate(this->height_ * this->width_ * bytes_per_pixel); if (this->buffer_ == nullptr) { - this->mark_failed("Could not allocate buffer for display!"); + this->mark_failed(LOG_STR("Could not allocate buffer for display!")); return false; } return true; diff --git a/esphome/components/mipi_dsi/mipi_dsi.h b/esphome/components/mipi_dsi/mipi_dsi.h index ce8a2a2236..1fe69c4fb0 100644 --- a/esphome/components/mipi_dsi/mipi_dsi.h +++ b/esphome/components/mipi_dsi/mipi_dsi.h @@ -62,9 +62,9 @@ class MIPI_DSI : public display::Display { void set_lanes(uint8_t lanes) { this->lanes_ = lanes; } void set_madctl(uint8_t madctl) { this->madctl_ = madctl; } - void smark_failed(const char *message, esp_err_t err) { - auto str = str_sprintf("Setup failed: %s: %s", message, esp_err_to_name(err)); - this->mark_failed(str.c_str()); + void smark_failed(const LogString *message, esp_err_t err) { + ESP_LOGE("mipi_dsi", "%s: %s", LOG_STR_ARG(message), esp_err_to_name(err)); + this->mark_failed(message); } void update() override; diff --git a/esphome/components/mipi_rgb/mipi_rgb.cpp b/esphome/components/mipi_rgb/mipi_rgb.cpp index 080fb08c09..74eedae4f4 100644 --- a/esphome/components/mipi_rgb/mipi_rgb.cpp +++ b/esphome/components/mipi_rgb/mipi_rgb.cpp @@ -73,7 +73,7 @@ void MipiRgbSpi::write_init_sequence_() { auto &vec = this->init_sequence_; while (index != vec.size()) { if (vec.size() - index < 2) { - this->mark_failed("Malformed init sequence"); + this->mark_failed(LOG_STR("Malformed init sequence")); return; } uint8_t cmd = vec[index++]; @@ -84,7 +84,7 @@ void MipiRgbSpi::write_init_sequence_() { } else { uint8_t num_args = x & 0x7F; if (vec.size() - index < num_args) { - this->mark_failed("Malformed init sequence"); + this->mark_failed(LOG_STR("Malformed init sequence")); return; } if (cmd == SLEEP_OUT) { @@ -164,8 +164,8 @@ void MipiRgb::common_setup_() { if (err == ESP_OK) err = esp_lcd_panel_init(this->handle_); if (err != ESP_OK) { - auto msg = str_sprintf("lcd setup failed: %s", esp_err_to_name(err)); - this->mark_failed(msg.c_str()); + ESP_LOGE(TAG, "lcd setup failed: %s", esp_err_to_name(err)); + this->mark_failed(LOG_STR("lcd setup failed")); } ESP_LOGCONFIG(TAG, "MipiRgb setup complete"); } @@ -249,7 +249,7 @@ bool MipiRgb::check_buffer_() { RAMAllocator allocator; this->buffer_ = allocator.allocate(this->height_ * this->width_); if (this->buffer_ == nullptr) { - this->mark_failed("Could not allocate buffer for display!"); + this->mark_failed(LOG_STR("Could not allocate buffer for display!")); return false; } return true; diff --git a/esphome/components/mipi_spi/mipi_spi.h b/esphome/components/mipi_spi/mipi_spi.h index 7e597d1c61..1953aef035 100644 --- a/esphome/components/mipi_spi/mipi_spi.h +++ b/esphome/components/mipi_spi/mipi_spi.h @@ -478,7 +478,7 @@ class MipiSpiBuffer : public MipiSpi allocator{}; this->buffer_ = allocator.allocate(BUFFER_WIDTH * BUFFER_HEIGHT / FRACTION); if (this->buffer_ == nullptr) { - this->mark_failed("Buffer allocation failed"); + this->mark_failed(LOG_STR("Buffer allocation failed")); } } diff --git a/esphome/components/qmp6988/qmp6988.cpp b/esphome/components/qmp6988/qmp6988.cpp index 61fde186d7..57f54b6432 100644 --- a/esphome/components/qmp6988/qmp6988.cpp +++ b/esphome/components/qmp6988/qmp6988.cpp @@ -310,7 +310,7 @@ void QMP6988Component::calculate_pressure_() { void QMP6988Component::setup() { if (!this->device_check_()) { - this->mark_failed(ESP_LOG_MSG_COMM_FAIL); + this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL)); return; } diff --git a/esphome/components/stts22h/stts22h.cpp b/esphome/components/stts22h/stts22h.cpp index 614dc1da8b..2b2559c843 100644 --- a/esphome/components/stts22h/stts22h.cpp +++ b/esphome/components/stts22h/stts22h.cpp @@ -21,7 +21,7 @@ static const float SENSOR_SCALE = 0.01f; // Sensor resolution in degrees Celsiu void STTS22HComponent::setup() { // Check if device is a STTS22H if (!this->is_stts22h_sensor_()) { - this->mark_failed("Device is not a STTS22H sensor"); + this->mark_failed(LOG_STR("Device is not a STTS22H sensor")); return; } @@ -61,12 +61,12 @@ float STTS22HComponent::read_temperature_() { bool STTS22HComponent::is_stts22h_sensor_() { uint8_t whoami_value; if (this->read_register(WHOAMI_REG, &whoami_value, 1) != i2c::NO_ERROR) { - this->mark_failed(ESP_LOG_MSG_COMM_FAIL); + this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL)); return false; } if (whoami_value != WHOAMI_STTS22H_IDENTIFICATION) { - this->mark_failed("Unexpected WHOAMI identifier. Sensor is not a STTS22H"); + this->mark_failed(LOG_STR("Unexpected WHOAMI identifier. Sensor is not a STTS22H")); return false; } @@ -77,7 +77,7 @@ void STTS22HComponent::initialize_sensor_() { // Read current CTRL_REG configuration uint8_t ctrl_value; if (this->read_register(CTRL_REG, &ctrl_value, 1) != i2c::NO_ERROR) { - this->mark_failed(ESP_LOG_MSG_COMM_FAIL); + this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL)); return; } @@ -86,14 +86,14 @@ void STTS22HComponent::initialize_sensor_() { // FREERUN bit must be cleared (see sensor documentation) ctrl_value &= ~FREERUN_CTRL_ENABLE_FLAG; // Clear FREERUN bit if (this->write_register(CTRL_REG, &ctrl_value, 1) != i2c::NO_ERROR) { - this->mark_failed(ESP_LOG_MSG_COMM_FAIL); + this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL)); return; } // Enable LOW ODR mode and ADD_INC ctrl_value |= LOW_ODR_CTRL_ENABLE_FLAG | ADD_INC_ENABLE_FLAG; // Set LOW ODR bit and ADD_INC bit if (this->write_register(CTRL_REG, &ctrl_value, 1) != i2c::NO_ERROR) { - this->mark_failed(ESP_LOG_MSG_COMM_FAIL); + this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL)); return; } } diff --git a/esphome/core/component.h b/esphome/core/component.h index 3415114992..63d758d25f 100644 --- a/esphome/core/component.h +++ b/esphome/core/component.h @@ -158,11 +158,18 @@ class Component { */ virtual void mark_failed(); + // Remove before 2026.6.0 + ESPDEPRECATED("Use mark_failed(LOG_STR(message)) instead. Will stop working in 2026.6.0", "2025.12.0") void mark_failed(const char *message) { this->status_set_error(message); this->mark_failed(); } + void mark_failed(const LogString *message) { + this->status_set_error(message); + this->mark_failed(); + } + /** Disable this component's loop. The loop() method will no longer be called. * * This is useful for components that only need to run for a certain period of time From 780fe37a1385817c30f62c79b458bd42678370cd Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 24 Nov 2025 10:27:53 -0600 Subject: [PATCH 230/415] fix mipi_dsi implementation --- esphome/components/mipi_dsi/mipi_dsi.cpp | 6 ++++++ esphome/components/mipi_dsi/mipi_dsi.h | 5 +---- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/esphome/components/mipi_dsi/mipi_dsi.cpp b/esphome/components/mipi_dsi/mipi_dsi.cpp index ec36447f09..cae8647398 100644 --- a/esphome/components/mipi_dsi/mipi_dsi.cpp +++ b/esphome/components/mipi_dsi/mipi_dsi.cpp @@ -11,6 +11,12 @@ static bool notify_refresh_ready(esp_lcd_panel_handle_t panel, esp_lcd_dpi_panel xSemaphoreGiveFromISR(sem, &need_yield); return (need_yield == pdTRUE); } + +void MIPI_DSI::smark_failed(const LogString *message, esp_err_t err) { + ESP_LOGE(TAG, "%s: %s", LOG_STR_ARG(message), esp_err_to_name(err)); + this->mark_failed(message); +} + void MIPI_DSI::setup() { ESP_LOGCONFIG(TAG, "Running Setup"); diff --git a/esphome/components/mipi_dsi/mipi_dsi.h b/esphome/components/mipi_dsi/mipi_dsi.h index 1fe69c4fb0..1cffe3b178 100644 --- a/esphome/components/mipi_dsi/mipi_dsi.h +++ b/esphome/components/mipi_dsi/mipi_dsi.h @@ -62,10 +62,7 @@ class MIPI_DSI : public display::Display { void set_lanes(uint8_t lanes) { this->lanes_ = lanes; } void set_madctl(uint8_t madctl) { this->madctl_ = madctl; } - void smark_failed(const LogString *message, esp_err_t err) { - ESP_LOGE("mipi_dsi", "%s: %s", LOG_STR_ARG(message), esp_err_to_name(err)); - this->mark_failed(message); - } + void smark_failed(const LogString *message, esp_err_t err); void update() override; From 06815fe177e5980ef049ec859c3fc8d26a28290d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 24 Nov 2025 10:41:24 -0600 Subject: [PATCH 231/415] [script][wait_until] Fix FIFO ordering and reentrancy bugs (#12049) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- esphome/components/script/script.h | 20 +-- esphome/core/base_automation.h | 9 +- .../fixtures/script_delay_with_params.yaml | 131 ++++++++++++++++++ .../fixtures/wait_until_fifo_ordering.yaml | 82 +++++++++++ tests/integration/test_script_delay_params.py | 121 ++++++++++++++++ tests/integration/test_wait_until_ordering.py | 90 ++++++++++++ 6 files changed, 441 insertions(+), 12 deletions(-) create mode 100644 tests/integration/fixtures/script_delay_with_params.yaml create mode 100644 tests/integration/fixtures/wait_until_fifo_ordering.yaml create mode 100644 tests/integration/test_script_delay_params.py create mode 100644 tests/integration/test_wait_until_ordering.py diff --git a/esphome/components/script/script.h b/esphome/components/script/script.h index 51cece01e4..d60ed657f7 100644 --- a/esphome/components/script/script.h +++ b/esphome/components/script/script.h @@ -1,8 +1,8 @@ #pragma once +#include #include #include -#include #include "esphome/core/automation.h" #include "esphome/core/component.h" #include "esphome/core/helpers.h" @@ -290,10 +290,10 @@ template class ScriptWaitAction : public Action, } // Store parameters for later execution - this->param_queue_.emplace_front(x...); - // Enable loop now that we have work to do + this->param_queue_.emplace_back(x...); + // Enable loop now that we have work to do - don't call loop() synchronously! + // Let the event loop call it to avoid reentrancy issues this->enable_loop(); - this->loop(); } void loop() override { @@ -303,13 +303,17 @@ template class ScriptWaitAction : public Action, if (this->script_->is_running()) return; - while (!this->param_queue_.empty()) { + // Only process ONE queued item per loop iteration + // Processing all items in a while loop causes infinite loops because + // play_next_() can trigger more items to be queued + if (!this->param_queue_.empty()) { auto ¶ms = this->param_queue_.front(); this->play_next_tuple_(params, typename gens::type()); this->param_queue_.pop_front(); + } else { + // Queue is now empty - disable loop until next play_complex + this->disable_loop(); } - // Queue is now empty - disable loop until next play_complex - this->disable_loop(); } void play(const Ts &...x) override { /* ignore - see play_complex */ @@ -326,7 +330,7 @@ template class ScriptWaitAction : public Action, } C *script_; - std::forward_list> param_queue_; + std::list> param_queue_; }; } // namespace script diff --git a/esphome/core/base_automation.h b/esphome/core/base_automation.h index c2519da839..e8878ac251 100644 --- a/esphome/core/base_automation.h +++ b/esphome/core/base_automation.h @@ -9,8 +9,8 @@ #include "esphome/core/application.h" #include "esphome/core/helpers.h" +#include #include -#include namespace esphome { @@ -445,9 +445,10 @@ template class WaitUntilAction : public Action, public Co // Store for later processing auto now = millis(); auto timeout = this->timeout_value_.optional_value(x...); - this->var_queue_.emplace_front(now, timeout, std::make_tuple(x...)); + this->var_queue_.emplace_back(now, timeout, std::make_tuple(x...)); - // Do immediate check with fresh timestamp + // Do immediate check with fresh timestamp - don't call loop() synchronously! + // Let the event loop call it to avoid reentrancy issues if (this->process_queue_(now)) { // Only enable loop if we still have pending items this->enable_loop(); @@ -499,7 +500,7 @@ template class WaitUntilAction : public Action, public Co } Condition *condition_; - std::forward_list, std::tuple>> var_queue_{}; + std::list, std::tuple>> var_queue_{}; }; template class UpdateComponentAction : public Action { diff --git a/tests/integration/fixtures/script_delay_with_params.yaml b/tests/integration/fixtures/script_delay_with_params.yaml new file mode 100644 index 0000000000..2a0f16d9fe --- /dev/null +++ b/tests/integration/fixtures/script_delay_with_params.yaml @@ -0,0 +1,131 @@ +esphome: + name: test-script-delay-params + +host: + +api: + actions: + # Test case from issue #12044: parent script with repeat calling child with delay + - action: test_repeat_with_delay + then: + - logger.log: "=== TEST: Repeat loop calling script with delay and parameters ===" + - script.execute: father_script + + # Test case from issue #12043: script.wait with delayed child script + - action: test_script_wait + then: + - logger.log: "=== TEST: script.wait with delayed child script ===" + - script.execute: show_start_page + - script.wait: show_start_page + - logger.log: "After wait: script completed successfully" + + # Test: Delay with different parameter types + - action: test_delay_param_types + then: + - logger.log: "=== TEST: Delay with various parameter types ===" + - script.execute: + id: delay_with_int + val: 42 + - delay: 50ms + - script.execute: + id: delay_with_string + msg: "test message" + - delay: 50ms + - script.execute: + id: delay_with_float + num: 3.14 + +logger: + level: DEBUG + +script: + # Reproduces issue #12044: child script with conditional delay + - id: son_script + mode: single + parameters: + iteration: int + then: + - logger.log: + format: "Son script started with iteration %d" + args: ['iteration'] + - if: + condition: + lambda: 'return iteration >= 5;' + then: + - logger.log: + format: "Son script delaying for iteration %d" + args: ['iteration'] + - delay: 100ms + - logger.log: + format: "Son script finished with iteration %d" + args: ['iteration'] + + # Reproduces issue #12044: parent script with repeat loop + - id: father_script + mode: single + then: + - repeat: + count: 10 + then: + - logger.log: + format: "Father iteration %d: calling son" + args: ['iteration'] + - script.execute: + id: son_script + iteration: !lambda 'return iteration;' + - script.wait: son_script + - logger.log: + format: "Father iteration %d: son finished, wait returned" + args: ['iteration'] + + # Reproduces issue #12043: script.wait hangs + - id: show_start_page + mode: single + then: + - logger.log: "Start page: beginning" + - delay: 100ms + - logger.log: "Start page: after delay" + - delay: 100ms + - logger.log: "Start page: completed" + + # Test delay with int parameter + - id: delay_with_int + mode: single + parameters: + val: int + then: + - logger.log: + format: "Int test: before delay, val=%d" + args: ['val'] + - delay: 50ms + - logger.log: + format: "Int test: after delay, val=%d" + args: ['val'] + + # Test delay with string parameter + - id: delay_with_string + mode: single + parameters: + msg: string + then: + - logger.log: + format: "String test: before delay, msg=%s" + args: ['msg.c_str()'] + - delay: 50ms + - logger.log: + format: "String test: after delay, msg=%s" + args: ['msg.c_str()'] + + # Test delay with float parameter + - id: delay_with_float + mode: single + parameters: + num: float + then: + - logger.log: + format: "Float test: before delay, num=%.2f" + args: ['num'] + - delay: 50ms + - logger.log: + format: "Float test: after delay, num=%.2f" + args: ['num'] diff --git a/tests/integration/fixtures/wait_until_fifo_ordering.yaml b/tests/integration/fixtures/wait_until_fifo_ordering.yaml new file mode 100644 index 0000000000..5dd60c8755 --- /dev/null +++ b/tests/integration/fixtures/wait_until_fifo_ordering.yaml @@ -0,0 +1,82 @@ +esphome: + name: test-wait-until-ordering + +host: + +api: + actions: + - action: test_wait_until_fifo + then: + - logger.log: "=== TEST: wait_until should execute in FIFO order ===" + - globals.set: + id: gate_open + value: 'false' + - delay: 100ms + # Start multiple parallel executions of coordinator script + # Each will call the shared waiter script, queueing in same wait_until + - script.execute: coordinator_0 + - script.execute: coordinator_1 + - script.execute: coordinator_2 + - script.execute: coordinator_3 + - script.execute: coordinator_4 + # Give scripts time to reach wait_until and queue + - delay: 200ms + - logger.log: "Opening gate - all wait_until should complete now" + - globals.set: + id: gate_open + value: 'true' + - delay: 500ms + - logger.log: "Test complete" + +globals: + - id: gate_open + type: bool + initial_value: 'false' + +script: + # Shared waiter with single wait_until action (all coordinators call this) + - id: waiter + mode: parallel + parameters: + iter: int + then: + - lambda: 'ESP_LOGD("main", "Queueing iteration %d", iter);' + - wait_until: + condition: + lambda: 'return id(gate_open);' + timeout: 5s + - lambda: 'ESP_LOGD("main", "Completed iteration %d", iter);' + + # Coordinator scripts - each calls shared waiter with different iteration number + - id: coordinator_0 + then: + - script.execute: + id: waiter + iter: 0 + + - id: coordinator_1 + then: + - script.execute: + id: waiter + iter: 1 + + - id: coordinator_2 + then: + - script.execute: + id: waiter + iter: 2 + + - id: coordinator_3 + then: + - script.execute: + id: waiter + iter: 3 + + - id: coordinator_4 + then: + - script.execute: + id: waiter + iter: 4 + +logger: + level: DEBUG diff --git a/tests/integration/test_script_delay_params.py b/tests/integration/test_script_delay_params.py new file mode 100644 index 0000000000..1b5d70863b --- /dev/null +++ b/tests/integration/test_script_delay_params.py @@ -0,0 +1,121 @@ +"""Integration test for script.wait FIFO ordering (issues #12043, #12044). + +This test verifies that ScriptWaitAction processes queued items in FIFO order. + +PR #7972 introduced bugs in ScriptWaitAction: +- Used emplace_front() causing LIFO ordering instead of FIFO +- Called loop() synchronously causing reentrancy issues +- Used while loop processing entire queue causing infinite loops + +These bugs manifested as: +- Scripts becoming "zombies" (stuck in running state) +- script.wait hanging forever +- Incorrect execution order +""" + +from __future__ import annotations + +import asyncio +import re + +import pytest + +from .types import APIClientConnectedFactory, RunCompiledFunction + + +@pytest.mark.asyncio +async def test_script_delay_with_params( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """Test that script.wait processes queued items in FIFO order. + + This reproduces issues #12043 and #12044 where scripts would hang or become + zombies due to LIFO ordering bugs in ScriptWaitAction from PR #7972. + """ + test_complete = asyncio.Event() + + # Patterns to match in logs + father_calling_pattern = re.compile(r"Father iteration (\d+): calling son") + son_started_pattern = re.compile(r"Son script started with iteration (\d+)") + son_delaying_pattern = re.compile(r"Son script delaying for iteration (\d+)") + son_finished_pattern = re.compile(r"Son script finished with iteration (\d+)") + father_wait_returned_pattern = re.compile( + r"Father iteration (\d+): son finished, wait returned" + ) + + # Track which iterations completed + father_calling = set() + son_started = set() + son_delaying = set() + son_finished = set() + wait_returned = set() + + def check_output(line: str) -> None: + """Check log output for expected messages.""" + if test_complete.is_set(): + return + + if mo := father_calling_pattern.search(line): + father_calling.add(int(mo.group(1))) + elif mo := son_started_pattern.search(line): + son_started.add(int(mo.group(1))) + elif mo := son_delaying_pattern.search(line): + son_delaying.add(int(mo.group(1))) + elif mo := son_finished_pattern.search(line): + son_finished.add(int(mo.group(1))) + elif mo := father_wait_returned_pattern.search(line): + iteration = int(mo.group(1)) + wait_returned.add(iteration) + # Test completes when iteration 9 finishes + if iteration == 9: + test_complete.set() + + # 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 == "test-script-delay-params" + + # Get services + _, services = await client.list_entities_services() + test_service = next( + (s for s in services if s.name == "test_repeat_with_delay"), None + ) + assert test_service is not None, "test_repeat_with_delay service not found" + + # Execute the test + client.execute_service(test_service, {}) + + # Wait for test to complete (10 iterations * ~100ms each + margin) + try: + await asyncio.wait_for(test_complete.wait(), timeout=5.0) + except TimeoutError: + pytest.fail( + f"Test timed out. Completed iterations: {sorted(wait_returned)}. " + f"This likely indicates the script became a zombie (issue #12044)." + ) + + # Verify all 10 iterations completed successfully + expected_iterations = set(range(10)) + assert father_calling == expected_iterations, "Not all iterations started" + assert son_started == expected_iterations, ( + "Son script not started for all iterations" + ) + assert son_finished == expected_iterations, ( + "Son script not finished for all iterations" + ) + assert wait_returned == expected_iterations, ( + "script.wait did not return for all iterations" + ) + + # Verify delays were triggered for iterations >= 5 + expected_delays = set(range(5, 10)) + assert son_delaying == expected_delays, ( + "Delays not triggered for iterations >= 5" + ) diff --git a/tests/integration/test_wait_until_ordering.py b/tests/integration/test_wait_until_ordering.py new file mode 100644 index 0000000000..7c39913e5a --- /dev/null +++ b/tests/integration/test_wait_until_ordering.py @@ -0,0 +1,90 @@ +"""Integration test for wait_until FIFO ordering. + +This test verifies that when multiple wait_until actions are queued, +they execute in FIFO (First In First Out) order, not LIFO. + +PR #7972 introduced a bug where emplace_front() was used, causing +LIFO ordering which is incorrect. +""" + +from __future__ import annotations + +import asyncio +import re + +import pytest + +from .types import APIClientConnectedFactory, RunCompiledFunction + + +@pytest.mark.asyncio +async def test_wait_until_fifo_ordering( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """Test that wait_until executes queued items in FIFO order. + + With the bug (using emplace_front), the order would be 4,3,2,1,0 (LIFO). + With the fix (using emplace_back), the order should be 0,1,2,3,4 (FIFO). + """ + test_complete = asyncio.Event() + + # Track completion order + completed_order = [] + + # Patterns to match + queuing_pattern = re.compile(r"Queueing iteration (\d+)") + completed_pattern = re.compile(r"Completed iteration (\d+)") + + def check_output(line: str) -> None: + """Check log output for completion order.""" + if test_complete.is_set(): + return + + if mo := queuing_pattern.search(line): + iteration = int(mo.group(1)) + + elif mo := completed_pattern.search(line): + iteration = int(mo.group(1)) + completed_order.append(iteration) + + # Test completes when all 5 have completed + if len(completed_order) == 5: + test_complete.set() + + # 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 == "test-wait-until-ordering" + + # Get services + _, services = await client.list_entities_services() + test_service = next( + (s for s in services if s.name == "test_wait_until_fifo"), None + ) + assert test_service is not None, "test_wait_until_fifo service not found" + + # Execute the test + client.execute_service(test_service, {}) + + # Wait for test to complete + try: + await asyncio.wait_for(test_complete.wait(), timeout=5.0) + except TimeoutError: + pytest.fail( + f"Test timed out. Completed order: {completed_order}. " + f"Expected 5 completions but got {len(completed_order)}." + ) + + # Verify FIFO order + expected_order = [0, 1, 2, 3, 4] + assert completed_order == expected_order, ( + f"Unexpected order: {completed_order}. " + f"Expected FIFO order: {expected_order}" + ) From 0764f4da86dee9a325237ca1fa2e5f3d71d0f6c7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 24 Nov 2025 11:02:24 -0600 Subject: [PATCH 232/415] [esp_ldo,mipi_dsi,mipi_rgb] Fix dangling pointer bugs in mark_failed() (#12077) --- esphome/components/esp_ldo/esp_ldo.cpp | 4 ++-- esphome/components/mipi_dsi/mipi_dsi.cpp | 6 ++++++ esphome/components/mipi_dsi/mipi_dsi.h | 5 +---- esphome/components/mipi_rgb/mipi_rgb.cpp | 4 ++-- 4 files changed, 11 insertions(+), 8 deletions(-) diff --git a/esphome/components/esp_ldo/esp_ldo.cpp b/esphome/components/esp_ldo/esp_ldo.cpp index eb04670d7e..9ea7000b70 100644 --- a/esphome/components/esp_ldo/esp_ldo.cpp +++ b/esphome/components/esp_ldo/esp_ldo.cpp @@ -14,8 +14,8 @@ void EspLdo::setup() { config.flags.adjustable = this->adjustable_; auto err = esp_ldo_acquire_channel(&config, &this->handle_); if (err != ESP_OK) { - auto msg = str_sprintf("Failed to acquire LDO channel %d with voltage %fV", this->channel_, this->voltage_); - this->mark_failed(msg.c_str()); + ESP_LOGE(TAG, "Failed to acquire LDO channel %d with voltage %fV", this->channel_, this->voltage_); + this->mark_failed("Failed to acquire LDO channel"); } else { ESP_LOGD(TAG, "Acquired LDO channel %d with voltage %fV", this->channel_, this->voltage_); } diff --git a/esphome/components/mipi_dsi/mipi_dsi.cpp b/esphome/components/mipi_dsi/mipi_dsi.cpp index fbe251de41..7305435e4b 100644 --- a/esphome/components/mipi_dsi/mipi_dsi.cpp +++ b/esphome/components/mipi_dsi/mipi_dsi.cpp @@ -11,6 +11,12 @@ static bool notify_refresh_ready(esp_lcd_panel_handle_t panel, esp_lcd_dpi_panel xSemaphoreGiveFromISR(sem, &need_yield); return (need_yield == pdTRUE); } + +void MIPI_DSI::smark_failed(const char *message, esp_err_t err) { + ESP_LOGE(TAG, "%s: %s", message, esp_err_to_name(err)); + this->mark_failed(message); +} + void MIPI_DSI::setup() { ESP_LOGCONFIG(TAG, "Running Setup"); diff --git a/esphome/components/mipi_dsi/mipi_dsi.h b/esphome/components/mipi_dsi/mipi_dsi.h index ce8a2a2236..98ee092ed1 100644 --- a/esphome/components/mipi_dsi/mipi_dsi.h +++ b/esphome/components/mipi_dsi/mipi_dsi.h @@ -62,10 +62,7 @@ class MIPI_DSI : public display::Display { void set_lanes(uint8_t lanes) { this->lanes_ = lanes; } void set_madctl(uint8_t madctl) { this->madctl_ = madctl; } - void smark_failed(const char *message, esp_err_t err) { - auto str = str_sprintf("Setup failed: %s: %s", message, esp_err_to_name(err)); - this->mark_failed(str.c_str()); - } + void smark_failed(const char *message, esp_err_t err); void update() override; diff --git a/esphome/components/mipi_rgb/mipi_rgb.cpp b/esphome/components/mipi_rgb/mipi_rgb.cpp index 080fb08c09..4c687724cf 100644 --- a/esphome/components/mipi_rgb/mipi_rgb.cpp +++ b/esphome/components/mipi_rgb/mipi_rgb.cpp @@ -164,8 +164,8 @@ void MipiRgb::common_setup_() { if (err == ESP_OK) err = esp_lcd_panel_init(this->handle_); if (err != ESP_OK) { - auto msg = str_sprintf("lcd setup failed: %s", esp_err_to_name(err)); - this->mark_failed(msg.c_str()); + ESP_LOGE(TAG, "lcd setup failed: %s", esp_err_to_name(err)); + this->mark_failed("lcd setup failed"); } ESP_LOGCONFIG(TAG, "MipiRgb setup complete"); } From 66cda0466469531a7a9428db33251c6ad985c9bd Mon Sep 17 00:00:00 2001 From: Flo Date: Mon, 24 Nov 2025 18:19:38 +0100 Subject: [PATCH 233/415] [wifi] ap_active condition (#11852) Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> --- esphome/components/wifi/__init__.py | 6 ++++++ esphome/components/wifi/wifi_component.cpp | 1 + esphome/components/wifi/wifi_component.h | 6 ++++++ tests/components/wifi/common.yaml | 4 ++++ 4 files changed, 17 insertions(+) diff --git a/esphome/components/wifi/__init__.py b/esphome/components/wifi/__init__.py index 2b21478f30..b9c0fa28a7 100644 --- a/esphome/components/wifi/__init__.py +++ b/esphome/components/wifi/__init__.py @@ -97,6 +97,7 @@ WIFI_MIN_AUTH_MODES = { 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) +WiFiAPActiveCondition = wifi_ns.class_("WiFiAPActiveCondition", Condition) WiFiEnableAction = wifi_ns.class_("WiFiEnableAction", automation.Action) WiFiDisableAction = wifi_ns.class_("WiFiDisableAction", automation.Action) WiFiConfigureAction = wifi_ns.class_( @@ -590,6 +591,11 @@ async def wifi_enabled_to_code(config, condition_id, template_arg, args): return cg.new_Pvariable(condition_id, template_arg) +@automation.register_condition("wifi.ap_active", WiFiAPActiveCondition, cv.Schema({})) +async def wifi_ap_active_to_code(config, condition_id, template_arg, args): + return cg.new_Pvariable(condition_id, template_arg) + + @automation.register_action("wifi.enable", WiFiEnableAction, cv.Schema({})) async def wifi_enable_to_code(config, action_id, template_arg, args): return cg.new_Pvariable(action_id, template_arg) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index 6f698bc2a8..23a4020453 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -530,6 +530,7 @@ void WiFiComponent::loop() { WiFiComponent::WiFiComponent() { global_wifi_component = this; } bool WiFiComponent::has_ap() const { return this->has_ap_; } +bool WiFiComponent::is_ap_active() const { return this->state_ == WIFI_COMPONENT_STATE_AP; } bool WiFiComponent::has_sta() const { return !this->sta_.empty(); } #ifdef USE_WIFI_11KV_SUPPORT void WiFiComponent::set_btm(bool btm) { this->btm_ = btm; } diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index b3548078bc..441606a2c1 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -308,6 +308,7 @@ class WiFiComponent : public Component { bool has_sta() const; bool has_ap() const; + bool is_ap_active() const; #ifdef USE_WIFI_11KV_SUPPORT void set_btm(bool btm); @@ -557,6 +558,11 @@ template class WiFiEnabledCondition : public Condition { bool check(const Ts &...x) override { return !global_wifi_component->is_disabled(); } }; +template class WiFiAPActiveCondition : public Condition { + public: + bool check(const Ts &...x) override { return global_wifi_component->is_ap_active(); } +}; + template class WiFiEnableAction : public Action { public: void play(const Ts &...x) override { global_wifi_component->enable(); } diff --git a/tests/components/wifi/common.yaml b/tests/components/wifi/common.yaml index 5d9973cbc8..7ce74ab00d 100644 --- a/tests/components/wifi/common.yaml +++ b/tests/components/wifi/common.yaml @@ -10,6 +10,10 @@ esphome: - logger.log: "Connected to WiFi!" on_error: - logger.log: "Failed to connect to WiFi!" + - if: + condition: wifi.ap_active + then: + - logger.log: "WiFi AP is active!" wifi: networks: From d7a197b3a3444d996dcdd2b249ccaa7e88aa1421 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Mon, 24 Nov 2025 12:27:09 -0500 Subject: [PATCH 234/415] [esp32] Use the IDF I2C implementation on Arduino (#12076) --- esphome/components/i2c/__init__.py | 26 +++++++++++++--------- esphome/components/i2c/i2c_bus_arduino.cpp | 24 +++++--------------- esphome/components/i2c/i2c_bus_arduino.h | 7 +++--- esphome/components/i2c/i2c_bus_esp_idf.cpp | 4 ++-- esphome/components/i2c/i2c_bus_esp_idf.h | 4 ++-- 5 files changed, 27 insertions(+), 38 deletions(-) diff --git a/esphome/components/i2c/__init__.py b/esphome/components/i2c/__init__.py index 6308923759..738568cd3c 100644 --- a/esphome/components/i2c/__init__.py +++ b/esphome/components/i2c/__init__.py @@ -47,18 +47,20 @@ MULTI_CONF = True def _bus_declare_type(value): + if CORE.is_esp32: + return cv.declare_id(IDFI2CBus)(value) if CORE.using_arduino: return cv.declare_id(ArduinoI2CBus)(value) - if CORE.using_esp_idf: - return cv.declare_id(IDFI2CBus)(value) if CORE.using_zephyr: return cv.declare_id(ZephyrI2CBus)(value) raise NotImplementedError def validate_config(config): - if CORE.using_esp_idf: - return cv.require_framework_version(esp_idf=cv.Version(5, 4, 2))(config) + if CORE.is_esp32: + return cv.require_framework_version( + esp_idf=cv.Version(5, 4, 2), esp32_arduino=cv.Version(3, 2, 1) + )(config) return config @@ -67,12 +69,12 @@ CONFIG_SCHEMA = cv.All( { cv.GenerateID(): _bus_declare_type, cv.Optional(CONF_SDA, default="SDA"): pins.internal_gpio_pin_number, - cv.SplitDefault(CONF_SDA_PULLUP_ENABLED, esp32_idf=True): cv.All( - cv.only_with_esp_idf, cv.boolean + cv.SplitDefault(CONF_SDA_PULLUP_ENABLED, esp32=True): cv.All( + cv.only_on_esp32, cv.boolean ), cv.Optional(CONF_SCL, default="SCL"): pins.internal_gpio_pin_number, - cv.SplitDefault(CONF_SCL_PULLUP_ENABLED, esp32_idf=True): cv.All( - cv.only_with_esp_idf, cv.boolean + cv.SplitDefault(CONF_SCL_PULLUP_ENABLED, esp32=True): cv.All( + cv.only_on_esp32, cv.boolean ), cv.SplitDefault( CONF_FREQUENCY, @@ -151,7 +153,7 @@ async def to_code(config): cg.add(var.set_scan(config[CONF_SCAN])) if CONF_TIMEOUT in config: cg.add(var.set_timeout(int(config[CONF_TIMEOUT].total_microseconds))) - if CORE.using_arduino: + if CORE.using_arduino and not CORE.is_esp32: cg.add_library("Wire", None) @@ -248,14 +250,16 @@ def final_validate_device_schema( FILTER_SOURCE_FILES = filter_source_files_from_platform( { "i2c_bus_arduino.cpp": { - PlatformFramework.ESP32_ARDUINO, PlatformFramework.ESP8266_ARDUINO, PlatformFramework.RP2040_ARDUINO, PlatformFramework.BK72XX_ARDUINO, PlatformFramework.RTL87XX_ARDUINO, PlatformFramework.LN882X_ARDUINO, }, - "i2c_bus_esp_idf.cpp": {PlatformFramework.ESP32_IDF}, + "i2c_bus_esp_idf.cpp": { + PlatformFramework.ESP32_ARDUINO, + PlatformFramework.ESP32_IDF, + }, "i2c_bus_zephyr.cpp": {PlatformFramework.NRF52_ZEPHYR}, } ) diff --git a/esphome/components/i2c/i2c_bus_arduino.cpp b/esphome/components/i2c/i2c_bus_arduino.cpp index 221423418b..1579020c9b 100644 --- a/esphome/components/i2c/i2c_bus_arduino.cpp +++ b/esphome/components/i2c/i2c_bus_arduino.cpp @@ -1,4 +1,4 @@ -#ifdef USE_ARDUINO +#if defined(USE_ARDUINO) && !defined(USE_ESP32) #include "i2c_bus_arduino.h" #include @@ -15,16 +15,7 @@ static const char *const TAG = "i2c.arduino"; void ArduinoI2CBus::setup() { recover_(); -#if defined(USE_ESP32) - static uint8_t next_bus_num = 0; - if (next_bus_num == 0) { - wire_ = &Wire; - } else { - wire_ = new TwoWire(next_bus_num); // NOLINT(cppcoreguidelines-owning-memory) - } - this->port_ = next_bus_num; - next_bus_num++; -#elif defined(USE_ESP8266) +#if defined(USE_ESP8266) wire_ = new TwoWire(); // NOLINT(cppcoreguidelines-owning-memory) #elif defined(USE_RP2040) static bool first = true; @@ -54,10 +45,7 @@ void ArduinoI2CBus::set_pins_and_clock_() { wire_->begin(static_cast(sda_pin_), static_cast(scl_pin_)); #endif if (timeout_ > 0) { // if timeout specified in yaml -#if defined(USE_ESP32) - // https://github.com/espressif/arduino-esp32/blob/master/libraries/Wire/src/Wire.cpp - wire_->setTimeOut(timeout_ / 1000); // unit: ms -#elif defined(USE_ESP8266) +#if defined(USE_ESP8266) // https://github.com/esp8266/Arduino/blob/master/libraries/Wire/Wire.h wire_->setClockStretchLimit(timeout_); // unit: us #elif defined(USE_RP2040) @@ -76,9 +64,7 @@ void ArduinoI2CBus::dump_config() { " Frequency: %u Hz", this->sda_pin_, this->scl_pin_, this->frequency_); if (timeout_ > 0) { -#if defined(USE_ESP32) - ESP_LOGCONFIG(TAG, " Timeout: %u ms", this->timeout_ / 1000); -#elif defined(USE_ESP8266) +#if defined(USE_ESP8266) ESP_LOGCONFIG(TAG, " Timeout: %u us", this->timeout_); #elif defined(USE_RP2040) ESP_LOGCONFIG(TAG, " Timeout: %u ms", this->timeout_ / 1000); @@ -275,4 +261,4 @@ void ArduinoI2CBus::recover_() { } // namespace i2c } // namespace esphome -#endif // USE_ESP_IDF +#endif // defined(USE_ARDUINO) && !defined(USE_ESP32) diff --git a/esphome/components/i2c/i2c_bus_arduino.h b/esphome/components/i2c/i2c_bus_arduino.h index b441828353..2d69e7684c 100644 --- a/esphome/components/i2c/i2c_bus_arduino.h +++ b/esphome/components/i2c/i2c_bus_arduino.h @@ -1,6 +1,6 @@ #pragma once -#ifdef USE_ARDUINO +#if defined(USE_ARDUINO) && !defined(USE_ESP32) #include #include "esphome/core/component.h" @@ -29,7 +29,7 @@ class ArduinoI2CBus : public InternalI2CBus, public Component { void set_frequency(uint32_t frequency) { frequency_ = frequency; } void set_timeout(uint32_t timeout) { timeout_ = timeout; } - int get_port() const override { return this->port_; } + int get_port() const override { return 0; } private: void recover_(); @@ -37,7 +37,6 @@ class ArduinoI2CBus : public InternalI2CBus, public Component { RecoveryCode recovery_result_; protected: - int8_t port_{-1}; TwoWire *wire_; uint8_t sda_pin_; uint8_t scl_pin_; @@ -49,4 +48,4 @@ class ArduinoI2CBus : public InternalI2CBus, public Component { } // namespace i2c } // namespace esphome -#endif // USE_ARDUINO +#endif // defined(USE_ARDUINO) && !defined(USE_ESP32) diff --git a/esphome/components/i2c/i2c_bus_esp_idf.cpp b/esphome/components/i2c/i2c_bus_esp_idf.cpp index bf50ea0586..c22db51c68 100644 --- a/esphome/components/i2c/i2c_bus_esp_idf.cpp +++ b/esphome/components/i2c/i2c_bus_esp_idf.cpp @@ -1,4 +1,4 @@ -#ifdef USE_ESP_IDF +#ifdef USE_ESP32 #include "i2c_bus_esp_idf.h" @@ -299,4 +299,4 @@ void IDFI2CBus::recover_() { } // namespace i2c } // namespace esphome -#endif // USE_ESP_IDF +#endif // USE_ESP32 diff --git a/esphome/components/i2c/i2c_bus_esp_idf.h b/esphome/components/i2c/i2c_bus_esp_idf.h index f565be4535..63fe8b701c 100644 --- a/esphome/components/i2c/i2c_bus_esp_idf.h +++ b/esphome/components/i2c/i2c_bus_esp_idf.h @@ -1,6 +1,6 @@ #pragma once -#ifdef USE_ESP_IDF +#ifdef USE_ESP32 #include "esphome/core/component.h" #include "i2c_bus.h" @@ -53,4 +53,4 @@ class IDFI2CBus : public InternalI2CBus, public Component { } // namespace i2c } // namespace esphome -#endif // USE_ESP_IDF +#endif // USE_ESP32 From d7da55988552ca5a044e57b84a5c284763efb66c Mon Sep 17 00:00:00 2001 From: Sascha Ittner Date: Mon, 24 Nov 2025 18:31:26 +0100 Subject: [PATCH 235/415] [thermopro_ble] Add thermopro ble support (#11835) Co-authored-by: J. Nick Koston Co-authored-by: J. Nick Koston --- CODEOWNERS | 1 + esphome/components/thermopro_ble/__init__.py | 0 esphome/components/thermopro_ble/sensor.py | 97 +++++++++ .../thermopro_ble/thermopro_ble.cpp | 204 ++++++++++++++++++ .../components/thermopro_ble/thermopro_ble.h | 49 +++++ tests/components/thermopro_ble/common.yaml | 13 ++ .../thermopro_ble/test.esp32-idf.yaml | 4 + 7 files changed, 368 insertions(+) create mode 100644 esphome/components/thermopro_ble/__init__.py create mode 100644 esphome/components/thermopro_ble/sensor.py create mode 100644 esphome/components/thermopro_ble/thermopro_ble.cpp create mode 100644 esphome/components/thermopro_ble/thermopro_ble.h create mode 100644 tests/components/thermopro_ble/common.yaml create mode 100644 tests/components/thermopro_ble/test.esp32-idf.yaml diff --git a/CODEOWNERS b/CODEOWNERS index d6ec7b882e..c6332e3933 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -484,6 +484,7 @@ esphome/components/template/datetime/* @rfdarter esphome/components/template/event/* @nohat esphome/components/template/fan/* @ssieb esphome/components/text/* @mauritskorse +esphome/components/thermopro_ble/* @sittner esphome/components/thermostat/* @kbx81 esphome/components/time/* @esphome/core esphome/components/tinyusb/* @kbx81 diff --git a/esphome/components/thermopro_ble/__init__.py b/esphome/components/thermopro_ble/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/thermopro_ble/sensor.py b/esphome/components/thermopro_ble/sensor.py new file mode 100644 index 0000000000..de63229621 --- /dev/null +++ b/esphome/components/thermopro_ble/sensor.py @@ -0,0 +1,97 @@ +import esphome.codegen as cg +from esphome.components import esp32_ble_tracker, sensor +import esphome.config_validation as cv +from esphome.const import ( + CONF_BATTERY_LEVEL, + CONF_EXTERNAL_TEMPERATURE, + CONF_HUMIDITY, + CONF_ID, + CONF_MAC_ADDRESS, + CONF_SIGNAL_STRENGTH, + CONF_TEMPERATURE, + DEVICE_CLASS_BATTERY, + DEVICE_CLASS_HUMIDITY, + DEVICE_CLASS_SIGNAL_STRENGTH, + DEVICE_CLASS_TEMPERATURE, + ENTITY_CATEGORY_DIAGNOSTIC, + STATE_CLASS_MEASUREMENT, + UNIT_CELSIUS, + UNIT_DECIBEL_MILLIWATT, + UNIT_PERCENT, +) + +CODEOWNERS = ["@sittner"] + +DEPENDENCIES = ["esp32_ble_tracker"] + +thermopro_ble_ns = cg.esphome_ns.namespace("thermopro_ble") +ThermoProBLE = thermopro_ble_ns.class_( + "ThermoProBLE", esp32_ble_tracker.ESPBTDeviceListener, cg.Component +) + +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(ThermoProBLE), + cv.Required(CONF_MAC_ADDRESS): cv.mac_address, + cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( + unit_of_measurement=UNIT_CELSIUS, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_EXTERNAL_TEMPERATURE): sensor.sensor_schema( + unit_of_measurement=UNIT_CELSIUS, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_HUMIDITY): sensor.sensor_schema( + unit_of_measurement=UNIT_PERCENT, + accuracy_decimals=0, + device_class=DEVICE_CLASS_HUMIDITY, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_BATTERY_LEVEL): sensor.sensor_schema( + unit_of_measurement=UNIT_PERCENT, + accuracy_decimals=0, + device_class=DEVICE_CLASS_BATTERY, + state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + cv.Optional(CONF_SIGNAL_STRENGTH): sensor.sensor_schema( + unit_of_measurement=UNIT_DECIBEL_MILLIWATT, + accuracy_decimals=0, + device_class=DEVICE_CLASS_SIGNAL_STRENGTH, + state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + } + ) + .extend(esp32_ble_tracker.ESP_BLE_DEVICE_SCHEMA) + .extend(cv.COMPONENT_SCHEMA) +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await esp32_ble_tracker.register_ble_device(var, config) + + cg.add(var.set_address(config[CONF_MAC_ADDRESS].as_hex)) + + if temperature_config := config.get(CONF_TEMPERATURE): + sens = await sensor.new_sensor(temperature_config) + cg.add(var.set_temperature(sens)) + if external_temperature_config := config.get(CONF_EXTERNAL_TEMPERATURE): + sens = await sensor.new_sensor(external_temperature_config) + cg.add(var.set_external_temperature(sens)) + if humidity_config := config.get(CONF_HUMIDITY): + sens = await sensor.new_sensor(humidity_config) + cg.add(var.set_humidity(sens)) + if battery_level_config := config.get(CONF_BATTERY_LEVEL): + sens = await sensor.new_sensor(battery_level_config) + cg.add(var.set_battery_level(sens)) + if signal_strength_config := config.get(CONF_SIGNAL_STRENGTH): + sens = await sensor.new_sensor(signal_strength_config) + cg.add(var.set_signal_strength(sens)) diff --git a/esphome/components/thermopro_ble/thermopro_ble.cpp b/esphome/components/thermopro_ble/thermopro_ble.cpp new file mode 100644 index 0000000000..4b43c9b39e --- /dev/null +++ b/esphome/components/thermopro_ble/thermopro_ble.cpp @@ -0,0 +1,204 @@ +#include "thermopro_ble.h" +#include "esphome/core/log.h" + +#ifdef USE_ESP32 + +namespace esphome::thermopro_ble { + +// this size must be large enough to hold the largest data frame +// of all supported devices +static constexpr std::size_t MAX_DATA_SIZE = 24; + +struct DeviceParserMapping { + const char *prefix; + DeviceParser parser; +}; + +static float tp96_battery(uint16_t voltage); + +static optional parse_tp972(const uint8_t *data, std::size_t data_size); +static optional parse_tp96(const uint8_t *data, std::size_t data_size); +static optional parse_tp3(const uint8_t *data, std::size_t data_size); + +static const char *const TAG = "thermopro_ble"; + +static const struct DeviceParserMapping DEVICE_PARSER_MAP[] = { + {"TP972", parse_tp972}, {"TP970", parse_tp96}, {"TP96", parse_tp96}, {"TP3", parse_tp3}}; + +void ThermoProBLE::dump_config() { + ESP_LOGCONFIG(TAG, "ThermoPro BLE"); + LOG_SENSOR(" ", "Temperature", this->temperature_); + LOG_SENSOR(" ", "External temperature", this->external_temperature_); + LOG_SENSOR(" ", "Humidity", this->humidity_); + LOG_SENSOR(" ", "Battery Level", this->battery_level_); +} + +bool ThermoProBLE::parse_device(const esp32_ble_tracker::ESPBTDevice &device) { + // check for matching mac address + if (device.address_uint64() != this->address_) { + ESP_LOGVV(TAG, "parse_device(): unknown MAC address."); + return false; + } + + // check for valid device type + update_device_type_(device.get_name()); + if (this->device_parser_ == nullptr) { + ESP_LOGVV(TAG, "parse_device(): invalid device type."); + return false; + } + + ESP_LOGVV(TAG, "parse_device(): MAC address %s found.", device.address_str().c_str()); + + // publish signal strength + float signal_strength = float(device.get_rssi()); + if (this->signal_strength_ != nullptr) + this->signal_strength_->publish_state(signal_strength); + + bool success = false; + for (auto &service_data : device.get_manufacturer_datas()) { + // check maximum data size + std::size_t data_size = service_data.data.size() + 2; + if (data_size > MAX_DATA_SIZE) { + ESP_LOGVV(TAG, "parse_device(): maximum data size exceeded!"); + continue; + } + + // reconstruct whole record from 2 byte uuid and data + esp_bt_uuid_t uuid = service_data.uuid.get_uuid(); + uint8_t data[MAX_DATA_SIZE] = {static_cast(uuid.uuid.uuid16), static_cast(uuid.uuid.uuid16 >> 8)}; + std::copy(service_data.data.begin(), service_data.data.end(), std::begin(data) + 2); + + // dispatch data to parser + optional result = this->device_parser_(data, data_size); + if (!result.has_value()) { + continue; + } + + // publish sensor values + if (result->temperature.has_value() && this->temperature_ != nullptr) + this->temperature_->publish_state(*result->temperature); + if (result->external_temperature.has_value() && this->external_temperature_ != nullptr) + this->external_temperature_->publish_state(*result->external_temperature); + if (result->humidity.has_value() && this->humidity_ != nullptr) + this->humidity_->publish_state(*result->humidity); + if (result->battery_level.has_value() && this->battery_level_ != nullptr) + this->battery_level_->publish_state(*result->battery_level); + + success = true; + } + + return success; +} + +void ThermoProBLE::update_device_type_(const std::string &device_name) { + // check for changed device name (should only happen on initial call) + if (this->device_name_ == device_name) { + return; + } + + // remember device name + this->device_name_ = device_name; + + // try to find device parser + for (const auto &mapping : DEVICE_PARSER_MAP) { + if (device_name.starts_with(mapping.prefix)) { + this->device_parser_ = mapping.parser; + return; + } + } + + // device type unknown + this->device_parser_ = nullptr; + ESP_LOGVV(TAG, "update_device_type_(): unknown device type %s.", device_name.c_str()); +} + +static inline uint16_t read_uint16(const uint8_t *data, std::size_t offset) { + return static_cast(data[offset + 0]) | (static_cast(data[offset + 1]) << 8); +} + +static inline int16_t read_int16(const uint8_t *data, std::size_t offset) { + return static_cast(read_uint16(data, offset)); +} + +static inline uint32_t read_uint32(const uint8_t *data, std::size_t offset) { + return static_cast(data[offset + 0]) | (static_cast(data[offset + 1]) << 8) | + (static_cast(data[offset + 2]) << 16) | (static_cast(data[offset + 3]) << 24); +} + +// Battery calculation used with permission from: +// https://github.com/Bluetooth-Devices/thermopro-ble/blob/main/src/thermopro_ble/parser.py +// +// TP96x battery values appear to be a voltage reading, probably in millivolts. +// This means that calculating battery life from it is a non-linear function. +// Examining the curve, it looked fairly close to a curve from the tanh function. +// So, I created a script to use Tensorflow to optimize an equation in the format +// A*tanh(B*x+C)+D +// Where A,B,C,D are the variables to optimize for. This yielded the below function +static float tp96_battery(uint16_t voltage) { + float level = 52.317286f * tanh(static_cast(voltage) / 273.624277936f - 8.76485439394f) + 51.06925f; + return std::max(0.0f, std::min(level, 100.0f)); +} + +static optional parse_tp972(const uint8_t *data, std::size_t data_size) { + if (data_size != 23) { + ESP_LOGVV(TAG, "parse_tp972(): payload has wrong size of %d (!= 23)!", data_size); + return {}; + } + + ParseResult result; + + // ambient temperature, 2 bytes, 16-bit unsigned integer, -54 °C offset + result.external_temperature = static_cast(read_uint16(data, 1)) - 54.0f; + + // battery level, 2 bytes, 16-bit unsigned integer, voltage (convert to percentage) + result.battery_level = tp96_battery(read_uint16(data, 3)); + + // internal temperature, 4 bytes, float, -54 °C offset + result.temperature = static_cast(read_uint32(data, 9)) - 54.0f; + + return result; +} + +static optional parse_tp96(const uint8_t *data, std::size_t data_size) { + if (data_size != 7) { + ESP_LOGVV(TAG, "parse_tp96(): payload has wrong size of %d (!= 7)!", data_size); + return {}; + } + + ParseResult result; + + // internal temperature, 2 bytes, 16-bit unsigned integer, -30 °C offset + result.temperature = static_cast(read_uint16(data, 1)) - 30.0f; + + // battery level, 2 bytes, 16-bit unsigned integer, voltage (convert to percentage) + result.battery_level = tp96_battery(read_uint16(data, 3)); + + // ambient temperature, 2 bytes, 16-bit unsigned integer, -30 °C offset + result.external_temperature = static_cast(read_uint16(data, 5)) - 30.0f; + + return result; +} + +static optional parse_tp3(const uint8_t *data, std::size_t data_size) { + if (data_size < 6) { + ESP_LOGVV(TAG, "parse_tp3(): payload has wrong size of %d (< 6)!", data_size); + return {}; + } + + ParseResult result; + + // temperature, 2 bytes, 16-bit signed integer, 0.1 °C + result.temperature = static_cast(read_int16(data, 1)) * 0.1f; + + // humidity, 1 byte, 8-bit unsigned integer, 1.0 % + result.humidity = static_cast(data[3]); + + // battery level, 2 bits (0-2) + result.battery_level = static_cast(data[4] & 0x3) * 50.0; + + return result; +} + +} // namespace esphome::thermopro_ble + +#endif diff --git a/esphome/components/thermopro_ble/thermopro_ble.h b/esphome/components/thermopro_ble/thermopro_ble.h new file mode 100644 index 0000000000..38bed82102 --- /dev/null +++ b/esphome/components/thermopro_ble/thermopro_ble.h @@ -0,0 +1,49 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" + +#ifdef USE_ESP32 + +namespace esphome::thermopro_ble { + +struct ParseResult { + optional temperature; + optional external_temperature; + optional humidity; + optional battery_level; +}; + +using DeviceParser = optional (*)(const uint8_t *data, std::size_t data_size); + +class ThermoProBLE : public Component, public esp32_ble_tracker::ESPBTDeviceListener { + public: + void set_address(uint64_t address) { this->address_ = address; }; + + bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override; + void dump_config() override; + void set_signal_strength(sensor::Sensor *signal_strength) { this->signal_strength_ = signal_strength; } + void set_temperature(sensor::Sensor *temperature) { this->temperature_ = temperature; } + void set_external_temperature(sensor::Sensor *external_temperature) { + this->external_temperature_ = external_temperature; + } + void set_humidity(sensor::Sensor *humidity) { this->humidity_ = humidity; } + void set_battery_level(sensor::Sensor *battery_level) { this->battery_level_ = battery_level; } + + protected: + uint64_t address_; + std::string device_name_; + DeviceParser device_parser_{nullptr}; + sensor::Sensor *signal_strength_{nullptr}; + sensor::Sensor *temperature_{nullptr}; + sensor::Sensor *external_temperature_{nullptr}; + sensor::Sensor *humidity_{nullptr}; + sensor::Sensor *battery_level_{nullptr}; + + void update_device_type_(const std::string &device_name); +}; + +} // namespace esphome::thermopro_ble + +#endif diff --git a/tests/components/thermopro_ble/common.yaml b/tests/components/thermopro_ble/common.yaml new file mode 100644 index 0000000000..297725e1c3 --- /dev/null +++ b/tests/components/thermopro_ble/common.yaml @@ -0,0 +1,13 @@ +esp32_ble_tracker: + +sensor: + - platform: thermopro_ble + mac_address: FE:74:B8:6A:97:B7 + temperature: + name: "ThermoPro Temperature" + humidity: + name: "ThermoPro Humidity" + battery_level: + name: "ThermoPro Battery Level" + signal_strength: + name: "ThermoPro Signal Strength" diff --git a/tests/components/thermopro_ble/test.esp32-idf.yaml b/tests/components/thermopro_ble/test.esp32-idf.yaml new file mode 100644 index 0000000000..7a6541ae76 --- /dev/null +++ b/tests/components/thermopro_ble/test.esp32-idf.yaml @@ -0,0 +1,4 @@ +packages: + ble: !include ../../test_build_components/common/ble/esp32-idf.yaml + +<<: !include common.yaml From b820e676161295d7d135128ff38a99016ecb0e5e Mon Sep 17 00:00:00 2001 From: Jordan Zucker Date: Mon, 24 Nov 2025 09:42:07 -0800 Subject: [PATCH 236/415] [prometheus] Add event and text base components metrics (#10240) Co-authored-by: Jordan Zucker Co-authored-by: J. Nick Koston --- .../prometheus/prometheus_handler.cpp | 106 ++++++++++++++++++ .../prometheus/prometheus_handler.h | 16 +++ tests/components/prometheus/common.yaml | 28 +++++ 3 files changed, 150 insertions(+) diff --git a/esphome/components/prometheus/prometheus_handler.cpp b/esphome/components/prometheus/prometheus_handler.cpp index 5cfcacf0cb..6b57a3f718 100644 --- a/esphome/components/prometheus/prometheus_handler.cpp +++ b/esphome/components/prometheus/prometheus_handler.cpp @@ -53,6 +53,18 @@ void PrometheusHandler::handleRequest(AsyncWebServerRequest *req) { this->lock_row_(stream, obj, area, node, friendly_name); #endif +#ifdef USE_EVENT + this->event_type_(stream); + for (auto *obj : App.get_events()) + this->event_row_(stream, obj, area, node, friendly_name); +#endif + +#ifdef USE_TEXT + this->text_type_(stream); + for (auto *obj : App.get_texts()) + this->text_row_(stream, obj, area, node, friendly_name); +#endif + #ifdef USE_TEXT_SENSOR this->text_sensor_type_(stream); for (auto *obj : App.get_text_sensors()) @@ -547,6 +559,100 @@ void PrometheusHandler::text_sensor_row_(AsyncResponseStream *stream, text_senso } #endif +// Type-specific implementation +#ifdef USE_TEXT +void PrometheusHandler::text_type_(AsyncResponseStream *stream) { + stream->print(ESPHOME_F("#TYPE esphome_text_value gauge\n")); + stream->print(ESPHOME_F("#TYPE esphome_text_failed gauge\n")); +} +void PrometheusHandler::text_row_(AsyncResponseStream *stream, text::Text *obj, std::string &area, std::string &node, + std::string &friendly_name) { + if (obj->is_internal() && !this->include_internal_) + return; + if (obj->has_state()) { + // We have a valid value, output this value + stream->print(ESPHOME_F("esphome_text_failed{id=\"")); + stream->print(relabel_id_(obj).c_str()); + add_area_label_(stream, area); + add_node_label_(stream, node); + add_friendly_name_label_(stream, friendly_name); + stream->print(ESPHOME_F("\",name=\"")); + stream->print(relabel_name_(obj).c_str()); + stream->print(ESPHOME_F("\"} 0\n")); + // Data itself + stream->print(ESPHOME_F("esphome_text_value{id=\"")); + stream->print(relabel_id_(obj).c_str()); + add_area_label_(stream, area); + add_node_label_(stream, node); + add_friendly_name_label_(stream, friendly_name); + stream->print(ESPHOME_F("\",name=\"")); + stream->print(relabel_name_(obj).c_str()); + stream->print(ESPHOME_F("\",value=\"")); + stream->print(obj->state.c_str()); + stream->print(ESPHOME_F("\"} ")); + stream->print(ESPHOME_F("1.0")); + stream->print(ESPHOME_F("\n")); + } else { + // Invalid state + stream->print(ESPHOME_F("esphome_text_failed{id=\"")); + stream->print(relabel_id_(obj).c_str()); + add_area_label_(stream, area); + add_node_label_(stream, node); + add_friendly_name_label_(stream, friendly_name); + stream->print(ESPHOME_F("\",name=\"")); + stream->print(relabel_name_(obj).c_str()); + stream->print(ESPHOME_F("\"} 1\n")); + } +} +#endif + +// Type-specific implementation +#ifdef USE_EVENT +void PrometheusHandler::event_type_(AsyncResponseStream *stream) { + stream->print(ESPHOME_F("#TYPE esphome_event_value gauge\n")); + stream->print(ESPHOME_F("#TYPE esphome_event_failed gauge\n")); +} +void PrometheusHandler::event_row_(AsyncResponseStream *stream, event::Event *obj, std::string &area, std::string &node, + std::string &friendly_name) { + if (obj->is_internal() && !this->include_internal_) + return; + if (obj->get_last_event_type() != nullptr) { + // We have a valid event type, output this value + stream->print(ESPHOME_F("esphome_event_failed{id=\"")); + stream->print(relabel_id_(obj).c_str()); + add_area_label_(stream, area); + add_node_label_(stream, node); + add_friendly_name_label_(stream, friendly_name); + stream->print(ESPHOME_F("\",name=\"")); + stream->print(relabel_name_(obj).c_str()); + stream->print(ESPHOME_F("\"} 0\n")); + // Data itself + stream->print(ESPHOME_F("esphome_event_value{id=\"")); + stream->print(relabel_id_(obj).c_str()); + add_area_label_(stream, area); + add_node_label_(stream, node); + add_friendly_name_label_(stream, friendly_name); + stream->print(ESPHOME_F("\",name=\"")); + stream->print(relabel_name_(obj).c_str()); + stream->print(ESPHOME_F("\",last_event_type=\"")); + stream->print(obj->get_last_event_type()); + stream->print(ESPHOME_F("\"} ")); + stream->print(ESPHOME_F("1.0")); + stream->print(ESPHOME_F("\n")); + } else { + // No event triggered yet + stream->print(ESPHOME_F("esphome_event_failed{id=\"")); + stream->print(relabel_id_(obj).c_str()); + add_area_label_(stream, area); + add_node_label_(stream, node); + add_friendly_name_label_(stream, friendly_name); + stream->print(ESPHOME_F("\",name=\"")); + stream->print(relabel_name_(obj).c_str()); + stream->print(ESPHOME_F("\"} 1\n")); + } +} +#endif + // Type-specific implementation #ifdef USE_NUMBER void PrometheusHandler::number_type_(AsyncResponseStream *stream) { diff --git a/esphome/components/prometheus/prometheus_handler.h b/esphome/components/prometheus/prometheus_handler.h index c4598f44b0..45cc81b899 100644 --- a/esphome/components/prometheus/prometheus_handler.h +++ b/esphome/components/prometheus/prometheus_handler.h @@ -123,6 +123,22 @@ class PrometheusHandler : public AsyncWebHandler, public Component { std::string &friendly_name); #endif +#ifdef USE_EVENT + /// Return the type for prometheus + void event_type_(AsyncResponseStream *stream); + /// Return the event values state as prometheus data point + void event_row_(AsyncResponseStream *stream, event::Event *obj, std::string &area, std::string &node, + std::string &friendly_name); +#endif + +#ifdef USE_TEXT + /// Return the type for prometheus + void text_type_(AsyncResponseStream *stream); + /// Return the text values state as prometheus data point + void text_row_(AsyncResponseStream *stream, text::Text *obj, std::string &area, std::string &node, + std::string &friendly_name); +#endif + #ifdef USE_TEXT_SENSOR /// Return the type for prometheus void text_sensor_type_(AsyncResponseStream *stream); diff --git a/tests/components/prometheus/common.yaml b/tests/components/prometheus/common.yaml index cf46e882a7..0b90d614dd 100644 --- a/tests/components/prometheus/common.yaml +++ b/tests/components/prometheus/common.yaml @@ -39,6 +39,15 @@ sensor: return 0.0; update_interval: 60s +text: + - platform: template + name: "Template text" + optimistic: true + min_length: 0 + max_length: 100 + mode: text + initial_value: "Hello World" + text_sensor: - platform: version name: "ESPHome Version" @@ -52,6 +61,25 @@ text_sensor: return {"Goodbye (cruel) World"}; update_interval: 60s +event: + - platform: template + name: "Template Event" + id: template_event1 + event_types: + - "custom_event_1" + - "custom_event_2" + +button: + - platform: template + name: "Template Event Button" + on_press: + - logger.log: "Template Event Button pressed" + - lambda: |- + ESP_LOGD("template_event_button", "Template Event Button pressed"); + - event.trigger: + id: template_event1 + event_type: custom_event_1 + binary_sensor: - platform: template id: template_binary_sensor1 From 09f3f6219493ec28ed12fe3495e7c85c608620e1 Mon Sep 17 00:00:00 2001 From: Flo Date: Mon, 24 Nov 2025 18:49:16 +0100 Subject: [PATCH 237/415] [api] Connected Condition - state_subscription_only flag (#11906) Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> Co-authored-by: J. Nick Koston --- esphome/components/api/__init__.py | 20 ++++++++++++++++++-- esphome/components/api/api_server.cpp | 13 ++++++++++++- esphome/components/api/api_server.h | 7 +++++-- tests/components/api/common-base.yaml | 4 ++++ 4 files changed, 39 insertions(+), 5 deletions(-) diff --git a/esphome/components/api/__init__.py b/esphome/components/api/__init__.py index 7f84f2f247..2910643dfb 100644 --- a/esphome/components/api/__init__.py +++ b/esphome/components/api/__init__.py @@ -85,6 +85,7 @@ CONF_HOMEASSISTANT_SERVICES = "homeassistant_services" CONF_HOMEASSISTANT_STATES = "homeassistant_states" CONF_LISTEN_BACKLOG = "listen_backlog" CONF_MAX_SEND_QUEUE = "max_send_queue" +CONF_STATE_SUBSCRIPTION_ONLY = "state_subscription_only" def validate_encryption_key(value): @@ -537,9 +538,24 @@ async def homeassistant_tag_scanned_to_code(config, action_id, template_arg, arg return var -@automation.register_condition("api.connected", APIConnectedCondition, {}) +API_CONNECTED_CONDITION_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.use_id(APIServer), + cv.Optional(CONF_STATE_SUBSCRIPTION_ONLY, default=False): cv.templatable( + cv.boolean + ), + } +) + + +@automation.register_condition( + "api.connected", APIConnectedCondition, API_CONNECTED_CONDITION_SCHEMA +) async def api_connected_to_code(config, condition_id, template_arg, args): - return cg.new_Pvariable(condition_id, template_arg) + var = cg.new_Pvariable(condition_id, template_arg) + templ = await cg.templatable(config[CONF_STATE_SUBSCRIPTION_ONLY], args, cg.bool_) + cg.add(var.set_state_subscription_only(templ)) + return var def FILTER_SOURCE_FILES() -> list[str]: diff --git a/esphome/components/api/api_server.cpp b/esphome/components/api/api_server.cpp index 18601d74ff..d33c98abc9 100644 --- a/esphome/components/api/api_server.cpp +++ b/esphome/components/api/api_server.cpp @@ -528,7 +528,18 @@ void APIServer::request_time() { } #endif -bool APIServer::is_connected() const { return !this->clients_.empty(); } +bool APIServer::is_connected(bool state_subscription_only) const { + if (!state_subscription_only) { + return !this->clients_.empty(); + } + + for (const auto &client : this->clients_) { + if (client->flags_.state_subscription) { + return true; + } + } + return false; +} void APIServer::on_shutdown() { this->shutting_down_ = true; diff --git a/esphome/components/api/api_server.h b/esphome/components/api/api_server.h index a3a082e165..786cd63f44 100644 --- a/esphome/components/api/api_server.h +++ b/esphome/components/api/api_server.h @@ -150,7 +150,7 @@ class APIServer : public Component, public Controller { void on_zwave_proxy_request(const esphome::api::ProtoMessage &msg); #endif - bool is_connected() const; + bool is_connected(bool state_subscription_only = false) const; #ifdef USE_API_HOMEASSISTANT_STATES struct HomeAssistantStateSubscription { @@ -236,8 +236,11 @@ class APIServer : public Component, public Controller { extern APIServer *global_api_server; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) template class APIConnectedCondition : public Condition { + TEMPLATABLE_VALUE(bool, state_subscription_only) public: - bool check(const Ts &...x) override { return global_api_server->is_connected(); } + bool check(const Ts &...x) override { + return global_api_server->is_connected(this->state_subscription_only_.value(x...)); + } }; } // namespace esphome::api diff --git a/tests/components/api/common-base.yaml b/tests/components/api/common-base.yaml index fc53b8ac7e..0416cebf9b 100644 --- a/tests/components/api/common-base.yaml +++ b/tests/components/api/common-base.yaml @@ -1,6 +1,10 @@ esphome: on_boot: then: + - wait_until: + condition: + api.connected: + state_subscription_only: true - homeassistant.event: event: esphome.button_pressed data: From c888becfa7369396c96fd1d4e8f807ebde57cc7b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 24 Nov 2025 11:52:15 -0600 Subject: [PATCH 238/415] [api] Optimize APINoiseContext memory usage by removing shared_ptr overhead (#11981) --- esphome/components/api/api_connection.cpp | 4 ++-- esphome/components/api/api_frame_helper_noise.cpp | 2 +- esphome/components/api/api_frame_helper_noise.h | 9 ++++----- esphome/components/api/api_server.cpp | 6 +++--- esphome/components/api/api_server.h | 6 +++--- esphome/components/mdns/mdns_component.cpp | 2 +- esphome/components/mqtt/mqtt_client.cpp | 2 +- 7 files changed, 15 insertions(+), 16 deletions(-) diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 04221a237b..ebfc641537 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -90,8 +90,8 @@ static const int CAMERA_STOP_STREAM = 5000; APIConnection::APIConnection(std::unique_ptr sock, APIServer *parent) : parent_(parent), initial_state_iterator_(this), list_entities_iterator_(this) { #if defined(USE_API_PLAINTEXT) && defined(USE_API_NOISE) - auto noise_ctx = parent->get_noise_ctx(); - if (noise_ctx->has_psk()) { + auto &noise_ctx = parent->get_noise_ctx(); + if (noise_ctx.has_psk()) { this->helper_ = std::unique_ptr{new APINoiseFrameHelper(std::move(sock), noise_ctx, &this->client_info_)}; } else { diff --git a/esphome/components/api/api_frame_helper_noise.cpp b/esphome/components/api/api_frame_helper_noise.cpp index 8bcec0f9f3..f1028fa299 100644 --- a/esphome/components/api/api_frame_helper_noise.cpp +++ b/esphome/components/api/api_frame_helper_noise.cpp @@ -528,7 +528,7 @@ APIError APINoiseFrameHelper::init_handshake_() { if (aerr != APIError::OK) return aerr; - const auto &psk = ctx_->get_psk(); + const auto &psk = this->ctx_.get_psk(); err = noise_handshakestate_set_pre_shared_key(handshake_, psk.data(), psk.size()); aerr = handle_noise_error_(err, LOG_STR("noise_handshakestate_set_pre_shared_key"), APIError::HANDSHAKESTATE_SETUP_FAILED); diff --git a/esphome/components/api/api_frame_helper_noise.h b/esphome/components/api/api_frame_helper_noise.h index e3243e4fa5..7eb01058db 100644 --- a/esphome/components/api/api_frame_helper_noise.h +++ b/esphome/components/api/api_frame_helper_noise.h @@ -9,9 +9,8 @@ namespace esphome::api { class APINoiseFrameHelper final : public APIFrameHelper { public: - APINoiseFrameHelper(std::unique_ptr socket, std::shared_ptr ctx, - const ClientInfo *client_info) - : APIFrameHelper(std::move(socket), client_info), ctx_(std::move(ctx)) { + APINoiseFrameHelper(std::unique_ptr socket, APINoiseContext &ctx, const ClientInfo *client_info) + : APIFrameHelper(std::move(socket), client_info), ctx_(ctx) { // Noise header structure: // Pos 0: indicator (0x01) // Pos 1-2: encrypted payload size (16-bit big-endian) @@ -41,8 +40,8 @@ class APINoiseFrameHelper final : public APIFrameHelper { NoiseCipherState *send_cipher_{nullptr}; NoiseCipherState *recv_cipher_{nullptr}; - // Shared pointer (8 bytes on 32-bit = 4 bytes control block pointer + 4 bytes object pointer) - std::shared_ptr ctx_; + // Reference to noise context (4 bytes on 32-bit) + APINoiseContext &ctx_; // Vector (12 bytes on 32-bit) std::vector prologue_; diff --git a/esphome/components/api/api_server.cpp b/esphome/components/api/api_server.cpp index d33c98abc9..64f8751c35 100644 --- a/esphome/components/api/api_server.cpp +++ b/esphome/components/api/api_server.cpp @@ -227,8 +227,8 @@ void APIServer::dump_config() { " Max connections: %u", network::get_use_address(), this->port_, this->listen_backlog_, this->max_connections_); #ifdef USE_API_NOISE - ESP_LOGCONFIG(TAG, " Noise encryption: %s", YESNO(this->noise_ctx_->has_psk())); - if (!this->noise_ctx_->has_psk()) { + ESP_LOGCONFIG(TAG, " Noise encryption: %s", YESNO(this->noise_ctx_.has_psk())); + if (!this->noise_ctx_.has_psk()) { ESP_LOGCONFIG(TAG, " Supports encryption: YES"); } #else @@ -493,7 +493,7 @@ bool APIServer::save_noise_psk(psk_t psk, bool make_active) { ESP_LOGW(TAG, "Key set in YAML"); return false; #else - auto &old_psk = this->noise_ctx_->get_psk(); + auto &old_psk = this->noise_ctx_.get_psk(); if (std::equal(old_psk.begin(), old_psk.end(), psk.begin())) { ESP_LOGW(TAG, "New PSK matches old"); return true; diff --git a/esphome/components/api/api_server.h b/esphome/components/api/api_server.h index 786cd63f44..428429418a 100644 --- a/esphome/components/api/api_server.h +++ b/esphome/components/api/api_server.h @@ -54,8 +54,8 @@ class APIServer : public Component, public Controller { #ifdef USE_API_NOISE bool save_noise_psk(psk_t psk, bool make_active = true); bool clear_noise_psk(bool make_active = true); - void set_noise_psk(psk_t psk) { noise_ctx_->set_psk(psk); } - std::shared_ptr get_noise_ctx() { return noise_ctx_; } + void set_noise_psk(psk_t psk) { this->noise_ctx_.set_psk(psk); } + APINoiseContext &get_noise_ctx() { return this->noise_ctx_; } #endif // USE_API_NOISE void handle_disconnect(APIConnection *conn); @@ -228,7 +228,7 @@ class APIServer : public Component, public Controller { // 7 bytes used, 1 byte padding #ifdef USE_API_NOISE - std::shared_ptr noise_ctx_ = std::make_shared(); + APINoiseContext noise_ctx_; ESPPreferenceObject noise_pref_; #endif // USE_API_NOISE }; diff --git a/esphome/components/mdns/mdns_component.cpp b/esphome/components/mdns/mdns_component.cpp index c81defd19f..4655907983 100644 --- a/esphome/components/mdns/mdns_component.cpp +++ b/esphome/components/mdns/mdns_component.cpp @@ -118,7 +118,7 @@ void MDNSComponent::compile_records_(StaticVectorget_noise_ctx()->has_psk(); + bool has_psk = api::global_api_server->get_noise_ctx().has_psk(); const char *encryption_key = has_psk ? TXT_API_ENCRYPTION : TXT_API_ENCRYPTION_SUPPORTED; txt_records.push_back({MDNS_STR(encryption_key), MDNS_STR(NOISE_ENCRYPTION)}); #endif diff --git a/esphome/components/mqtt/mqtt_client.cpp b/esphome/components/mqtt/mqtt_client.cpp index 9055b4421e..a810d98adf 100644 --- a/esphome/components/mqtt/mqtt_client.cpp +++ b/esphome/components/mqtt/mqtt_client.cpp @@ -140,7 +140,7 @@ void MQTTClientComponent::send_device_info_() { #endif #ifdef USE_API_NOISE - root[api::global_api_server->get_noise_ctx()->has_psk() ? "api_encryption" : "api_encryption_supported"] = + root[api::global_api_server->get_noise_ctx().has_psk() ? "api_encryption" : "api_encryption_supported"] = "Noise_NNpsk0_25519_ChaChaPoly_SHA256"; #endif }, From c146d924255eebc958e4f5b8fc5706c2021af494 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 24 Nov 2025 11:53:42 -0600 Subject: [PATCH 239/415] [api] Remove redundant socket pointer from APIFrameHelper (#11985) --- esphome/components/api/api_frame_helper.h | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/esphome/components/api/api_frame_helper.h b/esphome/components/api/api_frame_helper.h index 9aaada3cf7..d931a6e3a9 100644 --- a/esphome/components/api/api_frame_helper.h +++ b/esphome/components/api/api_frame_helper.h @@ -84,9 +84,7 @@ class APIFrameHelper { public: APIFrameHelper() = default; explicit APIFrameHelper(std::unique_ptr socket, const ClientInfo *client_info) - : socket_owned_(std::move(socket)), client_info_(client_info) { - socket_ = socket_owned_.get(); - } + : socket_(std::move(socket)), client_info_(client_info) {} virtual ~APIFrameHelper() = default; virtual APIError init() = 0; virtual APIError loop(); @@ -149,9 +147,8 @@ class APIFrameHelper { APIError write_raw_(const struct iovec *iov, int iovcnt, socket::Socket *socket, std::vector &tx_buf, const std::string &info, StateEnum &state, StateEnum failed_state); - // Pointers first (4 bytes each) - socket::Socket *socket_{nullptr}; - std::unique_ptr socket_owned_; + // Socket ownership (4 bytes on 32-bit, 8 bytes on 64-bit) + std::unique_ptr socket_; // Common state enum for all frame helpers // Note: Not all states are used by all implementations From d1a1bb446b9014ff4e591580102dfc07931099d9 Mon Sep 17 00:00:00 2001 From: Kevin Ahrendt Date: Mon, 24 Nov 2025 12:55:04 -0500 Subject: [PATCH 240/415] [wifi] Add runtime power saving mode control (#11478) Co-authored-by: J. Nick Koston --- esphome/components/wifi/__init__.py | 18 ++++- esphome/components/wifi/wifi_component.cpp | 90 +++++++++++++++++++++- esphome/components/wifi/wifi_component.h | 42 ++++++++++ esphome/core/defines.h | 1 + tests/components/wifi/test.esp32-idf.yaml | 11 +++ 5 files changed, 160 insertions(+), 2 deletions(-) diff --git a/esphome/components/wifi/__init__.py b/esphome/components/wifi/__init__.py index b9c0fa28a7..8a5e5329f1 100644 --- a/esphome/components/wifi/__init__.py +++ b/esphome/components/wifi/__init__.py @@ -607,6 +607,7 @@ async def wifi_disable_to_code(config, action_id, template_arg, args): KEEP_SCAN_RESULTS_KEY = "wifi_keep_scan_results" +RUNTIME_POWER_SAVE_KEY = "wifi_runtime_power_save" def request_wifi_scan_results(): @@ -619,13 +620,28 @@ def request_wifi_scan_results(): CORE.data[KEEP_SCAN_RESULTS_KEY] = True +def enable_runtime_power_save_control(): + """Enable runtime WiFi power save control. + + Components that need to dynamically switch WiFi power saving on/off for latency + performance (e.g., audio streaming, large data transfers) should call this + function during their code generation. This enables the request_high_performance() + and release_high_performance() APIs. + + Only supported on ESP32. + """ + CORE.data[RUNTIME_POWER_SAVE_KEY] = True + + @coroutine_with_priority(CoroPriority.FINAL) async def final_step(): - """Final code generation step to configure scan result retention.""" + """Final code generation step to configure optional WiFi features.""" if CORE.data.get(KEEP_SCAN_RESULTS_KEY, False): cg.add( cg.RawExpression("wifi::global_wifi_component->set_keep_scan_results(true)") ) + if CORE.data.get(RUNTIME_POWER_SAVE_KEY, False): + cg.add_define("USE_WIFI_RUNTIME_POWER_SAVE") @automation.register_action( diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index 23a4020453..41931a7785 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -330,6 +330,19 @@ float WiFiComponent::get_setup_priority() const { return setup_priority::WIFI; } void WiFiComponent::setup() { this->wifi_pre_setup_(); + +#if defined(USE_ESP32) && defined(USE_WIFI_RUNTIME_POWER_SAVE) + // Create semaphore for high-performance mode requests + // Start at 0, increment on request, decrement on release + this->high_performance_semaphore_ = xSemaphoreCreateCounting(UINT32_MAX, 0); + if (this->high_performance_semaphore_ == nullptr) { + ESP_LOGE(TAG, "Failed semaphore"); + } + + // Store the configured power save mode as baseline + this->configured_power_save_ = this->power_save_; +#endif + if (this->enable_on_boot_) { this->start(); } else { @@ -371,6 +384,19 @@ void WiFiComponent::start() { ESP_LOGV(TAG, "Setting Output Power Option failed"); } +#if defined(USE_ESP32) && defined(USE_WIFI_RUNTIME_POWER_SAVE) + // Synchronize power_save_ with semaphore state before applying + if (this->high_performance_semaphore_ != nullptr) { + UBaseType_t semaphore_count = uxSemaphoreGetCount(this->high_performance_semaphore_); + if (semaphore_count > 0) { + this->power_save_ = WIFI_POWER_SAVE_NONE; + this->is_high_performance_mode_ = true; + } else { + this->power_save_ = this->configured_power_save_; + this->is_high_performance_mode_ = false; + } + } +#endif if (!this->wifi_apply_power_save_()) { ESP_LOGV(TAG, "Setting Power Save Option failed"); } @@ -525,6 +551,31 @@ void WiFiComponent::loop() { } } } + +#if defined(USE_ESP32) && defined(USE_WIFI_RUNTIME_POWER_SAVE) + // Check if power save mode needs to be updated based on high-performance requests + if (this->high_performance_semaphore_ != nullptr) { + // Semaphore count directly represents active requests (starts at 0, increments on request) + UBaseType_t semaphore_count = uxSemaphoreGetCount(this->high_performance_semaphore_); + + if (semaphore_count > 0 && !this->is_high_performance_mode_) { + // Transition to high-performance mode (no power save) + ESP_LOGV(TAG, "Switching to high-performance mode (%" PRIu32 " active %s)", (uint32_t) semaphore_count, + semaphore_count == 1 ? "request" : "requests"); + this->power_save_ = WIFI_POWER_SAVE_NONE; + if (this->wifi_apply_power_save_()) { + this->is_high_performance_mode_ = true; + } + } else if (semaphore_count == 0 && this->is_high_performance_mode_) { + // Restore to configured power save mode + ESP_LOGV(TAG, "Restoring power save mode to configured setting"); + this->power_save_ = this->configured_power_save_; + if (this->wifi_apply_power_save_()) { + this->is_high_performance_mode_ = false; + } + } + } +#endif } WiFiComponent::WiFiComponent() { global_wifi_component = this; } @@ -1567,7 +1618,12 @@ bool WiFiComponent::is_connected() { return this->state_ == WIFI_COMPONENT_STATE_STA_CONNECTED && this->wifi_sta_connect_status_() == WiFiSTAConnectStatus::CONNECTED && !this->error_from_callback_; } -void WiFiComponent::set_power_save_mode(WiFiPowerSaveMode power_save) { this->power_save_ = power_save; } +void WiFiComponent::set_power_save_mode(WiFiPowerSaveMode power_save) { + this->power_save_ = power_save; +#if defined(USE_ESP32) && defined(USE_WIFI_RUNTIME_POWER_SAVE) + this->configured_power_save_ = power_save; +#endif +} void WiFiComponent::set_passive_scan(bool passive) { this->passive_scan_ = passive; } @@ -1586,6 +1642,38 @@ bool WiFiComponent::is_esp32_improv_active_() { #endif } +#if defined(USE_ESP32) && defined(USE_WIFI_RUNTIME_POWER_SAVE) +bool WiFiComponent::request_high_performance() { + // Already configured for high performance - request satisfied + if (this->configured_power_save_ == WIFI_POWER_SAVE_NONE) { + return true; + } + + // Semaphore initialization failed + if (this->high_performance_semaphore_ == nullptr) { + return false; + } + + // Give the semaphore (non-blocking). This increments the count. + return xSemaphoreGive(this->high_performance_semaphore_) == pdTRUE; +} + +bool WiFiComponent::release_high_performance() { + // Already configured for high performance - nothing to release + if (this->configured_power_save_ == WIFI_POWER_SAVE_NONE) { + return true; + } + + // Semaphore initialization failed + if (this->high_performance_semaphore_ == nullptr) { + return false; + } + + // Take the semaphore (non-blocking). This decrements the count. + return xSemaphoreTake(this->high_performance_semaphore_, 0) == pdTRUE; +} +#endif // USE_ESP32 && USE_WIFI_RUNTIME_POWER_SAVE + #ifdef USE_WIFI_FAST_CONNECT bool WiFiComponent::load_fast_connect_settings_(WiFiAP ¶ms) { SavedWifiFastConnectSettings fast_connect_save{}; diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index 441606a2c1..0dac80ad21 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -49,6 +49,11 @@ extern "C" { #include #endif +#if defined(USE_ESP32) && defined(USE_WIFI_RUNTIME_POWER_SAVE) +#include +#include +#endif + namespace esphome { namespace wifi { @@ -365,6 +370,37 @@ class WiFiComponent : public Component { int32_t get_wifi_channel(); +#ifdef USE_WIFI_RUNTIME_POWER_SAVE + /** Request high-performance mode (no power saving) for improved WiFi latency. + * + * Components that need maximum WiFi performance (e.g., audio streaming, large data transfers) + * can call this method to temporarily disable WiFi power saving. Multiple components can + * request high performance simultaneously using a counting semaphore. + * + * Power saving will be restored to the YAML-configured mode when all components have + * called release_high_performance(). + * + * Note: Only supported on ESP32. + * + * @return true if request was satisfied (high-performance mode active or already configured), + * false if operation failed (semaphore error) + */ + bool request_high_performance(); + + /** Release a high-performance mode request. + * + * Should be called when a component no longer needs maximum WiFi latency. + * When all requests are released (semaphore count reaches zero), WiFi power saving + * is restored to the YAML-configured mode. + * + * Note: Only supported on ESP32. + * + * @return true if release was successful (or already in high-performance config), + * false if operation failed (semaphore error) + */ + bool release_high_performance(); +#endif // USE_WIFI_RUNTIME_POWER_SAVE + protected: #ifdef USE_WIFI_AP void setup_ap_config_(); @@ -535,6 +571,12 @@ class WiFiComponent : public Component { bool keep_scan_results_{false}; bool did_scan_this_cycle_{false}; bool skip_cooldown_next_cycle_{false}; +#if defined(USE_ESP32) && defined(USE_WIFI_RUNTIME_POWER_SAVE) + WiFiPowerSaveMode configured_power_save_{WIFI_POWER_SAVE_NONE}; + bool is_high_performance_mode_{false}; + + SemaphoreHandle_t high_performance_semaphore_{nullptr}; +#endif // Pointers at the end (naturally aligned) Trigger<> *connect_trigger_{new Trigger<>()}; diff --git a/esphome/core/defines.h b/esphome/core/defines.h index 5e7f51e04c..4b24c395b9 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -210,6 +210,7 @@ #define USE_WEBSERVER_SORTING #define USE_WIFI_11KV_SUPPORT #define USE_WIFI_FAST_CONNECT +#define USE_WIFI_RUNTIME_POWER_SAVE #define USB_HOST_MAX_REQUESTS 16 #ifdef USE_ARDUINO diff --git a/tests/components/wifi/test.esp32-idf.yaml b/tests/components/wifi/test.esp32-idf.yaml index 6b3ef20963..3e01d7f990 100644 --- a/tests/components/wifi/test.esp32-idf.yaml +++ b/tests/components/wifi/test.esp32-idf.yaml @@ -1,5 +1,16 @@ psram: +# Tests the high performance request and release; requires the USE_WIFI_RUNTIME_POWER_SAVE define +esphome: + platformio_options: + build_flags: + - "-DUSE_WIFI_RUNTIME_POWER_SAVE" + on_boot: + - then: + - lambda: |- + esphome::wifi::global_wifi_component->request_high_performance(); + esphome::wifi::global_wifi_component->release_high_performance(); + wifi: use_psram: true min_auth_mode: WPA From 84b2bea7060d6c6be20db6e3e5d2a6901af37aa7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 24 Nov 2025 12:12:05 -0600 Subject: [PATCH 241/415] fix dual dep --- esphome/core/component.h | 3 +++ 1 file changed, 3 insertions(+) diff --git a/esphome/core/component.h b/esphome/core/component.h index 63d758d25f..82ac66f23c 100644 --- a/esphome/core/component.h +++ b/esphome/core/component.h @@ -161,7 +161,10 @@ class Component { // Remove before 2026.6.0 ESPDEPRECATED("Use mark_failed(LOG_STR(message)) instead. Will stop working in 2026.6.0", "2025.12.0") void mark_failed(const char *message) { +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wdeprecated-declarations" this->status_set_error(message); +#pragma GCC diagnostic pop this->mark_failed(); } From 9d49ca58b5ade7971a2c1e85033d76a51db75d9d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 24 Nov 2025 12:16:52 -0600 Subject: [PATCH 242/415] silence warning for nullptr --- esphome/core/component.cpp | 6 ++++++ esphome/core/component.h | 3 ++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/esphome/core/component.cpp b/esphome/core/component.cpp index aa487d8ef5..7d60a3d792 100644 --- a/esphome/core/component.cpp +++ b/esphome/core/component.cpp @@ -333,6 +333,12 @@ void Component::status_set_warning(const LogString *message) { ESP_LOGW(TAG, "%s set Warning flag: %s", LOG_STR_ARG(this->get_component_log_str()), message ? LOG_STR_ARG(message) : LOG_STR_LITERAL("unspecified")); } +void Component::status_set_error() { +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wdeprecated-declarations" + this->status_set_error(nullptr); +#pragma GCC diagnostic pop +} void Component::status_set_error(const char *message) { if ((this->component_state_ & STATUS_LED_ERROR) != 0) return; diff --git a/esphome/core/component.h b/esphome/core/component.h index 82ac66f23c..db716d5b4a 100644 --- a/esphome/core/component.h +++ b/esphome/core/component.h @@ -227,9 +227,10 @@ class Component { void status_set_warning(const char *message = nullptr); void status_set_warning(const LogString *message); + void status_set_error(); // Set error flag without message // Remove before 2026.6.0 ESPDEPRECATED("Use status_set_error(LOG_STR(message)) instead. Will stop working in 2026.6.0", "2025.12.0") - void status_set_error(const char *message = nullptr); + void status_set_error(const char *message); void status_set_error(const LogString *message); void status_clear_warning(); From 7496d20ae6893ce3fd950673060adf9155375164 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 24 Nov 2025 12:18:59 -0600 Subject: [PATCH 243/415] fix ambiguous --- esphome/core/component.cpp | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/esphome/core/component.cpp b/esphome/core/component.cpp index 7d60a3d792..5e6ace8873 100644 --- a/esphome/core/component.cpp +++ b/esphome/core/component.cpp @@ -333,12 +333,7 @@ void Component::status_set_warning(const LogString *message) { ESP_LOGW(TAG, "%s set Warning flag: %s", LOG_STR_ARG(this->get_component_log_str()), message ? LOG_STR_ARG(message) : LOG_STR_LITERAL("unspecified")); } -void Component::status_set_error() { -#pragma GCC diagnostic push -#pragma GCC diagnostic ignored "-Wdeprecated-declarations" - this->status_set_error(nullptr); -#pragma GCC diagnostic pop -} +void Component::status_set_error() { this->status_set_error((const LogString *) nullptr); } void Component::status_set_error(const char *message) { if ((this->component_state_ & STATUS_LED_ERROR) != 0) return; From 7a73a524b94008c31cdcd895feaa5131335af975 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 24 Nov 2025 12:21:09 -0600 Subject: [PATCH 244/415] [logger] Eliminate strlen overhead on LibreTiny (#11938) --- esphome/components/logger/logger.h | 4 ++-- esphome/components/logger/logger_libretiny.cpp | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/esphome/components/logger/logger.h b/esphome/components/logger/logger.h index 8ba3dacacb..6a8b640331 100644 --- a/esphome/components/logger/logger.h +++ b/esphome/components/logger/logger.h @@ -72,11 +72,11 @@ static constexpr uint16_t MAX_HEADER_SIZE = 128; static constexpr size_t MAX_POINTER_REPRESENTATION = 2 + sizeof(void *) * 2 + 1; // Platform-specific: does write_msg_ add its own newline? -// false: Caller must add newline to buffer before calling write_msg_ (ESP32, ESP8266) +// false: Caller must add newline to buffer before calling write_msg_ (ESP32, ESP8266, LibreTiny) // Allows single write call with newline included for efficiency // true: write_msg_ adds newline itself via puts()/println() (other platforms) // Newline should NOT be added to buffer -#if defined(USE_ESP32) || defined(USE_ESP8266) +#if defined(USE_ESP32) || defined(USE_ESP8266) || defined(USE_LIBRETINY) static constexpr bool WRITE_MSG_ADDS_NEWLINE = false; #else static constexpr bool WRITE_MSG_ADDS_NEWLINE = true; diff --git a/esphome/components/logger/logger_libretiny.cpp b/esphome/components/logger/logger_libretiny.cpp index b8017b841d..cdf55e710c 100644 --- a/esphome/components/logger/logger_libretiny.cpp +++ b/esphome/components/logger/logger_libretiny.cpp @@ -49,7 +49,7 @@ void Logger::pre_setup() { ESP_LOGI(TAG, "Log initialized"); } -void HOT Logger::write_msg_(const char *msg, size_t) { this->hw_serial_->println(msg); } +void HOT Logger::write_msg_(const char *msg, size_t len) { this->hw_serial_->write(msg, len); } const LogString *Logger::get_uart_selection_() { switch (this->uart_) { From bc7f67e0a29dd53db83cfcb993e722059740211e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 24 Nov 2025 12:23:51 -0600 Subject: [PATCH 245/415] clear --- esphome/core/component.h | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/esphome/core/component.h b/esphome/core/component.h index db716d5b4a..51a9290e8b 100644 --- a/esphome/core/component.h +++ b/esphome/core/component.h @@ -159,7 +159,9 @@ class Component { virtual void mark_failed(); // Remove before 2026.6.0 - ESPDEPRECATED("Use mark_failed(LOG_STR(message)) instead. Will stop working in 2026.6.0", "2025.12.0") + ESPDEPRECATED("Use mark_failed(LOG_STR(\"static string literal\")) instead. Do NOT use .c_str() from temporary " + "strings. Will stop working in 2026.6.0", + "2025.12.0") void mark_failed(const char *message) { #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wdeprecated-declarations" @@ -229,7 +231,9 @@ class Component { void status_set_error(); // Set error flag without message // Remove before 2026.6.0 - ESPDEPRECATED("Use status_set_error(LOG_STR(message)) instead. Will stop working in 2026.6.0", "2025.12.0") + ESPDEPRECATED("Use status_set_error(LOG_STR(\"static string literal\")) instead. Do NOT use .c_str() from temporary " + "strings. Will stop working in 2026.6.0", + "2025.12.0") void status_set_error(const char *message); void status_set_error(const LogString *message); From d2483347d0ed65bc0675db1cb02c7c19ce159539 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 24 Nov 2025 12:44:41 -0600 Subject: [PATCH 246/415] [wifi] Use ESP-IDF IP formatting macros directly to eliminate heap allocations --- esphome/components/wifi/wifi_component_esp_idf.cpp | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/esphome/components/wifi/wifi_component_esp_idf.cpp b/esphome/components/wifi/wifi_component_esp_idf.cpp index 4aac03885a..e6e914c0b4 100644 --- a/esphome/components/wifi/wifi_component_esp_idf.cpp +++ b/esphome/components/wifi/wifi_component_esp_idf.cpp @@ -603,10 +603,6 @@ const char *get_auth_mode_str(uint8_t mode) { } } -std::string format_ip4_addr(const esp_ip4_addr_t &ip) { return str_snprintf(IPSTR, 15, IP2STR(&ip)); } -#if LWIP_IPV6 -std::string format_ip6_addr(const esp_ip6_addr_t &ip) { return str_snprintf(IPV6STR, 39, IPV62STR(ip)); } -#endif /* LWIP_IPV6 */ const char *get_disconnect_reason_str(uint8_t reason) { switch (reason) { case WIFI_REASON_AUTH_EXPIRE: @@ -761,14 +757,13 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) { #if USE_NETWORK_IPV6 esp_netif_create_ip6_linklocal(s_sta_netif); #endif /* USE_NETWORK_IPV6 */ - ESP_LOGV(TAG, "static_ip=%s gateway=%s", format_ip4_addr(it.ip_info.ip).c_str(), - format_ip4_addr(it.ip_info.gw).c_str()); + ESP_LOGV(TAG, "static_ip=" IPSTR " gateway=" IPSTR, IP2STR(&it.ip_info.ip), IP2STR(&it.ip_info.gw)); this->got_ipv4_address_ = true; #if USE_NETWORK_IPV6 } else if (data->event_base == IP_EVENT && data->event_id == IP_EVENT_GOT_IP6) { const auto &it = data->data.ip_got_ip6; - ESP_LOGV(TAG, "IPv6 address=%s", format_ip6_addr(it.ip6_info.ip).c_str()); + ESP_LOGV(TAG, "IPv6 address=" IPV6STR, IPV62STR(it.ip6_info.ip)); this->num_ipv6_addresses_++; #endif /* USE_NETWORK_IPV6 */ @@ -832,7 +827,7 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) { } else if (data->event_base == IP_EVENT && data->event_id == IP_EVENT_AP_STAIPASSIGNED) { const auto &it = data->data.ip_ap_staipassigned; - ESP_LOGV(TAG, "AP client assigned IP %s", format_ip4_addr(it.ip).c_str()); + ESP_LOGV(TAG, "AP client assigned IP " IPSTR, IP2STR(&it.ip)); } } From 0dd842744a1c5ab50a0bae97fa51766f82bbd506 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 24 Nov 2025 13:44:09 -0600 Subject: [PATCH 247/415] Bump github/codeql-action from 4.31.4 to 4.31.5 (#12080) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/codeql.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 80fab8819a..d10c8bf267 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -58,7 +58,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@e12f0178983d466f2f6028f5cc7a6d786fd97f4b # v4.31.4 + uses: github/codeql-action/init@fdbfb4d2750291e159f0156def62b853c2798ca2 # v4.31.5 with: languages: ${{ matrix.language }} build-mode: ${{ matrix.build-mode }} @@ -86,6 +86,6 @@ jobs: exit 1 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@e12f0178983d466f2f6028f5cc7a6d786fd97f4b # v4.31.4 + uses: github/codeql-action/analyze@fdbfb4d2750291e159f0156def62b853c2798ca2 # v4.31.5 with: category: "/language:${{matrix.language}}" From 378fc4120ae7621bd03ecebc8a51808dc890b535 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 24 Nov 2025 13:44:27 -0600 Subject: [PATCH 248/415] Bump peter-evans/create-pull-request from 7.0.8 to 7.0.9 (#12082) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/sync-device-classes.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/sync-device-classes.yml b/.github/workflows/sync-device-classes.yml index 2e36dc517d..8f95fa68ee 100644 --- a/.github/workflows/sync-device-classes.yml +++ b/.github/workflows/sync-device-classes.yml @@ -41,7 +41,7 @@ jobs: python script/run-in-env.py pre-commit run --all-files - name: Commit changes - uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e # v7.0.8 + uses: peter-evans/create-pull-request@84ae59a2cdc2258d6fa0732dd66352dddae2a412 # v7.0.9 with: commit-message: "Synchronise Device Classes from Home Assistant" committer: esphomebot From e2cd0ccd0e05a43fff011c008425cb6bcfa457c6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 24 Nov 2025 13:44:43 -0600 Subject: [PATCH 249/415] Bump actions/create-github-app-token from 2.1.4 to 2.2.0 (#12081) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/auto-label-pr.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/auto-label-pr.yml b/.github/workflows/auto-label-pr.yml index 8d8e08a5fc..998f3315c6 100644 --- a/.github/workflows/auto-label-pr.yml +++ b/.github/workflows/auto-label-pr.yml @@ -26,7 +26,7 @@ jobs: - name: Generate a token id: generate-token - uses: actions/create-github-app-token@67018539274d69449ef7c02e8e71183d1719ab42 # v2 + uses: actions/create-github-app-token@7e473efe3cb98aa54f8d4bac15400b15fad77d94 # v2 with: app-id: ${{ secrets.ESPHOME_GITHUB_APP_ID }} private-key: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }} From a0440603b7ea6585680a28de05bc169dbead0739 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 24 Nov 2025 13:45:06 -0600 Subject: [PATCH 250/415] [wifi] Use ESP-IDF IP formatting macros directly to eliminate heap allocations (#12078) --- esphome/components/wifi/wifi_component_esp_idf.cpp | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/esphome/components/wifi/wifi_component_esp_idf.cpp b/esphome/components/wifi/wifi_component_esp_idf.cpp index 4aac03885a..e6e914c0b4 100644 --- a/esphome/components/wifi/wifi_component_esp_idf.cpp +++ b/esphome/components/wifi/wifi_component_esp_idf.cpp @@ -603,10 +603,6 @@ const char *get_auth_mode_str(uint8_t mode) { } } -std::string format_ip4_addr(const esp_ip4_addr_t &ip) { return str_snprintf(IPSTR, 15, IP2STR(&ip)); } -#if LWIP_IPV6 -std::string format_ip6_addr(const esp_ip6_addr_t &ip) { return str_snprintf(IPV6STR, 39, IPV62STR(ip)); } -#endif /* LWIP_IPV6 */ const char *get_disconnect_reason_str(uint8_t reason) { switch (reason) { case WIFI_REASON_AUTH_EXPIRE: @@ -761,14 +757,13 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) { #if USE_NETWORK_IPV6 esp_netif_create_ip6_linklocal(s_sta_netif); #endif /* USE_NETWORK_IPV6 */ - ESP_LOGV(TAG, "static_ip=%s gateway=%s", format_ip4_addr(it.ip_info.ip).c_str(), - format_ip4_addr(it.ip_info.gw).c_str()); + ESP_LOGV(TAG, "static_ip=" IPSTR " gateway=" IPSTR, IP2STR(&it.ip_info.ip), IP2STR(&it.ip_info.gw)); this->got_ipv4_address_ = true; #if USE_NETWORK_IPV6 } else if (data->event_base == IP_EVENT && data->event_id == IP_EVENT_GOT_IP6) { const auto &it = data->data.ip_got_ip6; - ESP_LOGV(TAG, "IPv6 address=%s", format_ip6_addr(it.ip6_info.ip).c_str()); + ESP_LOGV(TAG, "IPv6 address=" IPV6STR, IPV62STR(it.ip6_info.ip)); this->num_ipv6_addresses_++; #endif /* USE_NETWORK_IPV6 */ @@ -832,7 +827,7 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) { } else if (data->event_base == IP_EVENT && data->event_id == IP_EVENT_AP_STAIPASSIGNED) { const auto &it = data->data.ip_ap_staipassigned; - ESP_LOGV(TAG, "AP client assigned IP %s", format_ip4_addr(it.ip).c_str()); + ESP_LOGV(TAG, "AP client assigned IP " IPSTR, IP2STR(&it.ip)); } } From 909baf5e7a8daee99775775220367cf994a377db Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 24 Nov 2025 13:45:29 -0600 Subject: [PATCH 251/415] [prometheus] Use current_option() instead of deprecated .state for select entities (#12079) --- esphome/components/prometheus/prometheus_handler.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/prometheus/prometheus_handler.cpp b/esphome/components/prometheus/prometheus_handler.cpp index 6b57a3f718..812b547860 100644 --- a/esphome/components/prometheus/prometheus_handler.cpp +++ b/esphome/components/prometheus/prometheus_handler.cpp @@ -726,7 +726,7 @@ void PrometheusHandler::select_row_(AsyncResponseStream *stream, select::Select stream->print(ESPHOME_F("\",name=\"")); stream->print(relabel_name_(obj).c_str()); stream->print(ESPHOME_F("\",value=\"")); - stream->print(obj->state.c_str()); + stream->print(obj->current_option()); stream->print(ESPHOME_F("\"} ")); stream->print(ESPHOME_F("1.0")); stream->print(ESPHOME_F("\n")); From 97ba67f4eee3a85e0e416565cba3023900f66f0d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 24 Nov 2025 13:45:56 -0600 Subject: [PATCH 252/415] [core] Deprecate unsafe const char* APIs in mark_failed() and status_set_error(), add LogString* overloads (#12021) --- .../absolute_humidity/absolute_humidity.cpp | 2 +- esphome/components/aht10/aht10.cpp | 2 +- esphome/components/bh1900nux/bh1900nux.cpp | 2 +- .../components/bme280_base/bme280_base.cpp | 18 +++--- .../components/bmp280_base/bmp280_base.cpp | 16 +++--- esphome/components/camera/camera.cpp | 2 +- .../cst816/touchscreen/cst816_touchscreen.cpp | 4 +- esphome/components/epaper_spi/epaper_spi.cpp | 4 +- .../update/esp32_hosted_update.cpp | 10 ++-- esphome/components/esp_ldo/esp_ldo.cpp | 2 +- esphome/components/gdk101/gdk101.cpp | 6 +- .../gt911/touchscreen/gt911_touchscreen.cpp | 4 +- .../update/http_request_update.cpp | 9 +-- esphome/components/lvgl/lvgl_esphome.cpp | 4 +- esphome/components/max17043/max17043.cpp | 4 +- esphome/components/mipi_dsi/mipi_dsi.cpp | 22 ++++---- esphome/components/mipi_dsi/mipi_dsi.h | 2 +- esphome/components/mipi_rgb/mipi_rgb.cpp | 8 +-- esphome/components/mipi_spi/mipi_spi.h | 2 +- .../mixer/speaker/mixer_speaker.cpp | 13 +++-- esphome/components/nau7802/nau7802.cpp | 2 +- .../packet_transport/packet_transport.cpp | 2 +- esphome/components/qmp6988/qmp6988.cpp | 2 +- .../resampler/speaker/resampler_speaker.cpp | 12 ++-- esphome/components/sht4x/sht4x.cpp | 2 +- esphome/components/stts22h/stts22h.cpp | 12 ++-- esphome/components/udp/udp_component.cpp | 10 ++-- .../components/usb_host/usb_host_client.cpp | 2 +- .../usb_host/usb_host_component.cpp | 2 +- esphome/components/usb_uart/usb_uart.cpp | 4 +- .../voice_assistant/voice_assistant.cpp | 2 +- .../components/wake_on_lan/wake_on_lan.cpp | 2 +- esphome/core/component.cpp | 55 ++++++++++++++----- esphome/core/component.h | 21 ++++++- 34 files changed, 157 insertions(+), 109 deletions(-) diff --git a/esphome/components/absolute_humidity/absolute_humidity.cpp b/esphome/components/absolute_humidity/absolute_humidity.cpp index 2c5603ee3d..d16a024d86 100644 --- a/esphome/components/absolute_humidity/absolute_humidity.cpp +++ b/esphome/components/absolute_humidity/absolute_humidity.cpp @@ -87,7 +87,7 @@ void AbsoluteHumidityComponent::loop() { break; default: this->publish_state(NAN); - this->status_set_error("Invalid saturation vapor pressure equation selection!"); + this->status_set_error(LOG_STR("Invalid saturation vapor pressure equation selection!")); return; } ESP_LOGD(TAG, "Saturation vapor pressure %f kPa", es); diff --git a/esphome/components/aht10/aht10.cpp b/esphome/components/aht10/aht10.cpp index 53c712a7a7..03d9d9cd9e 100644 --- a/esphome/components/aht10/aht10.cpp +++ b/esphome/components/aht10/aht10.cpp @@ -83,7 +83,7 @@ void AHT10Component::setup() { void AHT10Component::restart_read_() { if (this->read_count_ == AHT10_ATTEMPTS) { this->read_count_ = 0; - this->status_set_error("Reading timed out"); + this->status_set_error(LOG_STR("Reading timed out")); return; } this->read_count_++; diff --git a/esphome/components/bh1900nux/bh1900nux.cpp b/esphome/components/bh1900nux/bh1900nux.cpp index 96a06adaa0..0e71bd6532 100644 --- a/esphome/components/bh1900nux/bh1900nux.cpp +++ b/esphome/components/bh1900nux/bh1900nux.cpp @@ -23,7 +23,7 @@ void BH1900NUXSensor::setup() { i2c::ErrorCode result_code = this->write_register(SOFT_RESET_REG, &SOFT_RESET_PAYLOAD, 1); // Software Reset to check communication if (result_code != i2c::ERROR_OK) { - this->mark_failed(ESP_LOG_MSG_COMM_FAIL); + this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL)); return; } } diff --git a/esphome/components/bme280_base/bme280_base.cpp b/esphome/components/bme280_base/bme280_base.cpp index 86b65d361d..c5d4c9c0a5 100644 --- a/esphome/components/bme280_base/bme280_base.cpp +++ b/esphome/components/bme280_base/bme280_base.cpp @@ -100,18 +100,18 @@ void BME280Component::setup() { if (!this->read_byte(BME280_REGISTER_CHIPID, &chip_id)) { this->error_code_ = COMMUNICATION_FAILED; - this->mark_failed(ESP_LOG_MSG_COMM_FAIL); + this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL)); return; } if (chip_id != 0x60) { this->error_code_ = WRONG_CHIP_ID; - this->mark_failed(BME280_ERROR_WRONG_CHIP_ID); + this->mark_failed(LOG_STR(BME280_ERROR_WRONG_CHIP_ID)); return; } // Send a soft reset. if (!this->write_byte(BME280_REGISTER_RESET, BME280_SOFT_RESET)) { - this->mark_failed("Reset failed"); + this->mark_failed(LOG_STR("Reset failed")); return; } // Wait until the NVM data has finished loading. @@ -120,12 +120,12 @@ void BME280Component::setup() { do { // NOLINT delay(2); if (!this->read_byte(BME280_REGISTER_STATUS, &status)) { - this->mark_failed("Error reading status register"); + this->mark_failed(LOG_STR("Error reading status register")); return; } } while ((status & BME280_STATUS_IM_UPDATE) && (--retry)); if (status & BME280_STATUS_IM_UPDATE) { - this->mark_failed("Timeout loading NVM"); + this->mark_failed(LOG_STR("Timeout loading NVM")); return; } @@ -153,26 +153,26 @@ void BME280Component::setup() { uint8_t humid_control_val = 0; if (!this->read_byte(BME280_REGISTER_CONTROLHUMID, &humid_control_val)) { - this->mark_failed("Read humidity control"); + this->mark_failed(LOG_STR("Read humidity control")); return; } humid_control_val &= ~0b00000111; humid_control_val |= this->humidity_oversampling_ & 0b111; if (!this->write_byte(BME280_REGISTER_CONTROLHUMID, humid_control_val)) { - this->mark_failed("Write humidity control"); + this->mark_failed(LOG_STR("Write humidity control")); return; } uint8_t config_register = 0; if (!this->read_byte(BME280_REGISTER_CONFIG, &config_register)) { - this->mark_failed("Read config"); + this->mark_failed(LOG_STR("Read config")); return; } config_register &= ~0b11111100; config_register |= 0b101 << 5; // 1000 ms standby time config_register |= (this->iir_filter_ & 0b111) << 2; if (!this->write_byte(BME280_REGISTER_CONFIG, config_register)) { - this->mark_failed("Write config"); + this->mark_failed(LOG_STR("Write config")); return; } } diff --git a/esphome/components/bmp280_base/bmp280_base.cpp b/esphome/components/bmp280_base/bmp280_base.cpp index 39654f5875..728eead521 100644 --- a/esphome/components/bmp280_base/bmp280_base.cpp +++ b/esphome/components/bmp280_base/bmp280_base.cpp @@ -65,23 +65,23 @@ void BMP280Component::setup() { // https://community.st.com/t5/stm32-mcus-products/issue-with-reading-bmp280-chip-id-using-spi/td-p/691855 if (!this->bmp_read_byte(0xD0, &chip_id)) { this->error_code_ = COMMUNICATION_FAILED; - this->mark_failed(ESP_LOG_MSG_COMM_FAIL); + this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL)); return; } if (!this->bmp_read_byte(0xD0, &chip_id)) { this->error_code_ = COMMUNICATION_FAILED; - this->mark_failed(ESP_LOG_MSG_COMM_FAIL); + this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL)); return; } if (chip_id != 0x58) { this->error_code_ = WRONG_CHIP_ID; - this->mark_failed(BMP280_ERROR_WRONG_CHIP_ID); + this->mark_failed(LOG_STR(BMP280_ERROR_WRONG_CHIP_ID)); return; } // Send a soft reset. if (!this->bmp_write_byte(BMP280_REGISTER_RESET, BMP280_SOFT_RESET)) { - this->mark_failed("Reset failed"); + this->mark_failed(LOG_STR("Reset failed")); return; } // Wait until the NVM data has finished loading. @@ -90,12 +90,12 @@ void BMP280Component::setup() { do { delay(2); if (!this->bmp_read_byte(BMP280_REGISTER_STATUS, &status)) { - this->mark_failed("Error reading status register"); + this->mark_failed(LOG_STR("Error reading status register")); return; } } while ((status & BMP280_STATUS_IM_UPDATE) && (--retry)); if (status & BMP280_STATUS_IM_UPDATE) { - this->mark_failed("Timeout loading NVM"); + this->mark_failed(LOG_STR("Timeout loading NVM")); return; } @@ -116,14 +116,14 @@ void BMP280Component::setup() { uint8_t config_register = 0; if (!this->bmp_read_byte(BMP280_REGISTER_CONFIG, &config_register)) { - this->mark_failed("Read config"); + this->mark_failed(LOG_STR("Read config")); return; } config_register &= ~0b11111100; config_register |= 0b000 << 5; // 0.5 ms standby time config_register |= (this->iir_filter_ & 0b111) << 2; if (!this->bmp_write_byte(BMP280_REGISTER_CONFIG, config_register)) { - this->mark_failed("Write config"); + this->mark_failed(LOG_STR("Write config")); return; } } diff --git a/esphome/components/camera/camera.cpp b/esphome/components/camera/camera.cpp index 3bd632af5c..66b8138f38 100644 --- a/esphome/components/camera/camera.cpp +++ b/esphome/components/camera/camera.cpp @@ -8,7 +8,7 @@ Camera *Camera::global_camera = nullptr; Camera::Camera() { if (global_camera != nullptr) { - this->status_set_error("Multiple cameras are configured, but only one is supported."); + this->status_set_error(LOG_STR("Multiple cameras are configured, but only one is supported.")); this->mark_failed(); return; } diff --git a/esphome/components/cst816/touchscreen/cst816_touchscreen.cpp b/esphome/components/cst816/touchscreen/cst816_touchscreen.cpp index 0560f1b475..f6280a75a1 100644 --- a/esphome/components/cst816/touchscreen/cst816_touchscreen.cpp +++ b/esphome/components/cst816/touchscreen/cst816_touchscreen.cpp @@ -20,13 +20,13 @@ void CST816Touchscreen::continue_setup_() { break; default: ESP_LOGE(TAG, "Unknown chip ID: 0x%02X", this->chip_id_); - this->status_set_error("Unknown chip ID"); + this->status_set_error(LOG_STR("Unknown chip ID")); this->mark_failed(); return; } this->write_byte(REG_IRQ_CTL, IRQ_EN_MOTION); } else if (!this->skip_probe_) { - this->status_set_error("Failed to read chip id"); + this->status_set_error(LOG_STR("Failed to read chip id")); this->mark_failed(); return; } diff --git a/esphome/components/epaper_spi/epaper_spi.cpp b/esphome/components/epaper_spi/epaper_spi.cpp index cf6a0b0c3d..39959cd743 100644 --- a/esphome/components/epaper_spi/epaper_spi.cpp +++ b/esphome/components/epaper_spi/epaper_spi.cpp @@ -22,7 +22,7 @@ const char *EPaperBase::epaper_state_to_string_() { void EPaperBase::setup() { if (!this->init_buffer_(this->buffer_length_)) { - this->mark_failed("Failed to initialise buffer"); + this->mark_failed(LOG_STR("Failed to initialise buffer")); return; } this->setup_pins_(); @@ -246,7 +246,7 @@ void EPaperBase::initialise_() { auto length = this->init_sequence_length_; while (index != length) { if (length - index < 2) { - this->mark_failed("Malformed init sequence"); + this->mark_failed(LOG_STR("Malformed init sequence")); return; } const uint8_t cmd = sequence[index++]; diff --git a/esphome/components/esp32_hosted/update/esp32_hosted_update.cpp b/esphome/components/esp32_hosted/update/esp32_hosted_update.cpp index adbcc5bf11..f34a0ae10e 100644 --- a/esphome/components/esp32_hosted/update/esp32_hosted_update.cpp +++ b/esphome/components/esp32_hosted/update/esp32_hosted_update.cpp @@ -88,7 +88,7 @@ void Esp32HostedUpdate::perform(bool force) { hasher.add(this->firmware_data_, this->firmware_size_); hasher.calculate(); if (!hasher.equals_bytes(this->firmware_sha256_.data())) { - this->status_set_error("SHA256 verification failed"); + this->status_set_error(LOG_STR("SHA256 verification failed")); this->publish_state(); return; } @@ -105,7 +105,7 @@ void Esp32HostedUpdate::perform(bool force) { if (err != ESP_OK) { ESP_LOGE(TAG, "Failed to begin OTA: %s", esp_err_to_name(err)); this->state_ = prev_state; - this->status_set_error("Failed to begin OTA"); + this->status_set_error(LOG_STR("Failed to begin OTA")); this->publish_state(); return; } @@ -121,7 +121,7 @@ void Esp32HostedUpdate::perform(bool force) { ESP_LOGE(TAG, "Failed to write OTA data: %s", esp_err_to_name(err)); esp_hosted_slave_ota_end(); // NOLINT this->state_ = prev_state; - this->status_set_error("Failed to write OTA data"); + this->status_set_error(LOG_STR("Failed to write OTA data")); this->publish_state(); return; } @@ -134,7 +134,7 @@ void Esp32HostedUpdate::perform(bool force) { if (err != ESP_OK) { ESP_LOGE(TAG, "Failed to end OTA: %s", esp_err_to_name(err)); this->state_ = prev_state; - this->status_set_error("Failed to end OTA"); + this->status_set_error(LOG_STR("Failed to end OTA")); this->publish_state(); return; } @@ -144,7 +144,7 @@ void Esp32HostedUpdate::perform(bool force) { if (err != ESP_OK) { ESP_LOGE(TAG, "Failed to activate OTA: %s", esp_err_to_name(err)); this->state_ = prev_state; - this->status_set_error("Failed to activate OTA"); + this->status_set_error(LOG_STR("Failed to activate OTA")); this->publish_state(); return; } diff --git a/esphome/components/esp_ldo/esp_ldo.cpp b/esphome/components/esp_ldo/esp_ldo.cpp index 9ea7000b70..5e3d4159f3 100644 --- a/esphome/components/esp_ldo/esp_ldo.cpp +++ b/esphome/components/esp_ldo/esp_ldo.cpp @@ -15,7 +15,7 @@ void EspLdo::setup() { auto err = esp_ldo_acquire_channel(&config, &this->handle_); if (err != ESP_OK) { ESP_LOGE(TAG, "Failed to acquire LDO channel %d with voltage %fV", this->channel_, this->voltage_); - this->mark_failed("Failed to acquire LDO channel"); + this->mark_failed(LOG_STR("Failed to acquire LDO channel")); } else { ESP_LOGD(TAG, "Acquired LDO channel %d with voltage %fV", this->channel_, this->voltage_); } diff --git a/esphome/components/gdk101/gdk101.cpp b/esphome/components/gdk101/gdk101.cpp index 6c218f03d9..617e2138fb 100644 --- a/esphome/components/gdk101/gdk101.cpp +++ b/esphome/components/gdk101/gdk101.cpp @@ -36,20 +36,20 @@ void GDK101Component::setup() { uint8_t data[2]; // first, reset the sensor if (!this->reset_sensor_(data)) { - this->status_set_error("Reset failed!"); + this->status_set_error(LOG_STR("Reset failed!")); this->mark_failed(); return; } // sensor should acknowledge success of the reset procedure if (data[0] != 1) { - this->status_set_error("Reset not acknowledged!"); + this->status_set_error(LOG_STR("Reset not acknowledged!")); this->mark_failed(); return; } delay(10); // read firmware version if (!this->read_fw_version_(data)) { - this->status_set_error("Failed to read firmware version"); + this->status_set_error(LOG_STR("Failed to read firmware version")); this->mark_failed(); return; } diff --git a/esphome/components/gt911/touchscreen/gt911_touchscreen.cpp b/esphome/components/gt911/touchscreen/gt911_touchscreen.cpp index 992a86cc21..b11880a042 100644 --- a/esphome/components/gt911/touchscreen/gt911_touchscreen.cpp +++ b/esphome/components/gt911/touchscreen/gt911_touchscreen.cpp @@ -79,13 +79,13 @@ void GT911Touchscreen::setup_internal_() { } } if (err != i2c::ERROR_OK) { - this->mark_failed("Calibration error"); + this->mark_failed(LOG_STR("Calibration error")); return; } } if (err != i2c::ERROR_OK) { - this->mark_failed(ESP_LOG_MSG_COMM_FAIL); + this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL)); return; } this->setup_done_ = true; diff --git a/esphome/components/http_request/update/http_request_update.cpp b/esphome/components/http_request/update/http_request_update.cpp index 9dbf8d181a..c91b0eba73 100644 --- a/esphome/components/http_request/update/http_request_update.cpp +++ b/esphome/components/http_request/update/http_request_update.cpp @@ -29,7 +29,7 @@ void HttpRequestUpdate::setup() { this->publish_state(); } else if (state == ota::OTAState::OTA_ABORT || state == ota::OTAState::OTA_ERROR) { this->state_ = update::UPDATE_STATE_AVAILABLE; - this->status_set_error("Failed to install firmware"); + this->status_set_error(LOG_STR("Failed to install firmware")); this->publish_state(); } }); @@ -51,7 +51,7 @@ void HttpRequestUpdate::update_task(void *params) { if (container == nullptr || container->status_code != HTTP_STATUS_OK) { ESP_LOGE(TAG, "Failed to fetch manifest from %s", this_update->source_url_.c_str()); // Defer to main loop to avoid race condition on component_state_ read-modify-write - this_update->defer([this_update]() { this_update->status_set_error("Failed to fetch manifest"); }); + this_update->defer([this_update]() { this_update->status_set_error(LOG_STR("Failed to fetch manifest")); }); UPDATE_RETURN; } @@ -60,7 +60,8 @@ void HttpRequestUpdate::update_task(void *params) { if (data == nullptr) { ESP_LOGE(TAG, "Failed to allocate %zu bytes for manifest", container->content_length); // Defer to main loop to avoid race condition on component_state_ read-modify-write - this_update->defer([this_update]() { this_update->status_set_error("Failed to allocate memory for manifest"); }); + this_update->defer( + [this_update]() { this_update->status_set_error(LOG_STR("Failed to allocate memory for manifest")); }); container->end(); UPDATE_RETURN; } @@ -123,7 +124,7 @@ void HttpRequestUpdate::update_task(void *params) { if (!valid) { ESP_LOGE(TAG, "Failed to parse JSON from %s", this_update->source_url_.c_str()); // Defer to main loop to avoid race condition on component_state_ read-modify-write - this_update->defer([this_update]() { this_update->status_set_error("Failed to parse manifest JSON"); }); + this_update->defer([this_update]() { this_update->status_set_error(LOG_STR("Failed to parse manifest JSON")); }); UPDATE_RETURN; } diff --git a/esphome/components/lvgl/lvgl_esphome.cpp b/esphome/components/lvgl/lvgl_esphome.cpp index 05005b0217..fbcd68378c 100644 --- a/esphome/components/lvgl/lvgl_esphome.cpp +++ b/esphome/components/lvgl/lvgl_esphome.cpp @@ -466,7 +466,7 @@ void LvglComponent::setup() { buffer = lv_custom_mem_alloc(buf_bytes); // NOLINT } if (buffer == nullptr) { - this->status_set_error("Memory allocation failure"); + this->status_set_error(LOG_STR("Memory allocation failure")); this->mark_failed(); return; } @@ -479,7 +479,7 @@ void LvglComponent::setup() { if (this->rotation != display::DISPLAY_ROTATION_0_DEGREES) { this->rotate_buf_ = static_cast(lv_custom_mem_alloc(buf_bytes)); // NOLINT if (this->rotate_buf_ == nullptr) { - this->status_set_error("Memory allocation failure"); + this->status_set_error(LOG_STR("Memory allocation failure")); this->mark_failed(); return; } diff --git a/esphome/components/max17043/max17043.cpp b/esphome/components/max17043/max17043.cpp index f605fb1324..e8cf4d5ab1 100644 --- a/esphome/components/max17043/max17043.cpp +++ b/esphome/components/max17043/max17043.cpp @@ -57,14 +57,14 @@ void MAX17043Component::setup() { if (config_reg != MAX17043_CONFIG_POWER_UP_DEFAULT) { ESP_LOGE(TAG, "Device does not appear to be a MAX17043"); - this->status_set_error("unrecognised"); + this->status_set_error(LOG_STR("unrecognised")); this->mark_failed(); return; } // need to write back to config register to reset the sleep bit if (!this->write_byte_16(MAX17043_CONFIG, MAX17043_CONFIG_POWER_UP_DEFAULT)) { - this->status_set_error("sleep reset failed"); + this->status_set_error(LOG_STR("sleep reset failed")); this->mark_failed(); return; } diff --git a/esphome/components/mipi_dsi/mipi_dsi.cpp b/esphome/components/mipi_dsi/mipi_dsi.cpp index 7305435e4b..cae8647398 100644 --- a/esphome/components/mipi_dsi/mipi_dsi.cpp +++ b/esphome/components/mipi_dsi/mipi_dsi.cpp @@ -12,8 +12,8 @@ static bool notify_refresh_ready(esp_lcd_panel_handle_t panel, esp_lcd_dpi_panel return (need_yield == pdTRUE); } -void MIPI_DSI::smark_failed(const char *message, esp_err_t err) { - ESP_LOGE(TAG, "%s: %s", message, esp_err_to_name(err)); +void MIPI_DSI::smark_failed(const LogString *message, esp_err_t err) { + ESP_LOGE(TAG, "%s: %s", LOG_STR_ARG(message), esp_err_to_name(err)); this->mark_failed(message); } @@ -37,7 +37,7 @@ void MIPI_DSI::setup() { }; auto err = esp_lcd_new_dsi_bus(&bus_config, &this->bus_handle_); if (err != ESP_OK) { - this->smark_failed("lcd_new_dsi_bus failed", err); + this->smark_failed(LOG_STR("lcd_new_dsi_bus failed"), err); return; } esp_lcd_dbi_io_config_t dbi_config = { @@ -47,7 +47,7 @@ void MIPI_DSI::setup() { }; err = esp_lcd_new_panel_io_dbi(this->bus_handle_, &dbi_config, &this->io_handle_); if (err != ESP_OK) { - this->smark_failed("new_panel_io_dbi failed", err); + this->smark_failed(LOG_STR("new_panel_io_dbi failed"), err); return; } auto pixel_format = LCD_COLOR_PIXEL_FORMAT_RGB565; @@ -75,7 +75,7 @@ void MIPI_DSI::setup() { }}; err = esp_lcd_new_panel_dpi(this->bus_handle_, &dpi_config, &this->handle_); if (err != ESP_OK) { - this->smark_failed("esp_lcd_new_panel_dpi failed", err); + this->smark_failed(LOG_STR("esp_lcd_new_panel_dpi failed"), err); return; } if (this->reset_pin_ != nullptr) { @@ -92,14 +92,14 @@ void MIPI_DSI::setup() { auto when = millis() + 120; err = esp_lcd_panel_init(this->handle_); if (err != ESP_OK) { - this->smark_failed("esp_lcd_init failed", err); + this->smark_failed(LOG_STR("esp_lcd_init failed"), err); return; } size_t index = 0; auto &vec = this->init_sequence_; while (index != vec.size()) { if (vec.size() - index < 2) { - this->mark_failed("Malformed init sequence"); + this->mark_failed(LOG_STR("Malformed init sequence")); return; } uint8_t cmd = vec[index++]; @@ -110,7 +110,7 @@ void MIPI_DSI::setup() { } else { uint8_t num_args = x & 0x7F; if (vec.size() - index < num_args) { - this->mark_failed("Malformed init sequence"); + this->mark_failed(LOG_STR("Malformed init sequence")); return; } if (cmd == SLEEP_OUT) { @@ -125,7 +125,7 @@ void MIPI_DSI::setup() { format_hex_pretty(ptr, num_args, '.', false).c_str()); err = esp_lcd_panel_io_tx_param(this->io_handle_, cmd, ptr, num_args); if (err != ESP_OK) { - this->smark_failed("lcd_panel_io_tx_param failed", err); + this->smark_failed(LOG_STR("lcd_panel_io_tx_param failed"), err); return; } index += num_args; @@ -140,7 +140,7 @@ void MIPI_DSI::setup() { err = (esp_lcd_dpi_panel_register_event_callbacks(this->handle_, &cbs, this->io_lock_)); if (err != ESP_OK) { - this->smark_failed("Failed to register callbacks", err); + this->smark_failed(LOG_STR("Failed to register callbacks"), err); return; } @@ -222,7 +222,7 @@ bool MIPI_DSI::check_buffer_() { RAMAllocator allocator; this->buffer_ = allocator.allocate(this->height_ * this->width_ * bytes_per_pixel); if (this->buffer_ == nullptr) { - this->mark_failed("Could not allocate buffer for display!"); + this->mark_failed(LOG_STR("Could not allocate buffer for display!")); return false; } return true; diff --git a/esphome/components/mipi_dsi/mipi_dsi.h b/esphome/components/mipi_dsi/mipi_dsi.h index 98ee092ed1..1cffe3b178 100644 --- a/esphome/components/mipi_dsi/mipi_dsi.h +++ b/esphome/components/mipi_dsi/mipi_dsi.h @@ -62,7 +62,7 @@ class MIPI_DSI : public display::Display { void set_lanes(uint8_t lanes) { this->lanes_ = lanes; } void set_madctl(uint8_t madctl) { this->madctl_ = madctl; } - void smark_failed(const char *message, esp_err_t err); + void smark_failed(const LogString *message, esp_err_t err); void update() override; diff --git a/esphome/components/mipi_rgb/mipi_rgb.cpp b/esphome/components/mipi_rgb/mipi_rgb.cpp index 4c687724cf..74eedae4f4 100644 --- a/esphome/components/mipi_rgb/mipi_rgb.cpp +++ b/esphome/components/mipi_rgb/mipi_rgb.cpp @@ -73,7 +73,7 @@ void MipiRgbSpi::write_init_sequence_() { auto &vec = this->init_sequence_; while (index != vec.size()) { if (vec.size() - index < 2) { - this->mark_failed("Malformed init sequence"); + this->mark_failed(LOG_STR("Malformed init sequence")); return; } uint8_t cmd = vec[index++]; @@ -84,7 +84,7 @@ void MipiRgbSpi::write_init_sequence_() { } else { uint8_t num_args = x & 0x7F; if (vec.size() - index < num_args) { - this->mark_failed("Malformed init sequence"); + this->mark_failed(LOG_STR("Malformed init sequence")); return; } if (cmd == SLEEP_OUT) { @@ -165,7 +165,7 @@ void MipiRgb::common_setup_() { err = esp_lcd_panel_init(this->handle_); if (err != ESP_OK) { ESP_LOGE(TAG, "lcd setup failed: %s", esp_err_to_name(err)); - this->mark_failed("lcd setup failed"); + this->mark_failed(LOG_STR("lcd setup failed")); } ESP_LOGCONFIG(TAG, "MipiRgb setup complete"); } @@ -249,7 +249,7 @@ bool MipiRgb::check_buffer_() { RAMAllocator allocator; this->buffer_ = allocator.allocate(this->height_ * this->width_); if (this->buffer_ == nullptr) { - this->mark_failed("Could not allocate buffer for display!"); + this->mark_failed(LOG_STR("Could not allocate buffer for display!")); return false; } return true; diff --git a/esphome/components/mipi_spi/mipi_spi.h b/esphome/components/mipi_spi/mipi_spi.h index 7e597d1c61..1953aef035 100644 --- a/esphome/components/mipi_spi/mipi_spi.h +++ b/esphome/components/mipi_spi/mipi_spi.h @@ -478,7 +478,7 @@ class MipiSpiBuffer : public MipiSpi allocator{}; this->buffer_ = allocator.allocate(BUFFER_WIDTH * BUFFER_HEIGHT / FRACTION); if (this->buffer_ == nullptr) { - this->mark_failed("Buffer allocation failed"); + this->mark_failed(LOG_STR("Buffer allocation failed")); } } diff --git a/esphome/components/mixer/speaker/mixer_speaker.cpp b/esphome/components/mixer/speaker/mixer_speaker.cpp index b0b64f5709..043b629cf1 100644 --- a/esphome/components/mixer/speaker/mixer_speaker.cpp +++ b/esphome/components/mixer/speaker/mixer_speaker.cpp @@ -78,19 +78,20 @@ void SourceSpeaker::loop() { } else { switch (err) { case ESP_ERR_NO_MEM: - this->status_set_error("Failed to start mixer: not enough memory"); + this->status_set_error(LOG_STR("Failed to start mixer: not enough memory")); break; case ESP_ERR_NOT_SUPPORTED: - this->status_set_error("Failed to start mixer: unsupported bits per sample"); + this->status_set_error(LOG_STR("Failed to start mixer: unsupported bits per sample")); break; case ESP_ERR_INVALID_ARG: - this->status_set_error("Failed to start mixer: audio stream isn't compatible with the other audio stream."); + this->status_set_error( + LOG_STR("Failed to start mixer: audio stream isn't compatible with the other audio stream.")); break; case ESP_ERR_INVALID_STATE: - this->status_set_error("Failed to start mixer: mixer task failed to start"); + this->status_set_error(LOG_STR("Failed to start mixer: mixer task failed to start")); break; default: - this->status_set_error("Failed to start mixer"); + this->status_set_error(LOG_STR("Failed to start mixer")); break; } @@ -317,7 +318,7 @@ void MixerSpeaker::loop() { xEventGroupClearBits(this->event_group_, MixerEventGroupBits::STATE_STARTING); } if (event_group_bits & MixerEventGroupBits::ERR_ESP_NO_MEM) { - this->status_set_error("Failed to allocate the mixer's internal buffer"); + this->status_set_error(LOG_STR("Failed to allocate the mixer's internal buffer")); xEventGroupClearBits(this->event_group_, MixerEventGroupBits::ERR_ESP_NO_MEM); } if (event_group_bits & MixerEventGroupBits::STATE_RUNNING) { diff --git a/esphome/components/nau7802/nau7802.cpp b/esphome/components/nau7802/nau7802.cpp index 6a31b754f7..11f63a9a33 100644 --- a/esphome/components/nau7802/nau7802.cpp +++ b/esphome/components/nau7802/nau7802.cpp @@ -278,7 +278,7 @@ void NAU7802Sensor::loop() { this->set_calibration_failure_(true); this->state_ = CalibrationState::INACTIVE; ESP_LOGE(TAG, "Failed to calibrate sensor"); - this->status_set_error("Calibration Failed"); + this->status_set_error(LOG_STR("Calibration Failed")); return; } diff --git a/esphome/components/packet_transport/packet_transport.cpp b/esphome/components/packet_transport/packet_transport.cpp index 857b40ca0e..37e5f3d9e1 100644 --- a/esphome/components/packet_transport/packet_transport.cpp +++ b/esphome/components/packet_transport/packet_transport.cpp @@ -195,7 +195,7 @@ static void add(std::vector &vec, const char *str) { void PacketTransport::setup() { this->name_ = App.get_name().c_str(); if (strlen(this->name_) > 255) { - this->status_set_error("Device name exceeds 255 chars"); + this->status_set_error(LOG_STR("Device name exceeds 255 chars")); this->mark_failed(); return; } diff --git a/esphome/components/qmp6988/qmp6988.cpp b/esphome/components/qmp6988/qmp6988.cpp index 61fde186d7..57f54b6432 100644 --- a/esphome/components/qmp6988/qmp6988.cpp +++ b/esphome/components/qmp6988/qmp6988.cpp @@ -310,7 +310,7 @@ void QMP6988Component::calculate_pressure_() { void QMP6988Component::setup() { if (!this->device_check_()) { - this->mark_failed(ESP_LOG_MSG_COMM_FAIL); + this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL)); return; } diff --git a/esphome/components/resampler/speaker/resampler_speaker.cpp b/esphome/components/resampler/speaker/resampler_speaker.cpp index 5e5615cbb9..ad61aca084 100644 --- a/esphome/components/resampler/speaker/resampler_speaker.cpp +++ b/esphome/components/resampler/speaker/resampler_speaker.cpp @@ -66,17 +66,17 @@ void ResamplerSpeaker::loop() { } if (event_group_bits & ResamplingEventGroupBits::ERR_ESP_NO_MEM) { - this->status_set_error("Resampler task failed to allocate the internal buffers"); + this->status_set_error(LOG_STR("Resampler task failed to allocate the internal buffers")); xEventGroupClearBits(this->event_group_, ResamplingEventGroupBits::ERR_ESP_NO_MEM); this->state_ = speaker::STATE_STOPPING; } if (event_group_bits & ResamplingEventGroupBits::ERR_ESP_NOT_SUPPORTED) { - this->status_set_error("Cannot resample due to an unsupported audio stream"); + this->status_set_error(LOG_STR("Cannot resample due to an unsupported audio stream")); xEventGroupClearBits(this->event_group_, ResamplingEventGroupBits::ERR_ESP_NOT_SUPPORTED); this->state_ = speaker::STATE_STOPPING; } if (event_group_bits & ResamplingEventGroupBits::ERR_ESP_FAIL) { - this->status_set_error("Resampler task failed"); + this->status_set_error(LOG_STR("Resampler task failed")); xEventGroupClearBits(this->event_group_, ResamplingEventGroupBits::ERR_ESP_FAIL); this->state_ = speaker::STATE_STOPPING; } @@ -106,12 +106,12 @@ void ResamplerSpeaker::loop() { } else { switch (err) { case ESP_ERR_INVALID_STATE: - this->status_set_error("Failed to start resampler: resampler task failed to start"); + this->status_set_error(LOG_STR("Failed to start resampler: resampler task failed to start")); break; case ESP_ERR_NO_MEM: - this->status_set_error("Failed to start resampler: not enough memory for task stack"); + this->status_set_error(LOG_STR("Failed to start resampler: not enough memory for task stack")); default: - this->status_set_error("Failed to start resampler"); + this->status_set_error(LOG_STR("Failed to start resampler")); break; } diff --git a/esphome/components/sht4x/sht4x.cpp b/esphome/components/sht4x/sht4x.cpp index 62b8717ded..617b19ef3e 100644 --- a/esphome/components/sht4x/sht4x.cpp +++ b/esphome/components/sht4x/sht4x.cpp @@ -13,7 +13,7 @@ void SHT4XComponent::start_heater_() { ESP_LOGD(TAG, "Heater turning on"); if (this->write(cmd, 1) != i2c::ERROR_OK) { - this->status_set_error("Failed to turn on heater"); + this->status_set_error(LOG_STR("Failed to turn on heater")); } } diff --git a/esphome/components/stts22h/stts22h.cpp b/esphome/components/stts22h/stts22h.cpp index 614dc1da8b..2b2559c843 100644 --- a/esphome/components/stts22h/stts22h.cpp +++ b/esphome/components/stts22h/stts22h.cpp @@ -21,7 +21,7 @@ static const float SENSOR_SCALE = 0.01f; // Sensor resolution in degrees Celsiu void STTS22HComponent::setup() { // Check if device is a STTS22H if (!this->is_stts22h_sensor_()) { - this->mark_failed("Device is not a STTS22H sensor"); + this->mark_failed(LOG_STR("Device is not a STTS22H sensor")); return; } @@ -61,12 +61,12 @@ float STTS22HComponent::read_temperature_() { bool STTS22HComponent::is_stts22h_sensor_() { uint8_t whoami_value; if (this->read_register(WHOAMI_REG, &whoami_value, 1) != i2c::NO_ERROR) { - this->mark_failed(ESP_LOG_MSG_COMM_FAIL); + this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL)); return false; } if (whoami_value != WHOAMI_STTS22H_IDENTIFICATION) { - this->mark_failed("Unexpected WHOAMI identifier. Sensor is not a STTS22H"); + this->mark_failed(LOG_STR("Unexpected WHOAMI identifier. Sensor is not a STTS22H")); return false; } @@ -77,7 +77,7 @@ void STTS22HComponent::initialize_sensor_() { // Read current CTRL_REG configuration uint8_t ctrl_value; if (this->read_register(CTRL_REG, &ctrl_value, 1) != i2c::NO_ERROR) { - this->mark_failed(ESP_LOG_MSG_COMM_FAIL); + this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL)); return; } @@ -86,14 +86,14 @@ void STTS22HComponent::initialize_sensor_() { // FREERUN bit must be cleared (see sensor documentation) ctrl_value &= ~FREERUN_CTRL_ENABLE_FLAG; // Clear FREERUN bit if (this->write_register(CTRL_REG, &ctrl_value, 1) != i2c::NO_ERROR) { - this->mark_failed(ESP_LOG_MSG_COMM_FAIL); + this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL)); return; } // Enable LOW ODR mode and ADD_INC ctrl_value |= LOW_ODR_CTRL_ENABLE_FLAG | ADD_INC_ENABLE_FLAG; // Set LOW ODR bit and ADD_INC bit if (this->write_register(CTRL_REG, &ctrl_value, 1) != i2c::NO_ERROR) { - this->mark_failed(ESP_LOG_MSG_COMM_FAIL); + this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL)); return; } } diff --git a/esphome/components/udp/udp_component.cpp b/esphome/components/udp/udp_component.cpp index 7714793e1c..9105ced21e 100644 --- a/esphome/components/udp/udp_component.cpp +++ b/esphome/components/udp/udp_component.cpp @@ -21,7 +21,7 @@ void UDPComponent::setup() { if (this->should_broadcast_) { this->broadcast_socket_ = socket::socket(AF_INET, SOCK_DGRAM, IPPROTO_IP); if (this->broadcast_socket_ == nullptr) { - this->status_set_error("Could not create socket"); + this->status_set_error(LOG_STR("Could not create socket")); this->mark_failed(); return; } @@ -41,14 +41,14 @@ void UDPComponent::setup() { if (this->should_listen_) { this->listen_socket_ = socket::socket(AF_INET, SOCK_DGRAM, IPPROTO_IP); if (this->listen_socket_ == nullptr) { - this->status_set_error("Could not create socket"); + this->status_set_error(LOG_STR("Could not create socket")); this->mark_failed(); return; } auto err = this->listen_socket_->setblocking(false); if (err < 0) { ESP_LOGE(TAG, "Unable to set nonblocking: errno %d", errno); - this->status_set_error("Unable to set nonblocking"); + this->status_set_error(LOG_STR("Unable to set nonblocking")); this->mark_failed(); return; } @@ -73,7 +73,7 @@ void UDPComponent::setup() { err = this->listen_socket_->setsockopt(IPPROTO_IP, IP_ADD_MEMBERSHIP, &imreq, sizeof(imreq)); if (err < 0) { ESP_LOGE(TAG, "Failed to set IP_ADD_MEMBERSHIP. Error %d", errno); - this->status_set_error("Failed to set IP_ADD_MEMBERSHIP"); + this->status_set_error(LOG_STR("Failed to set IP_ADD_MEMBERSHIP")); this->mark_failed(); return; } @@ -82,7 +82,7 @@ void UDPComponent::setup() { err = this->listen_socket_->bind((struct sockaddr *) &server, sizeof(server)); if (err != 0) { ESP_LOGE(TAG, "Socket unable to bind: errno %d", errno); - this->status_set_error("Unable to bind socket"); + this->status_set_error(LOG_STR("Unable to bind socket")); this->mark_failed(); return; } diff --git a/esphome/components/usb_host/usb_host_client.cpp b/esphome/components/usb_host/usb_host_client.cpp index 4c09cf8a49..fe61353b5d 100644 --- a/esphome/components/usb_host/usb_host_client.cpp +++ b/esphome/components/usb_host/usb_host_client.cpp @@ -188,7 +188,7 @@ void USBClient::setup() { auto err = usb_host_client_register(&config, &this->handle_); if (err != ESP_OK) { ESP_LOGE(TAG, "client register failed: %s", esp_err_to_name(err)); - this->status_set_error("Client register failed"); + this->status_set_error(LOG_STR("Client register failed")); this->mark_failed(); return; } diff --git a/esphome/components/usb_host/usb_host_component.cpp b/esphome/components/usb_host/usb_host_component.cpp index fb19239c73..1e70c289df 100644 --- a/esphome/components/usb_host/usb_host_component.cpp +++ b/esphome/components/usb_host/usb_host_component.cpp @@ -11,7 +11,7 @@ void USBHost::setup() { usb_host_config_t config{}; if (usb_host_install(&config) != ESP_OK) { - this->status_set_error("usb_host_install failed"); + this->status_set_error(LOG_STR("usb_host_install failed")); this->mark_failed(); return; } diff --git a/esphome/components/usb_uart/usb_uart.cpp b/esphome/components/usb_uart/usb_uart.cpp index c24fffb11d..6720c1e690 100644 --- a/esphome/components/usb_uart/usb_uart.cpp +++ b/esphome/components/usb_uart/usb_uart.cpp @@ -320,7 +320,7 @@ static void fix_mps(const usb_ep_desc_t *ep) { void USBUartTypeCdcAcm::on_connected() { auto cdc_devs = this->parse_descriptors(this->device_handle_); if (cdc_devs.empty()) { - this->status_set_error("No CDC-ACM device found"); + this->status_set_error(LOG_STR("No CDC-ACM device found")); this->disconnect(); return; } @@ -341,7 +341,7 @@ void USBUartTypeCdcAcm::on_connected() { if (err != ESP_OK) { ESP_LOGE(TAG, "usb_host_interface_claim failed: %s, channel=%d, intf=%d", esp_err_to_name(err), channel->index_, channel->cdc_dev_.bulk_interface_number); - this->status_set_error("usb_host_interface_claim failed"); + this->status_set_error(LOG_STR("usb_host_interface_claim failed")); this->disconnect(); return; } diff --git a/esphome/components/voice_assistant/voice_assistant.cpp b/esphome/components/voice_assistant/voice_assistant.cpp index fd35dc7d09..551f0370f2 100644 --- a/esphome/components/voice_assistant/voice_assistant.cpp +++ b/esphome/components/voice_assistant/voice_assistant.cpp @@ -206,7 +206,7 @@ void VoiceAssistant::loop() { case State::START_MICROPHONE: { ESP_LOGD(TAG, "Starting Microphone"); if (!this->allocate_buffers_()) { - this->status_set_error("Failed to allocate buffers"); + this->status_set_error(LOG_STR("Failed to allocate buffers")); return; } if (this->status_has_error()) { diff --git a/esphome/components/wake_on_lan/wake_on_lan.cpp b/esphome/components/wake_on_lan/wake_on_lan.cpp index 7993abd7e7..8c5bdac54b 100644 --- a/esphome/components/wake_on_lan/wake_on_lan.cpp +++ b/esphome/components/wake_on_lan/wake_on_lan.cpp @@ -67,7 +67,7 @@ void WakeOnLanButton::setup() { #if defined(USE_SOCKET_IMPL_BSD_SOCKETS) || defined(USE_SOCKET_IMPL_LWIP_SOCKETS) this->broadcast_socket_ = socket::socket(AF_INET, SOCK_DGRAM, IPPROTO_IP); if (this->broadcast_socket_ == nullptr) { - this->status_set_error("Could not create socket"); + this->status_set_error(LOG_STR("Could not create socket")); this->mark_failed(); return; } diff --git a/esphome/core/component.cpp b/esphome/core/component.cpp index de3dd99d0c..5e6ace8873 100644 --- a/esphome/core/component.cpp +++ b/esphome/core/component.cpp @@ -36,6 +36,9 @@ namespace { struct ComponentErrorMessage { const Component *component; const char *message; + // Track if message is flash pointer (needs LOG_STR_ARG) or RAM pointer + // Remove before 2026.6.0 when deprecated const char* API is removed + bool is_flash_ptr; }; struct ComponentPriorityOverride { @@ -49,6 +52,25 @@ std::unique_ptr> component_error_messages; // Setup priority overrides - freed after setup completes // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) std::unique_ptr> setup_priority_overrides; + +// Helper to store error messages - reduces duplication between deprecated and new API +// Remove before 2026.6.0 when deprecated const char* API is removed +void store_component_error_message(const Component *component, const char *message, bool is_flash_ptr) { + // Lazy allocate the error messages vector if needed + if (!component_error_messages) { + component_error_messages = std::make_unique>(); + } + // Check if this component already has an error message + for (auto &entry : *component_error_messages) { + if (entry.component == component) { + entry.message = message; + entry.is_flash_ptr = is_flash_ptr; + return; + } + } + // Add new error message + component_error_messages->emplace_back(ComponentErrorMessage{component, message, is_flash_ptr}); +} } // namespace namespace setup_priority { @@ -143,16 +165,20 @@ void Component::call_dump_config() { if (this->is_failed()) { // Look up error message from global vector const char *error_msg = nullptr; + bool is_flash_ptr = false; if (component_error_messages) { for (const auto &entry : *component_error_messages) { if (entry.component == this) { error_msg = entry.message; + is_flash_ptr = entry.is_flash_ptr; break; } } } + // Log with appropriate format based on pointer type ESP_LOGE(TAG, " %s is marked FAILED: %s", LOG_STR_ARG(this->get_component_log_str()), - error_msg ? error_msg : LOG_STR_LITERAL("unspecified")); + error_msg ? (is_flash_ptr ? LOG_STR_ARG((const LogString *) error_msg) : error_msg) + : LOG_STR_LITERAL("unspecified")); } } @@ -307,6 +333,7 @@ void Component::status_set_warning(const LogString *message) { ESP_LOGW(TAG, "%s set Warning flag: %s", LOG_STR_ARG(this->get_component_log_str()), message ? LOG_STR_ARG(message) : LOG_STR_LITERAL("unspecified")); } +void Component::status_set_error() { this->status_set_error((const LogString *) nullptr); } void Component::status_set_error(const char *message) { if ((this->component_state_ & STATUS_LED_ERROR) != 0) return; @@ -315,19 +342,19 @@ void Component::status_set_error(const char *message) { ESP_LOGE(TAG, "%s set Error flag: %s", LOG_STR_ARG(this->get_component_log_str()), message ? message : LOG_STR_LITERAL("unspecified")); if (message != nullptr) { - // Lazy allocate the error messages vector if needed - if (!component_error_messages) { - component_error_messages = std::make_unique>(); - } - // Check if this component already has an error message - for (auto &entry : *component_error_messages) { - if (entry.component == this) { - entry.message = message; - return; - } - } - // Add new error message - component_error_messages->emplace_back(ComponentErrorMessage{this, message}); + store_component_error_message(this, message, false); + } +} +void Component::status_set_error(const LogString *message) { + if ((this->component_state_ & STATUS_LED_ERROR) != 0) + return; + this->component_state_ |= STATUS_LED_ERROR; + App.app_state_ |= STATUS_LED_ERROR; + ESP_LOGE(TAG, "%s set Error flag: %s", LOG_STR_ARG(this->get_component_log_str()), + message ? LOG_STR_ARG(message) : LOG_STR_LITERAL("unspecified")); + if (message != nullptr) { + // Store the LogString pointer directly (safe because LogString is always in flash/static memory) + store_component_error_message(this, LOG_STR_ARG(message), true); } } void Component::status_clear_warning() { diff --git a/esphome/core/component.h b/esphome/core/component.h index 462e0e301c..51a9290e8b 100644 --- a/esphome/core/component.h +++ b/esphome/core/component.h @@ -5,6 +5,7 @@ #include #include +#include "esphome/core/helpers.h" #include "esphome/core/log.h" #include "esphome/core/optional.h" @@ -157,7 +158,19 @@ class Component { */ virtual void mark_failed(); + // Remove before 2026.6.0 + ESPDEPRECATED("Use mark_failed(LOG_STR(\"static string literal\")) instead. Do NOT use .c_str() from temporary " + "strings. Will stop working in 2026.6.0", + "2025.12.0") void mark_failed(const char *message) { +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wdeprecated-declarations" + this->status_set_error(message); +#pragma GCC diagnostic pop + this->mark_failed(); + } + + void mark_failed(const LogString *message) { this->status_set_error(message); this->mark_failed(); } @@ -216,7 +229,13 @@ class Component { void status_set_warning(const char *message = nullptr); void status_set_warning(const LogString *message); - void status_set_error(const char *message = nullptr); + void status_set_error(); // Set error flag without message + // Remove before 2026.6.0 + ESPDEPRECATED("Use status_set_error(LOG_STR(\"static string literal\")) instead. Do NOT use .c_str() from temporary " + "strings. Will stop working in 2026.6.0", + "2025.12.0") + void status_set_error(const char *message); + void status_set_error(const LogString *message); void status_clear_warning(); From eeb373fca98681ed576b767450044d2fb5cefedf Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Tue, 25 Nov 2025 09:15:30 +1300 Subject: [PATCH 253/415] [online_image] Fix some large PNGs causing watchdog timeout (#12025) Co-authored-by: guillempages --- esphome/components/online_image/png_image.cpp | 9 +++++++++ esphome/components/online_image/png_image.h | 4 ++++ 2 files changed, 13 insertions(+) diff --git a/esphome/components/online_image/png_image.cpp b/esphome/components/online_image/png_image.cpp index 2038d09ed0..ce9d3bdc91 100644 --- a/esphome/components/online_image/png_image.cpp +++ b/esphome/components/online_image/png_image.cpp @@ -2,6 +2,7 @@ #ifdef USE_ONLINE_IMAGE_PNG_SUPPORT #include "esphome/components/display/display_buffer.h" +#include "esphome/core/application.h" #include "esphome/core/helpers.h" #include "esphome/core/log.h" @@ -38,6 +39,14 @@ static void draw_callback(pngle_t *pngle, uint32_t x, uint32_t y, uint32_t w, ui PngDecoder *decoder = (PngDecoder *) pngle_get_user_data(pngle); Color color(rgba[0], rgba[1], rgba[2], rgba[3]); decoder->draw(x, y, w, h, color); + + // Feed watchdog periodically to avoid triggering during long decode operations. + // Feed every 1024 pixels to balance efficiency and responsiveness. + uint32_t pixels = w * h; + decoder->increment_pixels_decoded(pixels); + if ((decoder->get_pixels_decoded() % 1024) < pixels) { + App.feed_wdt(); + } } PngDecoder::PngDecoder(OnlineImage *image) : ImageDecoder(image) { diff --git a/esphome/components/online_image/png_image.h b/esphome/components/online_image/png_image.h index 46519f8ef4..40e85dde33 100644 --- a/esphome/components/online_image/png_image.h +++ b/esphome/components/online_image/png_image.h @@ -25,9 +25,13 @@ class PngDecoder : public ImageDecoder { int prepare(size_t download_size) override; int HOT decode(uint8_t *buffer, size_t size) override; + void increment_pixels_decoded(uint32_t count) { this->pixels_decoded_ += count; } + uint32_t get_pixels_decoded() const { return this->pixels_decoded_; } + protected: RAMAllocator allocator_; pngle_t *pngle_; + uint32_t pixels_decoded_{0}; }; } // namespace online_image From e09656f20e1abdd7984ee8353a49a1d108ef299d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 24 Nov 2025 15:21:03 -0600 Subject: [PATCH 254/415] Bump bleak from 1.1.1 to 2.0.0 (#12083) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 6ae050b35b..df036eeccc 100644 --- a/requirements.txt +++ b/requirements.txt @@ -22,7 +22,7 @@ pillow==11.3.0 cairosvg==2.8.2 freetype-py==2.5.1 jinja2==3.1.6 -bleak==1.1.1 +bleak==2.0.0 # esp-idf >= 5.0 requires this pyparsing >= 3.0 From fbe091f167850844e1aad34aee4906e17025aa00 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Wed, 19 Nov 2025 19:09:22 -0500 Subject: [PATCH 255/415] [graph] Fix legend border (#12000) --- esphome/components/graph/graph.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/graph/graph.cpp b/esphome/components/graph/graph.cpp index 88bb306408..e3b9119108 100644 --- a/esphome/components/graph/graph.cpp +++ b/esphome/components/graph/graph.cpp @@ -337,7 +337,7 @@ void Graph::draw_legend(display::Display *buff, uint16_t x_offset, uint16_t y_of return; /// Plot border - if (this->border_) { + if (legend_->border_) { int w = legend_->width_; int h = legend_->height_; buff->horizontal_line(x_offset, y_offset, w, color); From 45b8c1e267b398eda2367d480f141dbf0d4d8695 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 20 Nov 2025 07:59:16 -0600 Subject: [PATCH 256/415] [network] Fix IPAddress constructor causing comparison failures and garbage output (#12005) --- esphome/components/network/ip_address.h | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/esphome/components/network/ip_address.h b/esphome/components/network/ip_address.h index 5e6b0dbd96..b9364a1f81 100644 --- a/esphome/components/network/ip_address.h +++ b/esphome/components/network/ip_address.h @@ -81,7 +81,12 @@ struct IPAddress { ip_addr_.type = IPADDR_TYPE_V6; } #endif /* LWIP_IPV6 */ - IPAddress(esp_ip4_addr_t *other_ip) { memcpy((void *) &ip_addr_, (void *) other_ip, sizeof(esp_ip4_addr_t)); } + IPAddress(esp_ip4_addr_t *other_ip) { + memcpy((void *) &ip_addr_, (void *) other_ip, sizeof(esp_ip4_addr_t)); +#if LWIP_IPV6 + ip_addr_.type = IPADDR_TYPE_V4; +#endif + } IPAddress(esp_ip_addr_t *other_ip) { #if LWIP_IPV6 memcpy((void *) &ip_addr_, (void *) other_ip, sizeof(ip_addr_)); From 89ee37a2d58ff0b23fbb4eac5c1462ac80949a52 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Thu, 20 Nov 2025 10:58:21 -0500 Subject: [PATCH 257/415] [ltr501][ltr_als_ps] Rename enum to avoid collision with lwip defines (#12017) --- esphome/components/ltr501/ltr501.cpp | 10 +++++----- esphome/components/ltr501/ltr501.h | 4 ++-- esphome/components/ltr_als_ps/ltr_als_ps.cpp | 12 ++++++------ esphome/components/ltr_als_ps/ltr_als_ps.h | 4 ++-- 4 files changed, 15 insertions(+), 15 deletions(-) diff --git a/esphome/components/ltr501/ltr501.cpp b/esphome/components/ltr501/ltr501.cpp index be5a4ddccf..04de91e362 100644 --- a/esphome/components/ltr501/ltr501.cpp +++ b/esphome/components/ltr501/ltr501.cpp @@ -174,7 +174,7 @@ void LTRAlsPs501Component::loop() { break; case State::WAITING_FOR_DATA: - if (this->is_als_data_ready_(this->als_readings_) == DataAvail::DATA_OK) { + if (this->is_als_data_ready_(this->als_readings_) == LtrDataAvail::LTR_DATA_OK) { tries = 0; ESP_LOGV(TAG, "Reading sensor data assuming gain = %.0fx, time = %d ms", get_gain_coeff(this->als_readings_.gain), get_itime_ms(this->als_readings_.integration_time)); @@ -379,18 +379,18 @@ void LTRAlsPs501Component::configure_integration_time_(IntegrationTime501 time) } } -DataAvail LTRAlsPs501Component::is_als_data_ready_(AlsReadings &data) { +LtrDataAvail LTRAlsPs501Component::is_als_data_ready_(AlsReadings &data) { AlsPsStatusRegister als_status{0}; als_status.raw = this->reg((uint8_t) CommandRegisters::ALS_PS_STATUS).get(); if (!als_status.als_new_data) - return DataAvail::NO_DATA; + return LtrDataAvail::LTR_NO_DATA; ESP_LOGV(TAG, "Data ready, reported gain is %.0fx", get_gain_coeff(als_status.gain)); if (data.gain != als_status.gain) { ESP_LOGW(TAG, "Actual gain differs from requested (%.0f)", get_gain_coeff(data.gain)); - return DataAvail::BAD_DATA; + return LtrDataAvail::LTR_BAD_DATA; } data.gain = als_status.gain; - return DataAvail::DATA_OK; + return LtrDataAvail::LTR_DATA_OK; } void LTRAlsPs501Component::read_sensor_data_(AlsReadings &data) { diff --git a/esphome/components/ltr501/ltr501.h b/esphome/components/ltr501/ltr501.h index 849ff6bc23..02c025da30 100644 --- a/esphome/components/ltr501/ltr501.h +++ b/esphome/components/ltr501/ltr501.h @@ -11,7 +11,7 @@ namespace esphome { namespace ltr501 { -enum DataAvail : uint8_t { NO_DATA, BAD_DATA, DATA_OK }; +enum LtrDataAvail : uint8_t { LTR_NO_DATA, LTR_BAD_DATA, LTR_DATA_OK }; enum LtrType : uint8_t { LTR_TYPE_UNKNOWN = 0, @@ -106,7 +106,7 @@ class LTRAlsPs501Component : public PollingComponent, public i2c::I2CDevice { void configure_als_(); void configure_integration_time_(IntegrationTime501 time); void configure_gain_(AlsGain501 gain); - DataAvail is_als_data_ready_(AlsReadings &data); + LtrDataAvail is_als_data_ready_(AlsReadings &data); void read_sensor_data_(AlsReadings &data); bool are_adjustments_required_(AlsReadings &data); void apply_lux_calculation_(AlsReadings &data); diff --git a/esphome/components/ltr_als_ps/ltr_als_ps.cpp b/esphome/components/ltr_als_ps/ltr_als_ps.cpp index c3ea5848c8..f9c1474c85 100644 --- a/esphome/components/ltr_als_ps/ltr_als_ps.cpp +++ b/esphome/components/ltr_als_ps/ltr_als_ps.cpp @@ -165,7 +165,7 @@ void LTRAlsPsComponent::loop() { break; case State::WAITING_FOR_DATA: - if (this->is_als_data_ready_(this->als_readings_) == DataAvail::DATA_OK) { + if (this->is_als_data_ready_(this->als_readings_) == LtrDataAvail::LTR_DATA_OK) { tries = 0; ESP_LOGV(TAG, "Reading sensor data having gain = %.0fx, time = %d ms", get_gain_coeff(this->als_readings_.gain), get_itime_ms(this->als_readings_.integration_time)); @@ -376,23 +376,23 @@ void LTRAlsPsComponent::configure_integration_time_(IntegrationTime time) { } } -DataAvail LTRAlsPsComponent::is_als_data_ready_(AlsReadings &data) { +LtrDataAvail LTRAlsPsComponent::is_als_data_ready_(AlsReadings &data) { AlsPsStatusRegister als_status{0}; als_status.raw = this->reg((uint8_t) CommandRegisters::ALS_PS_STATUS).get(); if (!als_status.als_new_data) - return DataAvail::NO_DATA; + return LtrDataAvail::LTR_NO_DATA; if (als_status.data_invalid) { ESP_LOGW(TAG, "Data available but not valid"); - return DataAvail::BAD_DATA; + return LtrDataAvail::LTR_BAD_DATA; } ESP_LOGV(TAG, "Data ready, reported gain is %.0f", get_gain_coeff(als_status.gain)); if (data.gain != als_status.gain) { ESP_LOGW(TAG, "Actual gain differs from requested (%.0f)", get_gain_coeff(data.gain)); - return DataAvail::BAD_DATA; + return LtrDataAvail::LTR_BAD_DATA; } - return DataAvail::DATA_OK; + return LtrDataAvail::LTR_DATA_OK; } void LTRAlsPsComponent::read_sensor_data_(AlsReadings &data) { diff --git a/esphome/components/ltr_als_ps/ltr_als_ps.h b/esphome/components/ltr_als_ps/ltr_als_ps.h index 2c768009ab..c6052300de 100644 --- a/esphome/components/ltr_als_ps/ltr_als_ps.h +++ b/esphome/components/ltr_als_ps/ltr_als_ps.h @@ -11,7 +11,7 @@ namespace esphome { namespace ltr_als_ps { -enum DataAvail : uint8_t { NO_DATA, BAD_DATA, DATA_OK }; +enum LtrDataAvail : uint8_t { LTR_NO_DATA, LTR_BAD_DATA, LTR_DATA_OK }; enum LtrType : uint8_t { LTR_TYPE_UNKNOWN = 0, @@ -106,7 +106,7 @@ class LTRAlsPsComponent : public PollingComponent, public i2c::I2CDevice { void configure_als_(); void configure_integration_time_(IntegrationTime time); void configure_gain_(AlsGain gain); - DataAvail is_als_data_ready_(AlsReadings &data); + LtrDataAvail is_als_data_ready_(AlsReadings &data); void read_sensor_data_(AlsReadings &data); bool are_adjustments_required_(AlsReadings &data); void apply_lux_calculation_(AlsReadings &data); From 11ba6440d7124f8895e3e7fef900147f60b4c39e Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Thu, 20 Nov 2025 12:10:28 -0500 Subject: [PATCH 258/415] [cst816][packet_transport][udp][wake_on_lan] Fix error messages (#12019) --- .../cst816/touchscreen/cst816_touchscreen.cpp | 2 +- .../components/packet_transport/packet_transport.cpp | 2 +- esphome/components/udp/udp_component.cpp | 10 +++++----- esphome/components/wake_on_lan/wake_on_lan.cpp | 2 +- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/esphome/components/cst816/touchscreen/cst816_touchscreen.cpp b/esphome/components/cst816/touchscreen/cst816_touchscreen.cpp index 0ba2d9df94..8ed9fa3f87 100644 --- a/esphome/components/cst816/touchscreen/cst816_touchscreen.cpp +++ b/esphome/components/cst816/touchscreen/cst816_touchscreen.cpp @@ -19,8 +19,8 @@ void CST816Touchscreen::continue_setup_() { case CST816T_CHIP_ID: break; default: - this->mark_failed(); this->status_set_error(str_sprintf("Unknown chip ID 0x%02X", this->chip_id_).c_str()); + this->mark_failed(); return; } this->write_byte(REG_IRQ_CTL, IRQ_EN_MOTION); diff --git a/esphome/components/packet_transport/packet_transport.cpp b/esphome/components/packet_transport/packet_transport.cpp index 8bde4ee505..857b40ca0e 100644 --- a/esphome/components/packet_transport/packet_transport.cpp +++ b/esphome/components/packet_transport/packet_transport.cpp @@ -195,8 +195,8 @@ static void add(std::vector &vec, const char *str) { void PacketTransport::setup() { this->name_ = App.get_name().c_str(); if (strlen(this->name_) > 255) { - this->mark_failed(); this->status_set_error("Device name exceeds 255 chars"); + this->mark_failed(); return; } this->resend_ping_key_ = this->ping_pong_enable_; diff --git a/esphome/components/udp/udp_component.cpp b/esphome/components/udp/udp_component.cpp index 8a9ce612b4..7714793e1c 100644 --- a/esphome/components/udp/udp_component.cpp +++ b/esphome/components/udp/udp_component.cpp @@ -21,8 +21,8 @@ void UDPComponent::setup() { if (this->should_broadcast_) { this->broadcast_socket_ = socket::socket(AF_INET, SOCK_DGRAM, IPPROTO_IP); if (this->broadcast_socket_ == nullptr) { - this->mark_failed(); this->status_set_error("Could not create socket"); + this->mark_failed(); return; } int enable = 1; @@ -41,15 +41,15 @@ void UDPComponent::setup() { if (this->should_listen_) { this->listen_socket_ = socket::socket(AF_INET, SOCK_DGRAM, IPPROTO_IP); if (this->listen_socket_ == nullptr) { - this->mark_failed(); this->status_set_error("Could not create socket"); + this->mark_failed(); return; } auto err = this->listen_socket_->setblocking(false); if (err < 0) { ESP_LOGE(TAG, "Unable to set nonblocking: errno %d", errno); - this->mark_failed(); this->status_set_error("Unable to set nonblocking"); + this->mark_failed(); return; } int enable = 1; @@ -73,8 +73,8 @@ void UDPComponent::setup() { err = this->listen_socket_->setsockopt(IPPROTO_IP, IP_ADD_MEMBERSHIP, &imreq, sizeof(imreq)); if (err < 0) { ESP_LOGE(TAG, "Failed to set IP_ADD_MEMBERSHIP. Error %d", errno); - this->mark_failed(); this->status_set_error("Failed to set IP_ADD_MEMBERSHIP"); + this->mark_failed(); return; } } @@ -82,8 +82,8 @@ void UDPComponent::setup() { err = this->listen_socket_->bind((struct sockaddr *) &server, sizeof(server)); if (err != 0) { ESP_LOGE(TAG, "Socket unable to bind: errno %d", errno); - this->mark_failed(); this->status_set_error("Unable to bind socket"); + this->mark_failed(); return; } } diff --git a/esphome/components/wake_on_lan/wake_on_lan.cpp b/esphome/components/wake_on_lan/wake_on_lan.cpp index adf5a080e5..7993abd7e7 100644 --- a/esphome/components/wake_on_lan/wake_on_lan.cpp +++ b/esphome/components/wake_on_lan/wake_on_lan.cpp @@ -67,8 +67,8 @@ void WakeOnLanButton::setup() { #if defined(USE_SOCKET_IMPL_BSD_SOCKETS) || defined(USE_SOCKET_IMPL_LWIP_SOCKETS) this->broadcast_socket_ = socket::socket(AF_INET, SOCK_DGRAM, IPPROTO_IP); if (this->broadcast_socket_ == nullptr) { - this->mark_failed(); this->status_set_error("Could not create socket"); + this->mark_failed(); return; } int enable = 1; From d698083ede17444ac1f2fe31d75a8ab2290732dd Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Fri, 21 Nov 2025 07:39:59 -0500 Subject: [PATCH 259/415] [jsn_sr04t] Fix model AJ_SR04M (#11992) --- esphome/components/jsn_sr04t/jsn_sr04t.cpp | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/esphome/components/jsn_sr04t/jsn_sr04t.cpp b/esphome/components/jsn_sr04t/jsn_sr04t.cpp index 077d4e58ea..84181dac48 100644 --- a/esphome/components/jsn_sr04t/jsn_sr04t.cpp +++ b/esphome/components/jsn_sr04t/jsn_sr04t.cpp @@ -10,7 +10,7 @@ namespace jsn_sr04t { static const char *const TAG = "jsn_sr04t.sensor"; void Jsnsr04tComponent::update() { - this->write_byte(0x55); + this->write_byte((this->model_ == AJ_SR04M) ? 0x01 : 0x55); ESP_LOGV(TAG, "Request read out from sensor"); } @@ -31,19 +31,10 @@ void Jsnsr04tComponent::loop() { } void Jsnsr04tComponent::check_buffer_() { - uint8_t checksum = 0; - switch (this->model_) { - case JSN_SR04T: - checksum = this->buffer_[0] + this->buffer_[1] + this->buffer_[2]; - break; - case AJ_SR04M: - checksum = this->buffer_[1] + this->buffer_[2]; - break; - } - + uint8_t checksum = this->buffer_[0] + this->buffer_[1] + this->buffer_[2]; if (this->buffer_[3] == checksum) { uint16_t distance = encode_uint16(this->buffer_[1], this->buffer_[2]); - if (distance > 250) { + if (distance > ((this->model_ == AJ_SR04M) ? 200 : 250)) { float meters = distance / 1000.0f; ESP_LOGV(TAG, "Distance from sensor: %umm, %.3fm", distance, meters); this->publish_state(meters); From f8efefffaa9023a12529197d03a38f1986535a49 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 21 Nov 2025 06:41:48 -0600 Subject: [PATCH 260/415] [cst816][http_request] Fix status_set_error() dangling pointer bugs (#12033) --- .../cst816/touchscreen/cst816_touchscreen.cpp | 3 ++- .../http_request/update/http_request_update.cpp | 12 ++++++------ 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/esphome/components/cst816/touchscreen/cst816_touchscreen.cpp b/esphome/components/cst816/touchscreen/cst816_touchscreen.cpp index 8ed9fa3f87..0560f1b475 100644 --- a/esphome/components/cst816/touchscreen/cst816_touchscreen.cpp +++ b/esphome/components/cst816/touchscreen/cst816_touchscreen.cpp @@ -19,7 +19,8 @@ void CST816Touchscreen::continue_setup_() { case CST816T_CHIP_ID: break; default: - this->status_set_error(str_sprintf("Unknown chip ID 0x%02X", this->chip_id_).c_str()); + ESP_LOGE(TAG, "Unknown chip ID: 0x%02X", this->chip_id_); + this->status_set_error("Unknown chip ID"); this->mark_failed(); return; } diff --git a/esphome/components/http_request/update/http_request_update.cpp b/esphome/components/http_request/update/http_request_update.cpp index 06aa6da6a4..9dbf8d181a 100644 --- a/esphome/components/http_request/update/http_request_update.cpp +++ b/esphome/components/http_request/update/http_request_update.cpp @@ -49,18 +49,18 @@ void HttpRequestUpdate::update_task(void *params) { auto container = this_update->request_parent_->get(this_update->source_url_); if (container == nullptr || container->status_code != HTTP_STATUS_OK) { - std::string msg = str_sprintf("Failed to fetch manifest from %s", this_update->source_url_.c_str()); + ESP_LOGE(TAG, "Failed to fetch manifest from %s", this_update->source_url_.c_str()); // Defer to main loop to avoid race condition on component_state_ read-modify-write - this_update->defer([this_update, msg]() { this_update->status_set_error(msg.c_str()); }); + this_update->defer([this_update]() { this_update->status_set_error("Failed to fetch manifest"); }); UPDATE_RETURN; } RAMAllocator allocator; uint8_t *data = allocator.allocate(container->content_length); if (data == nullptr) { - std::string msg = str_sprintf("Failed to allocate %zu bytes for manifest", container->content_length); + ESP_LOGE(TAG, "Failed to allocate %zu bytes for manifest", container->content_length); // Defer to main loop to avoid race condition on component_state_ read-modify-write - this_update->defer([this_update, msg]() { this_update->status_set_error(msg.c_str()); }); + this_update->defer([this_update]() { this_update->status_set_error("Failed to allocate memory for manifest"); }); container->end(); UPDATE_RETURN; } @@ -121,9 +121,9 @@ void HttpRequestUpdate::update_task(void *params) { } if (!valid) { - std::string msg = str_sprintf("Failed to parse JSON from %s", this_update->source_url_.c_str()); + ESP_LOGE(TAG, "Failed to parse JSON from %s", this_update->source_url_.c_str()); // Defer to main loop to avoid race condition on component_state_ read-modify-write - this_update->defer([this_update, msg]() { this_update->status_set_error(msg.c_str()); }); + this_update->defer([this_update]() { this_update->status_set_error("Failed to parse manifest JSON"); }); UPDATE_RETURN; } From f31f023c891f906a6762a69d50978433859a1b9e Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Sun, 23 Nov 2025 23:31:14 -0500 Subject: [PATCH 261/415] [esp32] Fix C2 builds (#12050) --- esphome/components/esp32/__init__.py | 6 ++++++ esphome/components/esp32/pre_build.py.script | 9 +++++++++ 2 files changed, 15 insertions(+) create mode 100644 esphome/components/esp32/pre_build.py.script diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index 6f577d2926..59c6029334 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -883,6 +883,12 @@ async def to_code(config): CORE.relative_internal_path(".espressif") ) + add_extra_script( + "pre", + "pre_build.py", + Path(__file__).parent / "pre_build.py.script", + ) + add_extra_script( "post", "post_build.py", diff --git a/esphome/components/esp32/pre_build.py.script b/esphome/components/esp32/pre_build.py.script new file mode 100644 index 0000000000..af12275a0b --- /dev/null +++ b/esphome/components/esp32/pre_build.py.script @@ -0,0 +1,9 @@ +Import("env") # noqa: F821 + +# Remove custom_sdkconfig from the board config as it causes +# pioarduino to enable some strange hybrid build mode that breaks IDF +board = env.BoardConfig() +if "espidf.custom_sdkconfig" in board: + del board._manifest["espidf"]["custom_sdkconfig"] + if not board._manifest["espidf"]: + del board._manifest["espidf"] From 83525b7a926c0c51a50d4fe18259cdd6f5ca52f6 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Mon, 24 Nov 2025 10:10:24 -0500 Subject: [PATCH 262/415] [core] Add support for passing yaml files to clean-all (#12039) --- esphome/__main__.py | 2 +- esphome/writer.py | 8 +++++++- tests/unit_tests/test_writer.py | 31 +++++++++++++++++++++++++++++++ 3 files changed, 39 insertions(+), 2 deletions(-) diff --git a/esphome/__main__.py b/esphome/__main__.py index b0c081a34f..f8fb678cb2 100644 --- a/esphome/__main__.py +++ b/esphome/__main__.py @@ -1319,7 +1319,7 @@ def parse_args(argv): "clean-all", help="Clean all build and platform files." ) parser_clean_all.add_argument( - "configuration", help="Your YAML configuration directory.", nargs="*" + "configuration", help="Your YAML file or configuration directory.", nargs="*" ) parser_dashboard = subparsers.add_parser( diff --git a/esphome/writer.py b/esphome/writer.py index 8eee445cf1..1e49a2c961 100644 --- a/esphome/writer.py +++ b/esphome/writer.py @@ -340,7 +340,13 @@ def clean_build(): def clean_all(configuration: list[str]): import shutil - data_dirs = [Path(dir) / ".esphome" for dir in configuration] + data_dirs = [] + for config in configuration: + item = Path(config) + if item.is_file() and item.suffix in (".yaml", ".yml"): + data_dirs.append(item.parent / ".esphome") + else: + data_dirs.append(item / ".esphome") if is_ha_addon(): data_dirs.append(Path("/data")) if "ESPHOME_DATA_DIR" in os.environ: diff --git a/tests/unit_tests/test_writer.py b/tests/unit_tests/test_writer.py index a4490fbbc0..a2a358f4d3 100644 --- a/tests/unit_tests/test_writer.py +++ b/tests/unit_tests/test_writer.py @@ -737,6 +737,37 @@ def test_write_cpp_with_duplicate_markers( write_cpp("// New code") +@patch("esphome.writer.CORE") +def test_clean_all_with_yaml_file( + mock_core: MagicMock, + tmp_path: Path, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test clean_all with a .yaml file uses parent directory.""" + # Create config directory with yaml file + config_dir = tmp_path / "config" + config_dir.mkdir() + yaml_file = config_dir / "test.yaml" + yaml_file.write_text("esphome:\n name: test\n") + + build_dir = config_dir / ".esphome" + build_dir.mkdir() + (build_dir / "dummy.txt").write_text("x") + + from esphome.writer import clean_all + + with caplog.at_level("INFO"): + clean_all([str(yaml_file)]) + + # Verify .esphome directory still exists but contents cleaned + assert build_dir.exists() + assert not (build_dir / "dummy.txt").exists() + + # Verify logging mentions the build dir + assert "Cleaning" in caplog.text + assert str(build_dir) in caplog.text + + @patch("esphome.writer.CORE") def test_clean_all( mock_core: MagicMock, From 3a7a0c66ab500e2b0ce618967e2f13064509009f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 24 Nov 2025 10:41:24 -0600 Subject: [PATCH 263/415] [script][wait_until] Fix FIFO ordering and reentrancy bugs (#12049) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- esphome/components/script/script.h | 20 +-- esphome/core/base_automation.h | 9 +- .../fixtures/script_delay_with_params.yaml | 131 ++++++++++++++++++ .../fixtures/wait_until_fifo_ordering.yaml | 82 +++++++++++ tests/integration/test_script_delay_params.py | 121 ++++++++++++++++ tests/integration/test_wait_until_ordering.py | 90 ++++++++++++ 6 files changed, 441 insertions(+), 12 deletions(-) create mode 100644 tests/integration/fixtures/script_delay_with_params.yaml create mode 100644 tests/integration/fixtures/wait_until_fifo_ordering.yaml create mode 100644 tests/integration/test_script_delay_params.py create mode 100644 tests/integration/test_wait_until_ordering.py diff --git a/esphome/components/script/script.h b/esphome/components/script/script.h index 51cece01e4..d60ed657f7 100644 --- a/esphome/components/script/script.h +++ b/esphome/components/script/script.h @@ -1,8 +1,8 @@ #pragma once +#include #include #include -#include #include "esphome/core/automation.h" #include "esphome/core/component.h" #include "esphome/core/helpers.h" @@ -290,10 +290,10 @@ template class ScriptWaitAction : public Action, } // Store parameters for later execution - this->param_queue_.emplace_front(x...); - // Enable loop now that we have work to do + this->param_queue_.emplace_back(x...); + // Enable loop now that we have work to do - don't call loop() synchronously! + // Let the event loop call it to avoid reentrancy issues this->enable_loop(); - this->loop(); } void loop() override { @@ -303,13 +303,17 @@ template class ScriptWaitAction : public Action, if (this->script_->is_running()) return; - while (!this->param_queue_.empty()) { + // Only process ONE queued item per loop iteration + // Processing all items in a while loop causes infinite loops because + // play_next_() can trigger more items to be queued + if (!this->param_queue_.empty()) { auto ¶ms = this->param_queue_.front(); this->play_next_tuple_(params, typename gens::type()); this->param_queue_.pop_front(); + } else { + // Queue is now empty - disable loop until next play_complex + this->disable_loop(); } - // Queue is now empty - disable loop until next play_complex - this->disable_loop(); } void play(const Ts &...x) override { /* ignore - see play_complex */ @@ -326,7 +330,7 @@ template class ScriptWaitAction : public Action, } C *script_; - std::forward_list> param_queue_; + std::list> param_queue_; }; } // namespace script diff --git a/esphome/core/base_automation.h b/esphome/core/base_automation.h index a5e6139182..e46e5d92a9 100644 --- a/esphome/core/base_automation.h +++ b/esphome/core/base_automation.h @@ -9,8 +9,8 @@ #include "esphome/core/application.h" #include "esphome/core/helpers.h" +#include #include -#include namespace esphome { @@ -433,9 +433,10 @@ template class WaitUntilAction : public Action, public Co // Store for later processing auto now = millis(); auto timeout = this->timeout_value_.optional_value(x...); - this->var_queue_.emplace_front(now, timeout, std::make_tuple(x...)); + this->var_queue_.emplace_back(now, timeout, std::make_tuple(x...)); - // Do immediate check with fresh timestamp + // Do immediate check with fresh timestamp - don't call loop() synchronously! + // Let the event loop call it to avoid reentrancy issues if (this->process_queue_(now)) { // Only enable loop if we still have pending items this->enable_loop(); @@ -487,7 +488,7 @@ template class WaitUntilAction : public Action, public Co } Condition *condition_; - std::forward_list, std::tuple>> var_queue_{}; + std::list, std::tuple>> var_queue_{}; }; template class UpdateComponentAction : public Action { diff --git a/tests/integration/fixtures/script_delay_with_params.yaml b/tests/integration/fixtures/script_delay_with_params.yaml new file mode 100644 index 0000000000..2a0f16d9fe --- /dev/null +++ b/tests/integration/fixtures/script_delay_with_params.yaml @@ -0,0 +1,131 @@ +esphome: + name: test-script-delay-params + +host: + +api: + actions: + # Test case from issue #12044: parent script with repeat calling child with delay + - action: test_repeat_with_delay + then: + - logger.log: "=== TEST: Repeat loop calling script with delay and parameters ===" + - script.execute: father_script + + # Test case from issue #12043: script.wait with delayed child script + - action: test_script_wait + then: + - logger.log: "=== TEST: script.wait with delayed child script ===" + - script.execute: show_start_page + - script.wait: show_start_page + - logger.log: "After wait: script completed successfully" + + # Test: Delay with different parameter types + - action: test_delay_param_types + then: + - logger.log: "=== TEST: Delay with various parameter types ===" + - script.execute: + id: delay_with_int + val: 42 + - delay: 50ms + - script.execute: + id: delay_with_string + msg: "test message" + - delay: 50ms + - script.execute: + id: delay_with_float + num: 3.14 + +logger: + level: DEBUG + +script: + # Reproduces issue #12044: child script with conditional delay + - id: son_script + mode: single + parameters: + iteration: int + then: + - logger.log: + format: "Son script started with iteration %d" + args: ['iteration'] + - if: + condition: + lambda: 'return iteration >= 5;' + then: + - logger.log: + format: "Son script delaying for iteration %d" + args: ['iteration'] + - delay: 100ms + - logger.log: + format: "Son script finished with iteration %d" + args: ['iteration'] + + # Reproduces issue #12044: parent script with repeat loop + - id: father_script + mode: single + then: + - repeat: + count: 10 + then: + - logger.log: + format: "Father iteration %d: calling son" + args: ['iteration'] + - script.execute: + id: son_script + iteration: !lambda 'return iteration;' + - script.wait: son_script + - logger.log: + format: "Father iteration %d: son finished, wait returned" + args: ['iteration'] + + # Reproduces issue #12043: script.wait hangs + - id: show_start_page + mode: single + then: + - logger.log: "Start page: beginning" + - delay: 100ms + - logger.log: "Start page: after delay" + - delay: 100ms + - logger.log: "Start page: completed" + + # Test delay with int parameter + - id: delay_with_int + mode: single + parameters: + val: int + then: + - logger.log: + format: "Int test: before delay, val=%d" + args: ['val'] + - delay: 50ms + - logger.log: + format: "Int test: after delay, val=%d" + args: ['val'] + + # Test delay with string parameter + - id: delay_with_string + mode: single + parameters: + msg: string + then: + - logger.log: + format: "String test: before delay, msg=%s" + args: ['msg.c_str()'] + - delay: 50ms + - logger.log: + format: "String test: after delay, msg=%s" + args: ['msg.c_str()'] + + # Test delay with float parameter + - id: delay_with_float + mode: single + parameters: + num: float + then: + - logger.log: + format: "Float test: before delay, num=%.2f" + args: ['num'] + - delay: 50ms + - logger.log: + format: "Float test: after delay, num=%.2f" + args: ['num'] diff --git a/tests/integration/fixtures/wait_until_fifo_ordering.yaml b/tests/integration/fixtures/wait_until_fifo_ordering.yaml new file mode 100644 index 0000000000..5dd60c8755 --- /dev/null +++ b/tests/integration/fixtures/wait_until_fifo_ordering.yaml @@ -0,0 +1,82 @@ +esphome: + name: test-wait-until-ordering + +host: + +api: + actions: + - action: test_wait_until_fifo + then: + - logger.log: "=== TEST: wait_until should execute in FIFO order ===" + - globals.set: + id: gate_open + value: 'false' + - delay: 100ms + # Start multiple parallel executions of coordinator script + # Each will call the shared waiter script, queueing in same wait_until + - script.execute: coordinator_0 + - script.execute: coordinator_1 + - script.execute: coordinator_2 + - script.execute: coordinator_3 + - script.execute: coordinator_4 + # Give scripts time to reach wait_until and queue + - delay: 200ms + - logger.log: "Opening gate - all wait_until should complete now" + - globals.set: + id: gate_open + value: 'true' + - delay: 500ms + - logger.log: "Test complete" + +globals: + - id: gate_open + type: bool + initial_value: 'false' + +script: + # Shared waiter with single wait_until action (all coordinators call this) + - id: waiter + mode: parallel + parameters: + iter: int + then: + - lambda: 'ESP_LOGD("main", "Queueing iteration %d", iter);' + - wait_until: + condition: + lambda: 'return id(gate_open);' + timeout: 5s + - lambda: 'ESP_LOGD("main", "Completed iteration %d", iter);' + + # Coordinator scripts - each calls shared waiter with different iteration number + - id: coordinator_0 + then: + - script.execute: + id: waiter + iter: 0 + + - id: coordinator_1 + then: + - script.execute: + id: waiter + iter: 1 + + - id: coordinator_2 + then: + - script.execute: + id: waiter + iter: 2 + + - id: coordinator_3 + then: + - script.execute: + id: waiter + iter: 3 + + - id: coordinator_4 + then: + - script.execute: + id: waiter + iter: 4 + +logger: + level: DEBUG diff --git a/tests/integration/test_script_delay_params.py b/tests/integration/test_script_delay_params.py new file mode 100644 index 0000000000..1b5d70863b --- /dev/null +++ b/tests/integration/test_script_delay_params.py @@ -0,0 +1,121 @@ +"""Integration test for script.wait FIFO ordering (issues #12043, #12044). + +This test verifies that ScriptWaitAction processes queued items in FIFO order. + +PR #7972 introduced bugs in ScriptWaitAction: +- Used emplace_front() causing LIFO ordering instead of FIFO +- Called loop() synchronously causing reentrancy issues +- Used while loop processing entire queue causing infinite loops + +These bugs manifested as: +- Scripts becoming "zombies" (stuck in running state) +- script.wait hanging forever +- Incorrect execution order +""" + +from __future__ import annotations + +import asyncio +import re + +import pytest + +from .types import APIClientConnectedFactory, RunCompiledFunction + + +@pytest.mark.asyncio +async def test_script_delay_with_params( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """Test that script.wait processes queued items in FIFO order. + + This reproduces issues #12043 and #12044 where scripts would hang or become + zombies due to LIFO ordering bugs in ScriptWaitAction from PR #7972. + """ + test_complete = asyncio.Event() + + # Patterns to match in logs + father_calling_pattern = re.compile(r"Father iteration (\d+): calling son") + son_started_pattern = re.compile(r"Son script started with iteration (\d+)") + son_delaying_pattern = re.compile(r"Son script delaying for iteration (\d+)") + son_finished_pattern = re.compile(r"Son script finished with iteration (\d+)") + father_wait_returned_pattern = re.compile( + r"Father iteration (\d+): son finished, wait returned" + ) + + # Track which iterations completed + father_calling = set() + son_started = set() + son_delaying = set() + son_finished = set() + wait_returned = set() + + def check_output(line: str) -> None: + """Check log output for expected messages.""" + if test_complete.is_set(): + return + + if mo := father_calling_pattern.search(line): + father_calling.add(int(mo.group(1))) + elif mo := son_started_pattern.search(line): + son_started.add(int(mo.group(1))) + elif mo := son_delaying_pattern.search(line): + son_delaying.add(int(mo.group(1))) + elif mo := son_finished_pattern.search(line): + son_finished.add(int(mo.group(1))) + elif mo := father_wait_returned_pattern.search(line): + iteration = int(mo.group(1)) + wait_returned.add(iteration) + # Test completes when iteration 9 finishes + if iteration == 9: + test_complete.set() + + # 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 == "test-script-delay-params" + + # Get services + _, services = await client.list_entities_services() + test_service = next( + (s for s in services if s.name == "test_repeat_with_delay"), None + ) + assert test_service is not None, "test_repeat_with_delay service not found" + + # Execute the test + client.execute_service(test_service, {}) + + # Wait for test to complete (10 iterations * ~100ms each + margin) + try: + await asyncio.wait_for(test_complete.wait(), timeout=5.0) + except TimeoutError: + pytest.fail( + f"Test timed out. Completed iterations: {sorted(wait_returned)}. " + f"This likely indicates the script became a zombie (issue #12044)." + ) + + # Verify all 10 iterations completed successfully + expected_iterations = set(range(10)) + assert father_calling == expected_iterations, "Not all iterations started" + assert son_started == expected_iterations, ( + "Son script not started for all iterations" + ) + assert son_finished == expected_iterations, ( + "Son script not finished for all iterations" + ) + assert wait_returned == expected_iterations, ( + "script.wait did not return for all iterations" + ) + + # Verify delays were triggered for iterations >= 5 + expected_delays = set(range(5, 10)) + assert son_delaying == expected_delays, ( + "Delays not triggered for iterations >= 5" + ) diff --git a/tests/integration/test_wait_until_ordering.py b/tests/integration/test_wait_until_ordering.py new file mode 100644 index 0000000000..7c39913e5a --- /dev/null +++ b/tests/integration/test_wait_until_ordering.py @@ -0,0 +1,90 @@ +"""Integration test for wait_until FIFO ordering. + +This test verifies that when multiple wait_until actions are queued, +they execute in FIFO (First In First Out) order, not LIFO. + +PR #7972 introduced a bug where emplace_front() was used, causing +LIFO ordering which is incorrect. +""" + +from __future__ import annotations + +import asyncio +import re + +import pytest + +from .types import APIClientConnectedFactory, RunCompiledFunction + + +@pytest.mark.asyncio +async def test_wait_until_fifo_ordering( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """Test that wait_until executes queued items in FIFO order. + + With the bug (using emplace_front), the order would be 4,3,2,1,0 (LIFO). + With the fix (using emplace_back), the order should be 0,1,2,3,4 (FIFO). + """ + test_complete = asyncio.Event() + + # Track completion order + completed_order = [] + + # Patterns to match + queuing_pattern = re.compile(r"Queueing iteration (\d+)") + completed_pattern = re.compile(r"Completed iteration (\d+)") + + def check_output(line: str) -> None: + """Check log output for completion order.""" + if test_complete.is_set(): + return + + if mo := queuing_pattern.search(line): + iteration = int(mo.group(1)) + + elif mo := completed_pattern.search(line): + iteration = int(mo.group(1)) + completed_order.append(iteration) + + # Test completes when all 5 have completed + if len(completed_order) == 5: + test_complete.set() + + # 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 == "test-wait-until-ordering" + + # Get services + _, services = await client.list_entities_services() + test_service = next( + (s for s in services if s.name == "test_wait_until_fifo"), None + ) + assert test_service is not None, "test_wait_until_fifo service not found" + + # Execute the test + client.execute_service(test_service, {}) + + # Wait for test to complete + try: + await asyncio.wait_for(test_complete.wait(), timeout=5.0) + except TimeoutError: + pytest.fail( + f"Test timed out. Completed order: {completed_order}. " + f"Expected 5 completions but got {len(completed_order)}." + ) + + # Verify FIFO order + expected_order = [0, 1, 2, 3, 4] + assert completed_order == expected_order, ( + f"Unexpected order: {completed_order}. " + f"Expected FIFO order: {expected_order}" + ) From 50d08a2ebae6a1eb125c92d545138277bbe12d1d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 24 Nov 2025 11:02:24 -0600 Subject: [PATCH 264/415] [esp_ldo,mipi_dsi,mipi_rgb] Fix dangling pointer bugs in mark_failed() (#12077) --- esphome/components/esp_ldo/esp_ldo.cpp | 4 ++-- esphome/components/mipi_dsi/mipi_dsi.cpp | 6 ++++++ esphome/components/mipi_dsi/mipi_dsi.h | 5 +---- esphome/components/mipi_rgb/mipi_rgb.cpp | 4 ++-- 4 files changed, 11 insertions(+), 8 deletions(-) diff --git a/esphome/components/esp_ldo/esp_ldo.cpp b/esphome/components/esp_ldo/esp_ldo.cpp index eb04670d7e..9ea7000b70 100644 --- a/esphome/components/esp_ldo/esp_ldo.cpp +++ b/esphome/components/esp_ldo/esp_ldo.cpp @@ -14,8 +14,8 @@ void EspLdo::setup() { config.flags.adjustable = this->adjustable_; auto err = esp_ldo_acquire_channel(&config, &this->handle_); if (err != ESP_OK) { - auto msg = str_sprintf("Failed to acquire LDO channel %d with voltage %fV", this->channel_, this->voltage_); - this->mark_failed(msg.c_str()); + ESP_LOGE(TAG, "Failed to acquire LDO channel %d with voltage %fV", this->channel_, this->voltage_); + this->mark_failed("Failed to acquire LDO channel"); } else { ESP_LOGD(TAG, "Acquired LDO channel %d with voltage %fV", this->channel_, this->voltage_); } diff --git a/esphome/components/mipi_dsi/mipi_dsi.cpp b/esphome/components/mipi_dsi/mipi_dsi.cpp index fbe251de41..7305435e4b 100644 --- a/esphome/components/mipi_dsi/mipi_dsi.cpp +++ b/esphome/components/mipi_dsi/mipi_dsi.cpp @@ -11,6 +11,12 @@ static bool notify_refresh_ready(esp_lcd_panel_handle_t panel, esp_lcd_dpi_panel xSemaphoreGiveFromISR(sem, &need_yield); return (need_yield == pdTRUE); } + +void MIPI_DSI::smark_failed(const char *message, esp_err_t err) { + ESP_LOGE(TAG, "%s: %s", message, esp_err_to_name(err)); + this->mark_failed(message); +} + void MIPI_DSI::setup() { ESP_LOGCONFIG(TAG, "Running Setup"); diff --git a/esphome/components/mipi_dsi/mipi_dsi.h b/esphome/components/mipi_dsi/mipi_dsi.h index ce8a2a2236..98ee092ed1 100644 --- a/esphome/components/mipi_dsi/mipi_dsi.h +++ b/esphome/components/mipi_dsi/mipi_dsi.h @@ -62,10 +62,7 @@ class MIPI_DSI : public display::Display { void set_lanes(uint8_t lanes) { this->lanes_ = lanes; } void set_madctl(uint8_t madctl) { this->madctl_ = madctl; } - void smark_failed(const char *message, esp_err_t err) { - auto str = str_sprintf("Setup failed: %s: %s", message, esp_err_to_name(err)); - this->mark_failed(str.c_str()); - } + void smark_failed(const char *message, esp_err_t err); void update() override; diff --git a/esphome/components/mipi_rgb/mipi_rgb.cpp b/esphome/components/mipi_rgb/mipi_rgb.cpp index 080fb08c09..4c687724cf 100644 --- a/esphome/components/mipi_rgb/mipi_rgb.cpp +++ b/esphome/components/mipi_rgb/mipi_rgb.cpp @@ -164,8 +164,8 @@ void MipiRgb::common_setup_() { if (err == ESP_OK) err = esp_lcd_panel_init(this->handle_); if (err != ESP_OK) { - auto msg = str_sprintf("lcd setup failed: %s", esp_err_to_name(err)); - this->mark_failed(msg.c_str()); + ESP_LOGE(TAG, "lcd setup failed: %s", esp_err_to_name(err)); + this->mark_failed("lcd setup failed"); } ESP_LOGCONFIG(TAG, "MipiRgb setup complete"); } From 25bcd0ea25b979ffc064bd64a473fe900fd58c10 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Tue, 25 Nov 2025 09:15:30 +1300 Subject: [PATCH 265/415] [online_image] Fix some large PNGs causing watchdog timeout (#12025) Co-authored-by: guillempages --- esphome/components/online_image/png_image.cpp | 9 +++++++++ esphome/components/online_image/png_image.h | 4 ++++ 2 files changed, 13 insertions(+) diff --git a/esphome/components/online_image/png_image.cpp b/esphome/components/online_image/png_image.cpp index 2038d09ed0..ce9d3bdc91 100644 --- a/esphome/components/online_image/png_image.cpp +++ b/esphome/components/online_image/png_image.cpp @@ -2,6 +2,7 @@ #ifdef USE_ONLINE_IMAGE_PNG_SUPPORT #include "esphome/components/display/display_buffer.h" +#include "esphome/core/application.h" #include "esphome/core/helpers.h" #include "esphome/core/log.h" @@ -38,6 +39,14 @@ static void draw_callback(pngle_t *pngle, uint32_t x, uint32_t y, uint32_t w, ui PngDecoder *decoder = (PngDecoder *) pngle_get_user_data(pngle); Color color(rgba[0], rgba[1], rgba[2], rgba[3]); decoder->draw(x, y, w, h, color); + + // Feed watchdog periodically to avoid triggering during long decode operations. + // Feed every 1024 pixels to balance efficiency and responsiveness. + uint32_t pixels = w * h; + decoder->increment_pixels_decoded(pixels); + if ((decoder->get_pixels_decoded() % 1024) < pixels) { + App.feed_wdt(); + } } PngDecoder::PngDecoder(OnlineImage *image) : ImageDecoder(image) { diff --git a/esphome/components/online_image/png_image.h b/esphome/components/online_image/png_image.h index 46519f8ef4..40e85dde33 100644 --- a/esphome/components/online_image/png_image.h +++ b/esphome/components/online_image/png_image.h @@ -25,9 +25,13 @@ class PngDecoder : public ImageDecoder { int prepare(size_t download_size) override; int HOT decode(uint8_t *buffer, size_t size) override; + void increment_pixels_decoded(uint32_t count) { this->pixels_decoded_ += count; } + uint32_t get_pixels_decoded() const { return this->pixels_decoded_; } + protected: RAMAllocator allocator_; pngle_t *pngle_; + uint32_t pixels_decoded_{0}; }; } // namespace online_image From 9186144dcdb9a21bc02012ad8de44ce67d8ec0ab Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Mon, 24 Nov 2025 16:24:38 -0500 Subject: [PATCH 266/415] Bump version to 2025.11.1 --- Doxyfile | 2 +- esphome/const.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Doxyfile b/Doxyfile index 1448fd010d..a2b6efcfae 100644 --- a/Doxyfile +++ b/Doxyfile @@ -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 +PROJECT_NUMBER = 2025.11.1 # 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 diff --git a/esphome/const.py b/esphome/const.py index 3505ad169b..f4ddd01c09 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -4,7 +4,7 @@ from enum import Enum from esphome.enum import StrEnum -__version__ = "2025.11.0" +__version__ = "2025.11.1" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" VALID_SUBSTITUTIONS_CHARACTERS = ( From 88b898458b6a71a50ff63fed3aae2658e16a19a0 Mon Sep 17 00:00:00 2001 From: Keith Burzinski Date: Mon, 24 Nov 2025 15:25:49 -0600 Subject: [PATCH 267/415] [bluetooth_proxy] Fix crash due to null pointer (#12084) Co-authored-by: J. Nick Koston Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> --- esphome/components/bluetooth_proxy/bluetooth_proxy.h | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/esphome/components/bluetooth_proxy/bluetooth_proxy.h b/esphome/components/bluetooth_proxy/bluetooth_proxy.h index 4de541fac2..4363c508ec 100644 --- a/esphome/components/bluetooth_proxy/bluetooth_proxy.h +++ b/esphome/components/bluetooth_proxy/bluetooth_proxy.h @@ -132,7 +132,11 @@ class BluetoothProxy final : public esp32_ble_tracker::ESPBTDeviceListener, publ void get_bluetooth_mac_address_pretty(std::span output) { const uint8_t *mac = esp_bt_dev_get_address(); - format_mac_addr_upper(mac, output.data()); + if (mac != nullptr) { + format_mac_addr_upper(mac, output.data()); + } else { + output[0] = '\0'; + } } protected: From fca4512370023c3e7a64630ba78900519fd7aac2 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 24 Nov 2025 15:28:38 -0600 Subject: [PATCH 268/415] [mdns] Extract common Arduino mDNS registration to shared header --- esphome/components/mdns/mdns_libretiny.cpp | 34 ++-------------------- esphome/components/mdns/mdns_rp2040.cpp | 34 ++-------------------- 2 files changed, 6 insertions(+), 62 deletions(-) diff --git a/esphome/components/mdns/mdns_libretiny.cpp b/esphome/components/mdns/mdns_libretiny.cpp index 986099fa1f..7b104ed2d7 100644 --- a/esphome/components/mdns/mdns_libretiny.cpp +++ b/esphome/components/mdns/mdns_libretiny.cpp @@ -1,41 +1,13 @@ #include "esphome/core/defines.h" #if defined(USE_LIBRETINY) && defined(USE_MDNS) -#include "esphome/components/network/ip_address.h" -#include "esphome/components/network/util.h" -#include "esphome/core/application.h" -#include "esphome/core/log.h" -#include "mdns_component.h" - #include +#include "mdns_arduino.h" + namespace esphome::mdns { -static void register_libretiny(MDNSComponent *, StaticVector &services) { - MDNS.begin(App.get_name().c_str()); - - for (const auto &service : services) { - // Strip the leading underscore from the proto and service_type. While it is - // part of the wire protocol to have an underscore, and for example ESP-IDF - // expects the underscore to be there, the ESP8266 implementation always adds - // the underscore itself. - auto *proto = MDNS_STR_ARG(service.proto); - while (*proto == '_') { - proto++; - } - auto *service_type = MDNS_STR_ARG(service.service_type); - while (*service_type == '_') { - service_type++; - } - uint16_t port_ = const_cast &>(service.port).value(); - MDNS.addService(service_type, proto, port_); - for (const auto &record : service.txt_records) { - MDNS.addServiceTxt(service_type, proto, MDNS_STR_ARG(record.key), MDNS_STR_ARG(record.value)); - } - } -} - -void MDNSComponent::setup() { this->setup_buffers_and_register_(register_libretiny); } +void MDNSComponent::setup() { this->setup_buffers_and_register_(register_arduino_mdns); } void MDNSComponent::on_shutdown() {} diff --git a/esphome/components/mdns/mdns_rp2040.cpp b/esphome/components/mdns/mdns_rp2040.cpp index e4a9b60cdb..59688e7b36 100644 --- a/esphome/components/mdns/mdns_rp2040.cpp +++ b/esphome/components/mdns/mdns_rp2040.cpp @@ -1,41 +1,13 @@ #include "esphome/core/defines.h" #if defined(USE_RP2040) && defined(USE_MDNS) -#include "esphome/components/network/ip_address.h" -#include "esphome/components/network/util.h" -#include "esphome/core/application.h" -#include "esphome/core/log.h" -#include "mdns_component.h" - #include +#include "mdns_arduino.h" + namespace esphome::mdns { -static void register_rp2040(MDNSComponent *, StaticVector &services) { - MDNS.begin(App.get_name().c_str()); - - for (const auto &service : services) { - // Strip the leading underscore from the proto and service_type. While it is - // part of the wire protocol to have an underscore, and for example ESP-IDF - // expects the underscore to be there, the ESP8266 implementation always adds - // the underscore itself. - auto *proto = MDNS_STR_ARG(service.proto); - while (*proto == '_') { - proto++; - } - auto *service_type = MDNS_STR_ARG(service.service_type); - while (*service_type == '_') { - service_type++; - } - uint16_t port = const_cast &>(service.port).value(); - MDNS.addService(service_type, proto, port); - for (const auto &record : service.txt_records) { - MDNS.addServiceTxt(service_type, proto, MDNS_STR_ARG(record.key), MDNS_STR_ARG(record.value)); - } - } -} - -void MDNSComponent::setup() { this->setup_buffers_and_register_(register_rp2040); } +void MDNSComponent::setup() { this->setup_buffers_and_register_(register_arduino_mdns); } void MDNSComponent::loop() { MDNS.update(); } From 03767474b728c811785d0fb1181ab59045c7e210 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 24 Nov 2025 15:28:45 -0600 Subject: [PATCH 269/415] [mdns] Extract common Arduino mDNS registration to shared header --- esphome/components/mdns/mdns_arduino.h | 42 ++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 esphome/components/mdns/mdns_arduino.h diff --git a/esphome/components/mdns/mdns_arduino.h b/esphome/components/mdns/mdns_arduino.h new file mode 100644 index 0000000000..8d72c86f5c --- /dev/null +++ b/esphome/components/mdns/mdns_arduino.h @@ -0,0 +1,42 @@ +#pragma once +#include "esphome/core/defines.h" + +// Common Arduino mDNS registration for RP2040 and LibreTiny +#if defined(USE_MDNS) && (defined(USE_RP2040) || defined(USE_LIBRETINY)) + +#include "esphome/core/application.h" +#include "mdns_component.h" + +namespace esphome::mdns { + +/// Register mDNS services using Arduino-style mDNS library (RP2040/LibreTiny) +/// @param services The services to register +inline void register_arduino_mdns(MDNSComponent *, StaticVector &services) { + MDNS.begin(App.get_name().c_str()); + + for (const auto &service : services) { + // Strip the leading underscore from the proto and service_type. While it is + // part of the wire protocol to have an underscore, and for example ESP-IDF + // expects the underscore to be there, the RP2040/LibreTiny mDNS + // implementations always add the underscore themselves. + auto *proto = MDNS_STR_ARG(service.proto); + while (*proto == '_') { + proto++; + } + + auto *service_type = MDNS_STR_ARG(service.service_type); + while (*service_type == '_') { + service_type++; + } + + uint16_t port = const_cast &>(service.port).value(); + MDNS.addService(service_type, proto, port); + for (const auto &record : service.txt_records) { + MDNS.addServiceTxt(service_type, proto, MDNS_STR_ARG(record.key), MDNS_STR_ARG(record.value)); + } + } +} + +} // namespace esphome::mdns + +#endif From f5736303c31a314ac8825b8b7a83d2550c1be4ca Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 24 Nov 2025 15:36:31 -0600 Subject: [PATCH 270/415] fixs --- esphome/components/mdns/mdns_arduino.h | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/esphome/components/mdns/mdns_arduino.h b/esphome/components/mdns/mdns_arduino.h index 8d72c86f5c..e1292d201f 100644 --- a/esphome/components/mdns/mdns_arduino.h +++ b/esphome/components/mdns/mdns_arduino.h @@ -1,8 +1,8 @@ #pragma once -#include "esphome/core/defines.h" // Common Arduino mDNS registration for RP2040 and LibreTiny -#if defined(USE_MDNS) && (defined(USE_RP2040) || defined(USE_LIBRETINY)) +// NOTE: The platform's mDNS header (e.g., or ) must be +// included BEFORE this header to make the MDNS global available. #include "esphome/core/application.h" #include "mdns_component.h" @@ -38,5 +38,3 @@ inline void register_arduino_mdns(MDNSComponent *, StaticVector Date: Mon, 24 Nov 2025 15:39:52 -0600 Subject: [PATCH 271/415] mege --- esphome/components/mdns/mdns_component.cpp | 2 +- esphome/components/mdns/mdns_libretiny.cpp | 34 ++++++++++++++++++++-- esphome/components/mdns/mdns_rp2040.cpp | 34 ++++++++++++++++++++-- 3 files changed, 63 insertions(+), 7 deletions(-) diff --git a/esphome/components/mdns/mdns_component.cpp b/esphome/components/mdns/mdns_component.cpp index 47db92610a..3232cd3cac 100644 --- a/esphome/components/mdns/mdns_component.cpp +++ b/esphome/components/mdns/mdns_component.cpp @@ -120,7 +120,7 @@ void MDNSComponent::compile_records_(StaticVectorget_noise_ctx().has_psk(); + bool has_psk = api::global_api_server->get_noise_ctx()->has_psk(); const char *encryption_key = has_psk ? TXT_API_ENCRYPTION : TXT_API_ENCRYPTION_SUPPORTED; txt_records.push_back({MDNS_STR(encryption_key), MDNS_STR(NOISE_ENCRYPTION)}); #endif diff --git a/esphome/components/mdns/mdns_libretiny.cpp b/esphome/components/mdns/mdns_libretiny.cpp index 7b104ed2d7..986099fa1f 100644 --- a/esphome/components/mdns/mdns_libretiny.cpp +++ b/esphome/components/mdns/mdns_libretiny.cpp @@ -1,13 +1,41 @@ #include "esphome/core/defines.h" #if defined(USE_LIBRETINY) && defined(USE_MDNS) -#include +#include "esphome/components/network/ip_address.h" +#include "esphome/components/network/util.h" +#include "esphome/core/application.h" +#include "esphome/core/log.h" +#include "mdns_component.h" -#include "mdns_arduino.h" +#include namespace esphome::mdns { -void MDNSComponent::setup() { this->setup_buffers_and_register_(register_arduino_mdns); } +static void register_libretiny(MDNSComponent *, StaticVector &services) { + MDNS.begin(App.get_name().c_str()); + + for (const auto &service : services) { + // Strip the leading underscore from the proto and service_type. While it is + // part of the wire protocol to have an underscore, and for example ESP-IDF + // expects the underscore to be there, the ESP8266 implementation always adds + // the underscore itself. + auto *proto = MDNS_STR_ARG(service.proto); + while (*proto == '_') { + proto++; + } + auto *service_type = MDNS_STR_ARG(service.service_type); + while (*service_type == '_') { + service_type++; + } + uint16_t port_ = const_cast &>(service.port).value(); + MDNS.addService(service_type, proto, port_); + for (const auto &record : service.txt_records) { + MDNS.addServiceTxt(service_type, proto, MDNS_STR_ARG(record.key), MDNS_STR_ARG(record.value)); + } + } +} + +void MDNSComponent::setup() { this->setup_buffers_and_register_(register_libretiny); } void MDNSComponent::on_shutdown() {} diff --git a/esphome/components/mdns/mdns_rp2040.cpp b/esphome/components/mdns/mdns_rp2040.cpp index 59688e7b36..e4a9b60cdb 100644 --- a/esphome/components/mdns/mdns_rp2040.cpp +++ b/esphome/components/mdns/mdns_rp2040.cpp @@ -1,13 +1,41 @@ #include "esphome/core/defines.h" #if defined(USE_RP2040) && defined(USE_MDNS) -#include +#include "esphome/components/network/ip_address.h" +#include "esphome/components/network/util.h" +#include "esphome/core/application.h" +#include "esphome/core/log.h" +#include "mdns_component.h" -#include "mdns_arduino.h" +#include namespace esphome::mdns { -void MDNSComponent::setup() { this->setup_buffers_and_register_(register_arduino_mdns); } +static void register_rp2040(MDNSComponent *, StaticVector &services) { + MDNS.begin(App.get_name().c_str()); + + for (const auto &service : services) { + // Strip the leading underscore from the proto and service_type. While it is + // part of the wire protocol to have an underscore, and for example ESP-IDF + // expects the underscore to be there, the ESP8266 implementation always adds + // the underscore itself. + auto *proto = MDNS_STR_ARG(service.proto); + while (*proto == '_') { + proto++; + } + auto *service_type = MDNS_STR_ARG(service.service_type); + while (*service_type == '_') { + service_type++; + } + uint16_t port = const_cast &>(service.port).value(); + MDNS.addService(service_type, proto, port); + for (const auto &record : service.txt_records) { + MDNS.addServiceTxt(service_type, proto, MDNS_STR_ARG(record.key), MDNS_STR_ARG(record.value)); + } + } +} + +void MDNSComponent::setup() { this->setup_buffers_and_register_(register_rp2040); } void MDNSComponent::loop() { MDNS.update(); } From 9b50ed3589a2f9cd322d43ebbbec256dd44984e1 Mon Sep 17 00:00:00 2001 From: kbx81 Date: Mon, 24 Nov 2025 16:09:12 -0600 Subject: [PATCH 272/415] conditionally compile callbacks --- esphome/components/wifi/__init__.py | 14 ++++++++++++++ esphome/components/wifi/wifi_component.h | 4 ++++ esphome/components/wifi/wifi_component_esp8266.cpp | 8 ++++++++ esphome/components/wifi/wifi_component_esp_idf.cpp | 10 ++++++++++ .../components/wifi/wifi_component_libretiny.cpp | 10 ++++++++++ esphome/components/wifi/wifi_component_pico_w.cpp | 8 ++++++++ esphome/components/wifi_info/text_sensor.py | 13 +++++++++++++ esphome/core/defines.h | 1 + 8 files changed, 68 insertions(+) diff --git a/esphome/components/wifi/__init__.py b/esphome/components/wifi/__init__.py index 8a5e5329f1..16db3a990b 100644 --- a/esphome/components/wifi/__init__.py +++ b/esphome/components/wifi/__init__.py @@ -608,6 +608,7 @@ async def wifi_disable_to_code(config, action_id, template_arg, args): KEEP_SCAN_RESULTS_KEY = "wifi_keep_scan_results" RUNTIME_POWER_SAVE_KEY = "wifi_runtime_power_save" +WIFI_CALLBACKS_KEY = "wifi_callbacks" def request_wifi_scan_results(): @@ -633,6 +634,17 @@ def enable_runtime_power_save_control(): CORE.data[RUNTIME_POWER_SAVE_KEY] = True +def request_wifi_callbacks(): + """Request that WiFi callbacks be compiled in. + + Components that need to be notified about WiFi state changes (IP address changes, + scan results, connection state) should call this function during their code generation. + This enables the add_on_ip_state_callback(), add_on_wifi_scan_state_callback(), + and add_on_wifi_connect_state_callback() APIs. + """ + CORE.data[WIFI_CALLBACKS_KEY] = True + + @coroutine_with_priority(CoroPriority.FINAL) async def final_step(): """Final code generation step to configure optional WiFi features.""" @@ -642,6 +654,8 @@ async def final_step(): ) if CORE.data.get(RUNTIME_POWER_SAVE_KEY, False): cg.add_define("USE_WIFI_RUNTIME_POWER_SAVE") + if CORE.data.get(WIFI_CALLBACKS_KEY, False): + cg.add_define("USE_WIFI_CALLBACKS") @automation.register_action( diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index 8bfeb36c70..b6b956a12d 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -369,6 +369,7 @@ class WiFiComponent : public Component { int32_t get_wifi_channel(); +#ifdef USE_WIFI_CALLBACKS /// Add a callback that will be called on configuration changes (IP change, SSID change, etc.) /// @param callback The callback to be called; template arguments are: /// - IP addresses @@ -387,6 +388,7 @@ class WiFiComponent : public Component { void add_on_wifi_connect_state_callback(std::function &&callback) { this->wifi_connect_state_callback_.add(std::move(callback)); } +#endif // USE_WIFI_CALLBACKS #ifdef USE_WIFI_RUNTIME_POWER_SAVE /** Request high-performance mode (no power saving) for improved WiFi latency. @@ -544,9 +546,11 @@ class WiFiComponent : public Component { WiFiAP ap_; #endif optional output_power_; +#ifdef USE_WIFI_CALLBACKS CallbackManager ip_state_callback_; CallbackManager &)> wifi_scan_state_callback_; CallbackManager wifi_connect_state_callback_; +#endif // USE_WIFI_CALLBACKS ESPPreferenceObject pref_; #ifdef USE_WIFI_FAST_CONNECT ESPPreferenceObject fast_connect_pref_; diff --git a/esphome/components/wifi/wifi_component_esp8266.cpp b/esphome/components/wifi/wifi_component_esp8266.cpp index 28dc597620..540ad3a585 100644 --- a/esphome/components/wifi/wifi_component_esp8266.cpp +++ b/esphome/components/wifi/wifi_component_esp8266.cpp @@ -513,8 +513,10 @@ void WiFiComponent::wifi_event_callback(System_Event_t *event) { ESP_LOGV(TAG, "Connected ssid='%s' bssid=%s channel=%u", buf, format_mac_address_pretty(it.bssid).c_str(), it.channel); s_sta_connected = true; +#ifdef USE_WIFI_CALLBACKS global_wifi_component->wifi_connect_state_callback_.call(global_wifi_component->wifi_ssid(), global_wifi_component->wifi_bssid()); +#endif break; } case EVENT_STAMODE_DISCONNECTED: { @@ -534,7 +536,9 @@ void WiFiComponent::wifi_event_callback(System_Event_t *event) { } s_sta_connected = false; s_sta_connecting = false; +#ifdef USE_WIFI_CALLBACKS global_wifi_component->wifi_connect_state_callback_.call("", bssid_t({0, 0, 0, 0, 0, 0})); +#endif break; } case EVENT_STAMODE_AUTHMODE_CHANGE: { @@ -557,9 +561,11 @@ void WiFiComponent::wifi_event_callback(System_Event_t *event) { ESP_LOGV(TAG, "static_ip=%s gateway=%s netmask=%s", format_ip_addr(it.ip).c_str(), format_ip_addr(it.gw).c_str(), format_ip_addr(it.mask).c_str()); s_sta_got_ip = true; +#ifdef USE_WIFI_CALLBACKS global_wifi_component->ip_state_callback_.call(global_wifi_component->wifi_sta_ip_addresses(), global_wifi_component->get_dns_address(0), global_wifi_component->get_dns_address(1)); +#endif break; } case EVENT_STAMODE_DHCP_TIMEOUT: { @@ -734,7 +740,9 @@ void WiFiComponent::wifi_scan_done_callback_(void *arg, STATUS status) { it->is_hidden != 0); } this->scan_done_ = true; +#ifdef USE_WIFI_CALLBACKS global_wifi_component->wifi_scan_state_callback_.call(global_wifi_component->scan_result_); +#endif } #ifdef USE_WIFI_AP diff --git a/esphome/components/wifi/wifi_component_esp_idf.cpp b/esphome/components/wifi/wifi_component_esp_idf.cpp index 3bc37deb41..c20c96ced0 100644 --- a/esphome/components/wifi/wifi_component_esp_idf.cpp +++ b/esphome/components/wifi/wifi_component_esp_idf.cpp @@ -727,7 +727,9 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) { ESP_LOGV(TAG, "Connected ssid='%s' bssid=" LOG_SECRET("%s") " channel=%u, authmode=%s", buf, format_mac_address_pretty(it.bssid).c_str(), it.channel, get_auth_mode_str(it.authmode)); s_sta_connected = true; +#ifdef USE_WIFI_CALLBACKS this->wifi_connect_state_callback_.call(this->wifi_ssid(), this->wifi_bssid()); +#endif } else if (data->event_base == WIFI_EVENT && data->event_id == WIFI_EVENT_STA_DISCONNECTED) { const auto &it = data->data.sta_disconnected; @@ -751,7 +753,9 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) { s_sta_connected = false; s_sta_connecting = false; error_from_callback_ = true; +#ifdef USE_WIFI_CALLBACKS this->wifi_connect_state_callback_.call("", bssid_t({0, 0, 0, 0, 0, 0})); +#endif } else if (data->event_base == IP_EVENT && data->event_id == IP_EVENT_STA_GOT_IP) { const auto &it = data->data.ip_got_ip; @@ -760,14 +764,18 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) { #endif /* USE_NETWORK_IPV6 */ ESP_LOGV(TAG, "static_ip=" IPSTR " gateway=" IPSTR, IP2STR(&it.ip_info.ip), IP2STR(&it.ip_info.gw)); this->got_ipv4_address_ = true; +#ifdef USE_WIFI_CALLBACKS this->ip_state_callback_.call(this->wifi_sta_ip_addresses(), this->get_dns_address(0), this->get_dns_address(1)); +#endif #if USE_NETWORK_IPV6 } else if (data->event_base == IP_EVENT && data->event_id == IP_EVENT_GOT_IP6) { const auto &it = data->data.ip_got_ip6; ESP_LOGV(TAG, "IPv6 address=" IPV6STR, IPV62STR(it.ip6_info.ip)); this->num_ipv6_addresses_++; +#ifdef USE_WIFI_CALLBACKS this->ip_state_callback_.call(this->wifi_sta_ip_addresses(), this->get_dns_address(0), this->get_dns_address(1)); +#endif #endif /* USE_NETWORK_IPV6 */ } else if (data->event_base == IP_EVENT && data->event_id == IP_EVENT_STA_LOST_IP) { @@ -807,7 +815,9 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) { scan_result_.emplace_back(bssid, ssid, record.primary, record.rssi, record.authmode != WIFI_AUTH_OPEN, ssid.empty()); } +#ifdef USE_WIFI_CALLBACKS this->wifi_scan_state_callback_.call(this->scan_result_); +#endif } else if (data->event_base == WIFI_EVENT && data->event_id == WIFI_EVENT_AP_START) { ESP_LOGV(TAG, "AP start"); diff --git a/esphome/components/wifi/wifi_component_libretiny.cpp b/esphome/components/wifi/wifi_component_libretiny.cpp index bf1d7a5408..04d0d4fa85 100644 --- a/esphome/components/wifi/wifi_component_libretiny.cpp +++ b/esphome/components/wifi/wifi_component_libretiny.cpp @@ -287,7 +287,9 @@ void WiFiComponent::wifi_event_callback_(esphome_wifi_event_id_t event, esphome_ buf[it.ssid_len] = '\0'; ESP_LOGV(TAG, "Connected ssid='%s' bssid=" LOG_SECRET("%s") " channel=%u, authmode=%s", buf, format_mac_address_pretty(it.bssid).c_str(), it.channel, get_auth_mode_str(it.authmode)); +#ifdef USE_WIFI_CALLBACKS this->wifi_connect_state_callback_.call(this->wifi_ssid(), this->wifi_bssid()); +#endif break; } case ESPHOME_EVENT_ID_WIFI_STA_DISCONNECTED: { @@ -313,7 +315,9 @@ void WiFiComponent::wifi_event_callback_(esphome_wifi_event_id_t event, esphome_ } s_sta_connecting = false; +#ifdef USE_WIFI_CALLBACKS this->wifi_connect_state_callback_.call("", bssid_t({0, 0, 0, 0, 0, 0})); +#endif break; } case ESPHOME_EVENT_ID_WIFI_STA_AUTHMODE_CHANGE: { @@ -335,13 +339,17 @@ void WiFiComponent::wifi_event_callback_(esphome_wifi_event_id_t event, esphome_ ESP_LOGV(TAG, "static_ip=%s gateway=%s", format_ip4_addr(WiFi.localIP()).c_str(), format_ip4_addr(WiFi.gatewayIP()).c_str()); s_sta_connecting = false; +#ifdef USE_WIFI_CALLBACKS this->ip_state_callback_.call(this->wifi_sta_ip_addresses(), this->get_dns_address(0), this->get_dns_address(1)); +#endif break; } case ESPHOME_EVENT_ID_WIFI_STA_GOT_IP6: { // auto it = info.got_ip.ip_info; ESP_LOGV(TAG, "Got IPv6"); +#ifdef USE_WIFI_CALLBACKS this->ip_state_callback_.call(this->wifi_sta_ip_addresses(), this->get_dns_address(0), this->get_dns_address(1)); +#endif break; } case ESPHOME_EVENT_ID_WIFI_STA_LOST_IP: { @@ -435,7 +443,9 @@ void WiFiComponent::wifi_scan_done_callback_() { } WiFi.scanDelete(); this->scan_done_ = true; +#ifdef USE_WIFI_CALLBACKS this->wifi_scan_state_callback_.call(this->scan_result_); +#endif } #ifdef USE_WIFI_AP diff --git a/esphome/components/wifi/wifi_component_pico_w.cpp b/esphome/components/wifi/wifi_component_pico_w.cpp index 2cc7bd2567..326883c0c4 100644 --- a/esphome/components/wifi/wifi_component_pico_w.cpp +++ b/esphome/components/wifi/wifi_component_pico_w.cpp @@ -225,7 +225,9 @@ void WiFiComponent::wifi_loop_() { if (this->state_ == WIFI_COMPONENT_STATE_STA_SCANNING && !cyw43_wifi_scan_active(&cyw43_state)) { this->scan_done_ = true; ESP_LOGV(TAG, "Scan done"); +#ifdef USE_WIFI_CALLBACKS this->wifi_scan_state_callback_.call(this->scan_result_); +#endif } // Poll for connection state changes @@ -239,13 +241,17 @@ void WiFiComponent::wifi_loop_() { // Just connected s_sta_was_connected = true; ESP_LOGV(TAG, "Connected"); +#ifdef USE_WIFI_CALLBACKS this->wifi_connect_state_callback_.call(this->wifi_ssid(), this->wifi_bssid()); +#endif } else if (!is_connected && s_sta_was_connected) { // Just disconnected s_sta_was_connected = false; s_sta_had_ip = false; ESP_LOGV(TAG, "Disconnected"); +#ifdef USE_WIFI_CALLBACKS this->wifi_connect_state_callback_.call("", bssid_t({0, 0, 0, 0, 0, 0})); +#endif } // Detect IP address changes (only when connected) @@ -261,7 +267,9 @@ void WiFiComponent::wifi_loop_() { // Just got IP address s_sta_had_ip = true; ESP_LOGV(TAG, "Got IP address"); +#ifdef USE_WIFI_CALLBACKS this->ip_state_callback_.call(this->wifi_sta_ip_addresses(), this->get_dns_address(0), this->get_dns_address(1)); +#endif } } } diff --git a/esphome/components/wifi_info/text_sensor.py b/esphome/components/wifi_info/text_sensor.py index be402cfb7b..50fe31d151 100644 --- a/esphome/components/wifi_info/text_sensor.py +++ b/esphome/components/wifi_info/text_sensor.py @@ -70,6 +70,19 @@ async def setup_conf(config, key): async def to_code(config): + # Request WiFi callbacks for any sensor that needs them + if any( + key in config + for key in ( + CONF_SSID, + CONF_BSSID, + CONF_IP_ADDRESS, + CONF_DNS_ADDRESS, + CONF_SCAN_RESULTS, + ) + ): + wifi.request_wifi_callbacks() + await setup_conf(config, CONF_SSID) await setup_conf(config, CONF_BSSID) await setup_conf(config, CONF_MAC_ADDRESS) diff --git a/esphome/core/defines.h b/esphome/core/defines.h index 4b24c395b9..1373ea6366 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -210,6 +210,7 @@ #define USE_WEBSERVER_SORTING #define USE_WIFI_11KV_SUPPORT #define USE_WIFI_FAST_CONNECT +#define USE_WIFI_CALLBACKS #define USE_WIFI_RUNTIME_POWER_SAVE #define USB_HOST_MAX_REQUESTS 16 From c7d485e8bdc8305a5cb4228c66faf11384ca5092 Mon Sep 17 00:00:00 2001 From: kbx81 Date: Mon, 24 Nov 2025 17:08:55 -0600 Subject: [PATCH 273/415] Use set.intersection --- esphome/components/wifi_info/text_sensor.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/esphome/components/wifi_info/text_sensor.py b/esphome/components/wifi_info/text_sensor.py index 50fe31d151..8097767d3d 100644 --- a/esphome/components/wifi_info/text_sensor.py +++ b/esphome/components/wifi_info/text_sensor.py @@ -61,6 +61,15 @@ CONFIG_SCHEMA = cv.Schema( } ) +# Keys that require WiFi callbacks +_NETWORK_INFO_KEYS = { + CONF_SSID, + CONF_BSSID, + CONF_IP_ADDRESS, + CONF_DNS_ADDRESS, + CONF_SCAN_RESULTS, +} + async def setup_conf(config, key): if key in config: @@ -71,16 +80,7 @@ async def setup_conf(config, key): async def to_code(config): # Request WiFi callbacks for any sensor that needs them - if any( - key in config - for key in ( - CONF_SSID, - CONF_BSSID, - CONF_IP_ADDRESS, - CONF_DNS_ADDRESS, - CONF_SCAN_RESULTS, - ) - ): + if _NETWORK_INFO_KEYS & config.keys(): wifi.request_wifi_callbacks() await setup_conf(config, CONF_SSID) From a50c74471438ddfa876e0f72c248334275d85227 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 24 Nov 2025 17:50:36 -0600 Subject: [PATCH 274/415] Update text_sensor.py Co-authored-by: Keith Burzinski --- esphome/components/wifi_info/text_sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/wifi_info/text_sensor.py b/esphome/components/wifi_info/text_sensor.py index 8097767d3d..0feee3d4a9 100644 --- a/esphome/components/wifi_info/text_sensor.py +++ b/esphome/components/wifi_info/text_sensor.py @@ -80,7 +80,7 @@ async def setup_conf(config, key): async def to_code(config): # Request WiFi callbacks for any sensor that needs them - if _NETWORK_INFO_KEYS & config.keys(): + if _NETWORK_INFO_KEYS.intersection(config): wifi.request_wifi_callbacks() await setup_conf(config, CONF_SSID) From 90f38566ea5c3bfbdf738ce09b8644dc83f21357 Mon Sep 17 00:00:00 2001 From: Keith Burzinski Date: Mon, 24 Nov 2025 18:05:40 -0600 Subject: [PATCH 275/415] Update esphome/components/wifi/__init__.py Co-authored-by: J. Nick Koston --- esphome/components/wifi/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/wifi/__init__.py b/esphome/components/wifi/__init__.py index 16db3a990b..31d9ca0f70 100644 --- a/esphome/components/wifi/__init__.py +++ b/esphome/components/wifi/__init__.py @@ -634,7 +634,7 @@ def enable_runtime_power_save_control(): CORE.data[RUNTIME_POWER_SAVE_KEY] = True -def request_wifi_callbacks(): +def request_wifi_callbacks() -> None: """Request that WiFi callbacks be compiled in. Components that need to be notified about WiFi state changes (IP address changes, From f5bdbc7af2b4120690edae36e49d923dfed53d19 Mon Sep 17 00:00:00 2001 From: Keith Burzinski Date: Mon, 24 Nov 2025 18:19:27 -0600 Subject: [PATCH 276/415] More `const` Co-authored-by: J. Nick Koston --- esphome/components/wifi_info/wifi_info_text_sensor.cpp | 4 ++-- esphome/components/wifi_info/wifi_info_text_sensor.h | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/esphome/components/wifi_info/wifi_info_text_sensor.cpp b/esphome/components/wifi_info/wifi_info_text_sensor.cpp index bbf375970e..aba4d012d6 100644 --- a/esphome/components/wifi_info/wifi_info_text_sensor.cpp +++ b/esphome/components/wifi_info/wifi_info_text_sensor.cpp @@ -19,7 +19,7 @@ void IPAddressWiFiInfo::setup() { void IPAddressWiFiInfo::dump_config() { LOG_TEXT_SENSOR("", "IP Address", this); } -void IPAddressWiFiInfo::state_callback_(network::IPAddresses ips) { +void IPAddressWiFiInfo::state_callback_(const network::IPAddresses &ips) { this->publish_state(ips[0].str()); uint8_t sensor = 0; for (auto &ip : ips) { @@ -87,7 +87,7 @@ void SSIDWiFiInfo::setup() { void SSIDWiFiInfo::dump_config() { LOG_TEXT_SENSOR("", "SSID", this); } -void SSIDWiFiInfo::state_callback_(std::string &ssid) { this->publish_state(ssid); } +void SSIDWiFiInfo::state_callback_(const std::string &ssid) { this->publish_state(ssid); } /**************** * BSSIDWiFiInfo diff --git a/esphome/components/wifi_info/wifi_info_text_sensor.h b/esphome/components/wifi_info/wifi_info_text_sensor.h index 4daae00e9c..df9cd4eb3f 100644 --- a/esphome/components/wifi_info/wifi_info_text_sensor.h +++ b/esphome/components/wifi_info/wifi_info_text_sensor.h @@ -16,7 +16,7 @@ class IPAddressWiFiInfo : public Component, public text_sensor::TextSensor { void add_ip_sensors(uint8_t index, text_sensor::TextSensor *s) { this->ip_sensors_[index] = s; } protected: - void state_callback_(network::IPAddresses ips); + void state_callback_(const network::IPAddresses &ips); std::array ip_sensors_; }; @@ -45,7 +45,7 @@ class SSIDWiFiInfo : public Component, public text_sensor::TextSensor { void dump_config() override; protected: - void state_callback_(std::string &ssid); + void state_callback_(const std::string &ssid); }; class BSSIDWiFiInfo : public Component, public text_sensor::TextSensor { From 27547313cd44d88a2cd7b4ba7b1b48bb7ec5511f Mon Sep 17 00:00:00 2001 From: kbx81 Date: Mon, 24 Nov 2025 18:31:11 -0600 Subject: [PATCH 277/415] Suggestions from review --- esphome/components/wifi_info/wifi_info_text_sensor.cpp | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/esphome/components/wifi_info/wifi_info_text_sensor.cpp b/esphome/components/wifi_info/wifi_info_text_sensor.cpp index aba4d012d6..e843ae8998 100644 --- a/esphome/components/wifi_info/wifi_info_text_sensor.cpp +++ b/esphome/components/wifi_info/wifi_info_text_sensor.cpp @@ -6,6 +6,8 @@ namespace esphome::wifi_info { static const char *const TAG = "wifi_info"; +static constexpr size_t MAX_STATE_LENGTH = 255; + /******************** * IPAddressWiFiInfo *******************/ @@ -73,7 +75,10 @@ void ScanResultsWiFiInfo::state_callback_(const wifi::wifi_scan_vector_tpublish_state(scan_results.substr(0, 255)); + if (scan_results.length() > MAX_STATE_LENGTH) { + scan_results.resize(MAX_STATE_LENGTH); + } + this->publish_state(scan_results); } /*************** @@ -82,7 +87,7 @@ void ScanResultsWiFiInfo::state_callback_(const wifi::wifi_scan_vector_tadd_on_wifi_connect_state_callback( - [this](std::string ssid, wifi::bssid_t bssid) { this->state_callback_(ssid); }); + [this](const std::string &ssid, wifi::bssid_t bssid) { this->state_callback_(ssid); }); } void SSIDWiFiInfo::dump_config() { LOG_TEXT_SENSOR("", "SSID", this); } From a018809404877fcf28f7e1f929653d529f45b147 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 24 Nov 2025 19:38:13 -0600 Subject: [PATCH 278/415] [api] Use const char* pointers for light effects to eliminate heap allocations --- esphome/components/api/api.proto | 2 +- esphome/components/api/api_connection.cpp | 10 +++++++--- esphome/components/api/api_pb2.cpp | 10 +++++----- esphome/components/api/api_pb2.h | 2 +- esphome/components/api/api_pb2_dump.cpp | 2 +- 5 files changed, 15 insertions(+), 11 deletions(-) diff --git a/esphome/components/api/api.proto b/esphome/components/api/api.proto index 26d1fa6876..74a8e8ff7f 100644 --- a/esphome/components/api/api.proto +++ b/esphome/components/api/api.proto @@ -518,7 +518,7 @@ message ListEntitiesLightResponse { bool legacy_supports_color_temperature = 8 [deprecated=true]; float min_mireds = 9; float max_mireds = 10; - repeated string effects = 11; + repeated string effects = 11 [(container_pointer_no_template) = "FixedVector"]; bool disabled_by_default = 13; string icon = 14 [(field_ifdef) = "USE_ENTITY_ICON"]; EntityCategory entity_category = 15; diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index ebfc641537..12cbbb991d 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -484,12 +484,16 @@ uint16_t APIConnection::try_send_light_info(EntityBase *entity, APIConnection *c msg.min_mireds = traits.get_min_mireds(); msg.max_mireds = traits.get_max_mireds(); } + FixedVector effects_list; if (light->supports_effects()) { - msg.effects.emplace_back("None"); - for (auto *effect : light->get_effects()) { - msg.effects.emplace_back(effect->get_name()); + auto &light_effects = light->get_effects(); + effects_list.init(light_effects.size() + 1); + effects_list.push_back("None"); + for (auto *effect : light_effects) { + effects_list.push_back(effect->get_name()); } } + msg.effects = &effects_list; return fill_and_encode_entity_info(light, msg, ListEntitiesLightResponse::MESSAGE_TYPE, conn, remaining_size, is_single); } diff --git a/esphome/components/api/api_pb2.cpp b/esphome/components/api/api_pb2.cpp index d52135a566..c131815456 100644 --- a/esphome/components/api/api_pb2.cpp +++ b/esphome/components/api/api_pb2.cpp @@ -476,8 +476,8 @@ void ListEntitiesLightResponse::encode(ProtoWriteBuffer buffer) const { } buffer.encode_float(9, this->min_mireds); buffer.encode_float(10, this->max_mireds); - for (auto &it : this->effects) { - buffer.encode_string(11, it, true); + for (const char *it : *this->effects) { + buffer.encode_string(11, it, strlen(it), true); } buffer.encode_bool(13, this->disabled_by_default); #ifdef USE_ENTITY_ICON @@ -499,9 +499,9 @@ void ListEntitiesLightResponse::calculate_size(ProtoSize &size) const { } size.add_float(1, this->min_mireds); size.add_float(1, this->max_mireds); - if (!this->effects.empty()) { - for (const auto &it : this->effects) { - size.add_length_force(1, it.size()); + if (!this->effects->empty()) { + for (const char *it : *this->effects) { + size.add_length_force(1, strlen(it)); } } size.add_bool(1, this->disabled_by_default); diff --git a/esphome/components/api/api_pb2.h b/esphome/components/api/api_pb2.h index b19e92d4ff..93ece74d85 100644 --- a/esphome/components/api/api_pb2.h +++ b/esphome/components/api/api_pb2.h @@ -793,7 +793,7 @@ class ListEntitiesLightResponse final : public InfoResponseProtoMessage { const light::ColorModeMask *supported_color_modes{}; float min_mireds{0.0f}; float max_mireds{0.0f}; - std::vector effects{}; + const FixedVector *effects{}; void encode(ProtoWriteBuffer buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP diff --git a/esphome/components/api/api_pb2_dump.cpp b/esphome/components/api/api_pb2_dump.cpp index ea752ba3ba..a985e052ac 100644 --- a/esphome/components/api/api_pb2_dump.cpp +++ b/esphome/components/api/api_pb2_dump.cpp @@ -924,7 +924,7 @@ void ListEntitiesLightResponse::dump_to(std::string &out) const { } dump_field(out, "min_mireds", this->min_mireds); dump_field(out, "max_mireds", this->max_mireds); - for (const auto &it : this->effects) { + for (const auto &it : *this->effects) { dump_field(out, "effects", it, 4); } dump_field(out, "disabled_by_default", this->disabled_by_default); From 7f1a9a611f7cb8d5d8d17b7b096eeb5595cc8957 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 25 Nov 2025 02:09:02 +0000 Subject: [PATCH 279/415] Bump aioesphomeapi from 42.7.0 to 42.8.0 (#12092) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index df036eeccc..a5c919e95f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,7 +12,7 @@ platformio==6.1.18 # When updating platformio, also update /docker/Dockerfile esptool==5.1.0 click==8.1.7 esphome-dashboard==20251013.0 -aioesphomeapi==42.7.0 +aioesphomeapi==42.8.0 zeroconf==0.148.0 puremagic==1.30 ruamel.yaml==0.18.16 # dashboard_import From 2bc8a4a77980493101aa66f089d9f213d8f5213b Mon Sep 17 00:00:00 2001 From: Keith Burzinski Date: Mon, 24 Nov 2025 20:23:10 -0600 Subject: [PATCH 280/415] [wifi_info] Use callbacks instead of polling (#10748) Co-authored-by: J. Nick Koston --- esphome/components/wifi/__init__.py | 14 ++ esphome/components/wifi/automation.h | 116 +++++++++++++++ esphome/components/wifi/wifi_component.cpp | 6 +- esphome/components/wifi/wifi_component.h | 138 ++++-------------- .../wifi/wifi_component_esp8266.cpp | 22 ++- .../wifi/wifi_component_esp_idf.cpp | 22 ++- .../wifi/wifi_component_libretiny.cpp | 23 ++- .../components/wifi/wifi_component_pico_w.cpp | 61 +++++++- esphome/components/wifi_info/text_sensor.py | 37 +++-- .../wifi_info/wifi_info_text_sensor.cpp | 119 ++++++++++++++- .../wifi_info/wifi_info_text_sensor.h | 102 +++---------- esphome/core/defines.h | 1 + 12 files changed, 417 insertions(+), 244 deletions(-) create mode 100644 esphome/components/wifi/automation.h diff --git a/esphome/components/wifi/__init__.py b/esphome/components/wifi/__init__.py index 8a5e5329f1..31d9ca0f70 100644 --- a/esphome/components/wifi/__init__.py +++ b/esphome/components/wifi/__init__.py @@ -608,6 +608,7 @@ async def wifi_disable_to_code(config, action_id, template_arg, args): KEEP_SCAN_RESULTS_KEY = "wifi_keep_scan_results" RUNTIME_POWER_SAVE_KEY = "wifi_runtime_power_save" +WIFI_CALLBACKS_KEY = "wifi_callbacks" def request_wifi_scan_results(): @@ -633,6 +634,17 @@ def enable_runtime_power_save_control(): CORE.data[RUNTIME_POWER_SAVE_KEY] = True +def request_wifi_callbacks() -> None: + """Request that WiFi callbacks be compiled in. + + Components that need to be notified about WiFi state changes (IP address changes, + scan results, connection state) should call this function during their code generation. + This enables the add_on_ip_state_callback(), add_on_wifi_scan_state_callback(), + and add_on_wifi_connect_state_callback() APIs. + """ + CORE.data[WIFI_CALLBACKS_KEY] = True + + @coroutine_with_priority(CoroPriority.FINAL) async def final_step(): """Final code generation step to configure optional WiFi features.""" @@ -642,6 +654,8 @@ async def final_step(): ) if CORE.data.get(RUNTIME_POWER_SAVE_KEY, False): cg.add_define("USE_WIFI_RUNTIME_POWER_SAVE") + if CORE.data.get(WIFI_CALLBACKS_KEY, False): + cg.add_define("USE_WIFI_CALLBACKS") @automation.register_action( diff --git a/esphome/components/wifi/automation.h b/esphome/components/wifi/automation.h new file mode 100644 index 0000000000..7997baff65 --- /dev/null +++ b/esphome/components/wifi/automation.h @@ -0,0 +1,116 @@ +#pragma once + +#include "esphome/core/defines.h" +#ifdef USE_WIFI +#include "wifi_component.h" + +namespace esphome::wifi { + +template class WiFiConnectedCondition : public Condition { + public: + bool check(const Ts &...x) override { return global_wifi_component->is_connected(); } +}; + +template class WiFiEnabledCondition : public Condition { + public: + bool check(const Ts &...x) override { return !global_wifi_component->is_disabled(); } +}; + +template class WiFiAPActiveCondition : public Condition { + public: + bool check(const Ts &...x) override { return global_wifi_component->is_ap_active(); } +}; + +template class WiFiEnableAction : public Action { + public: + void play(const Ts &...x) override { global_wifi_component->enable(); } +}; + +template class WiFiDisableAction : public Action { + public: + void play(const Ts &...x) override { global_wifi_component->disable(); } +}; + +template class WiFiConfigureAction : public Action, public Component { + public: + TEMPLATABLE_VALUE(std::string, ssid) + TEMPLATABLE_VALUE(std::string, password) + TEMPLATABLE_VALUE(bool, save) + TEMPLATABLE_VALUE(uint32_t, connection_timeout) + + void play(const Ts &...x) override { + auto ssid = this->ssid_.value(x...); + auto password = this->password_.value(x...); + // Avoid multiple calls + if (this->connecting_) + return; + // If already connected to the same AP, do nothing + if (global_wifi_component->wifi_ssid() == ssid) { + // Callback to notify the user that the connection was successful + this->connect_trigger_->trigger(); + return; + } + // Create a new WiFiAP object with the new SSID and password + this->new_sta_.set_ssid(ssid); + this->new_sta_.set_password(password); + // Save the current STA + this->old_sta_ = global_wifi_component->get_sta(); + // Disable WiFi + global_wifi_component->disable(); + // Set the state to connecting + this->connecting_ = true; + // Store the new STA so once the WiFi is enabled, it will connect to it + // This is necessary because the WiFiComponent will raise an error and fallback to the saved STA + // if trying to connect to a new STA while already connected to another one + if (this->save_.value(x...)) { + global_wifi_component->save_wifi_sta(new_sta_.get_ssid(), new_sta_.get_password()); + } else { + global_wifi_component->set_sta(new_sta_); + } + // Enable WiFi + global_wifi_component->enable(); + // Set timeout for the connection + this->set_timeout("wifi-connect-timeout", this->connection_timeout_.value(x...), [this, x...]() { + // If the timeout is reached, stop connecting and revert to the old AP + global_wifi_component->disable(); + global_wifi_component->save_wifi_sta(old_sta_.get_ssid(), old_sta_.get_password()); + global_wifi_component->enable(); + // 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(); + }); + }); + } + + Trigger<> *get_connect_trigger() const { return this->connect_trigger_; } + Trigger<> *get_error_trigger() const { return this->error_trigger_; } + + void loop() override { + if (!this->connecting_) + return; + if (global_wifi_component->is_connected()) { + // The WiFi is connected, stop the timeout and reset the connecting flag + this->cancel_timeout("wifi-connect-timeout"); + this->cancel_timeout("wifi-fallback-timeout"); + this->connecting_ = false; + if (global_wifi_component->wifi_ssid() == this->new_sta_.get_ssid()) { + // Callback to notify the user that the connection was successful + this->connect_trigger_->trigger(); + } else { + // Callback to notify the user that the connection failed + this->error_trigger_->trigger(); + } + } + } + + protected: + bool connecting_{false}; + WiFiAP new_sta_; + WiFiAP old_sta_; + Trigger<> *connect_trigger_{new Trigger<>()}; + Trigger<> *error_trigger_{new Trigger<>()}; +}; + +} // namespace esphome::wifi +#endif diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index 41931a7785..d53de83bd3 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -37,8 +37,7 @@ #include "esphome/components/esp32_improv/esp32_improv_component.h" #endif -namespace esphome { -namespace wifi { +namespace esphome::wifi { static const char *const TAG = "wifi"; @@ -1813,6 +1812,5 @@ bool WiFiScanResult::operator==(const WiFiScanResult &rhs) const { return this-> WiFiComponent *global_wifi_component; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) -} // namespace wifi -} // namespace esphome +} // namespace esphome::wifi #endif diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index 0dac80ad21..b6b956a12d 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -54,8 +54,7 @@ extern "C" { #include #endif -namespace esphome { -namespace wifi { +namespace esphome::wifi { /// Sentinel value for RSSI when WiFi is not connected static constexpr int8_t WIFI_RSSI_DISCONNECTED = -127; @@ -370,6 +369,27 @@ class WiFiComponent : public Component { int32_t get_wifi_channel(); +#ifdef USE_WIFI_CALLBACKS + /// Add a callback that will be called on configuration changes (IP change, SSID change, etc.) + /// @param callback The callback to be called; template arguments are: + /// - IP addresses + /// - DNS address 1 + /// - DNS address 2 + void add_on_ip_state_callback( + std::function &&callback) { + this->ip_state_callback_.add(std::move(callback)); + } + /// - Wi-Fi scan results + void add_on_wifi_scan_state_callback(std::function &)> &&callback) { + this->wifi_scan_state_callback_.add(std::move(callback)); + } + /// - Wi-Fi SSID + /// - Wi-Fi BSSID + void add_on_wifi_connect_state_callback(std::function &&callback) { + this->wifi_connect_state_callback_.add(std::move(callback)); + } +#endif // USE_WIFI_CALLBACKS + #ifdef USE_WIFI_RUNTIME_POWER_SAVE /** Request high-performance mode (no power saving) for improved WiFi latency. * @@ -526,6 +546,11 @@ class WiFiComponent : public Component { WiFiAP ap_; #endif optional output_power_; +#ifdef USE_WIFI_CALLBACKS + CallbackManager ip_state_callback_; + CallbackManager &)> wifi_scan_state_callback_; + CallbackManager wifi_connect_state_callback_; +#endif // USE_WIFI_CALLBACKS ESPPreferenceObject pref_; #ifdef USE_WIFI_FAST_CONNECT ESPPreferenceObject fast_connect_pref_; @@ -590,112 +615,5 @@ class WiFiComponent : public Component { extern WiFiComponent *global_wifi_component; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) -template class WiFiConnectedCondition : public Condition { - public: - bool check(const Ts &...x) override { return global_wifi_component->is_connected(); } -}; - -template class WiFiEnabledCondition : public Condition { - public: - bool check(const Ts &...x) override { return !global_wifi_component->is_disabled(); } -}; - -template class WiFiAPActiveCondition : public Condition { - public: - bool check(const Ts &...x) override { return global_wifi_component->is_ap_active(); } -}; - -template class WiFiEnableAction : public Action { - public: - void play(const Ts &...x) override { global_wifi_component->enable(); } -}; - -template class WiFiDisableAction : public Action { - public: - void play(const Ts &...x) override { global_wifi_component->disable(); } -}; - -template class WiFiConfigureAction : public Action, public Component { - public: - TEMPLATABLE_VALUE(std::string, ssid) - TEMPLATABLE_VALUE(std::string, password) - TEMPLATABLE_VALUE(bool, save) - TEMPLATABLE_VALUE(uint32_t, connection_timeout) - - void play(const Ts &...x) override { - auto ssid = this->ssid_.value(x...); - auto password = this->password_.value(x...); - // Avoid multiple calls - if (this->connecting_) - return; - // If already connected to the same AP, do nothing - if (global_wifi_component->wifi_ssid() == ssid) { - // Callback to notify the user that the connection was successful - this->connect_trigger_->trigger(); - return; - } - // Create a new WiFiAP object with the new SSID and password - this->new_sta_.set_ssid(ssid); - this->new_sta_.set_password(password); - // Save the current STA - this->old_sta_ = global_wifi_component->get_sta(); - // Disable WiFi - global_wifi_component->disable(); - // Set the state to connecting - this->connecting_ = true; - // Store the new STA so once the WiFi is enabled, it will connect to it - // This is necessary because the WiFiComponent will raise an error and fallback to the saved STA - // if trying to connect to a new STA while already connected to another one - if (this->save_.value(x...)) { - global_wifi_component->save_wifi_sta(new_sta_.get_ssid(), new_sta_.get_password()); - } else { - global_wifi_component->set_sta(new_sta_); - } - // Enable WiFi - global_wifi_component->enable(); - // Set timeout for the connection - this->set_timeout("wifi-connect-timeout", this->connection_timeout_.value(x...), [this, x...]() { - // If the timeout is reached, stop connecting and revert to the old AP - global_wifi_component->disable(); - global_wifi_component->save_wifi_sta(old_sta_.get_ssid(), old_sta_.get_password()); - global_wifi_component->enable(); - // 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(); - }); - }); - } - - Trigger<> *get_connect_trigger() const { return this->connect_trigger_; } - Trigger<> *get_error_trigger() const { return this->error_trigger_; } - - void loop() override { - if (!this->connecting_) - return; - if (global_wifi_component->is_connected()) { - // The WiFi is connected, stop the timeout and reset the connecting flag - this->cancel_timeout("wifi-connect-timeout"); - this->cancel_timeout("wifi-fallback-timeout"); - this->connecting_ = false; - if (global_wifi_component->wifi_ssid() == this->new_sta_.get_ssid()) { - // Callback to notify the user that the connection was successful - this->connect_trigger_->trigger(); - } else { - // Callback to notify the user that the connection failed - this->error_trigger_->trigger(); - } - } - } - - protected: - bool connecting_{false}; - WiFiAP new_sta_; - WiFiAP old_sta_; - Trigger<> *connect_trigger_{new Trigger<>()}; - Trigger<> *error_trigger_{new Trigger<>()}; -}; - -} // namespace wifi -} // namespace esphome +} // namespace esphome::wifi #endif diff --git a/esphome/components/wifi/wifi_component_esp8266.cpp b/esphome/components/wifi/wifi_component_esp8266.cpp index 274a505db2..540ad3a585 100644 --- a/esphome/components/wifi/wifi_component_esp8266.cpp +++ b/esphome/components/wifi/wifi_component_esp8266.cpp @@ -38,8 +38,7 @@ extern "C" { #include "esphome/core/log.h" #include "esphome/core/util.h" -namespace esphome { -namespace wifi { +namespace esphome::wifi { static const char *const TAG = "wifi_esp8266"; @@ -514,6 +513,10 @@ void WiFiComponent::wifi_event_callback(System_Event_t *event) { ESP_LOGV(TAG, "Connected ssid='%s' bssid=%s channel=%u", buf, format_mac_address_pretty(it.bssid).c_str(), it.channel); s_sta_connected = true; +#ifdef USE_WIFI_CALLBACKS + global_wifi_component->wifi_connect_state_callback_.call(global_wifi_component->wifi_ssid(), + global_wifi_component->wifi_bssid()); +#endif break; } case EVENT_STAMODE_DISCONNECTED: { @@ -533,6 +536,9 @@ void WiFiComponent::wifi_event_callback(System_Event_t *event) { } s_sta_connected = false; s_sta_connecting = false; +#ifdef USE_WIFI_CALLBACKS + global_wifi_component->wifi_connect_state_callback_.call("", bssid_t({0, 0, 0, 0, 0, 0})); +#endif break; } case EVENT_STAMODE_AUTHMODE_CHANGE: { @@ -555,6 +561,11 @@ void WiFiComponent::wifi_event_callback(System_Event_t *event) { ESP_LOGV(TAG, "static_ip=%s gateway=%s netmask=%s", format_ip_addr(it.ip).c_str(), format_ip_addr(it.gw).c_str(), format_ip_addr(it.mask).c_str()); s_sta_got_ip = true; +#ifdef USE_WIFI_CALLBACKS + global_wifi_component->ip_state_callback_.call(global_wifi_component->wifi_sta_ip_addresses(), + global_wifi_component->get_dns_address(0), + global_wifi_component->get_dns_address(1)); +#endif break; } case EVENT_STAMODE_DHCP_TIMEOUT: { @@ -729,6 +740,9 @@ void WiFiComponent::wifi_scan_done_callback_(void *arg, STATUS status) { it->is_hidden != 0); } this->scan_done_ = true; +#ifdef USE_WIFI_CALLBACKS + global_wifi_component->wifi_scan_state_callback_.call(global_wifi_component->scan_result_); +#endif } #ifdef USE_WIFI_AP @@ -885,8 +899,6 @@ network::IPAddress WiFiComponent::wifi_gateway_ip_() { return {(const ip_addr_t network::IPAddress WiFiComponent::wifi_dns_ip_(int num) { return {(const ip_addr_t *) WiFi.dnsIP(num)}; } void WiFiComponent::wifi_loop_() {} -} // namespace wifi -} // namespace esphome - +} // namespace esphome::wifi #endif #endif diff --git a/esphome/components/wifi/wifi_component_esp_idf.cpp b/esphome/components/wifi/wifi_component_esp_idf.cpp index e6e914c0b4..c20c96ced0 100644 --- a/esphome/components/wifi/wifi_component_esp_idf.cpp +++ b/esphome/components/wifi/wifi_component_esp_idf.cpp @@ -41,8 +41,7 @@ #include "esphome/core/log.h" #include "esphome/core/util.h" -namespace esphome { -namespace wifi { +namespace esphome::wifi { static const char *const TAG = "wifi_esp32"; @@ -728,6 +727,9 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) { ESP_LOGV(TAG, "Connected ssid='%s' bssid=" LOG_SECRET("%s") " channel=%u, authmode=%s", buf, format_mac_address_pretty(it.bssid).c_str(), it.channel, get_auth_mode_str(it.authmode)); s_sta_connected = true; +#ifdef USE_WIFI_CALLBACKS + this->wifi_connect_state_callback_.call(this->wifi_ssid(), this->wifi_bssid()); +#endif } else if (data->event_base == WIFI_EVENT && data->event_id == WIFI_EVENT_STA_DISCONNECTED) { const auto &it = data->data.sta_disconnected; @@ -751,6 +753,9 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) { s_sta_connected = false; s_sta_connecting = false; error_from_callback_ = true; +#ifdef USE_WIFI_CALLBACKS + this->wifi_connect_state_callback_.call("", bssid_t({0, 0, 0, 0, 0, 0})); +#endif } else if (data->event_base == IP_EVENT && data->event_id == IP_EVENT_STA_GOT_IP) { const auto &it = data->data.ip_got_ip; @@ -759,12 +764,18 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) { #endif /* USE_NETWORK_IPV6 */ ESP_LOGV(TAG, "static_ip=" IPSTR " gateway=" IPSTR, IP2STR(&it.ip_info.ip), IP2STR(&it.ip_info.gw)); this->got_ipv4_address_ = true; +#ifdef USE_WIFI_CALLBACKS + this->ip_state_callback_.call(this->wifi_sta_ip_addresses(), this->get_dns_address(0), this->get_dns_address(1)); +#endif #if USE_NETWORK_IPV6 } else if (data->event_base == IP_EVENT && data->event_id == IP_EVENT_GOT_IP6) { const auto &it = data->data.ip_got_ip6; ESP_LOGV(TAG, "IPv6 address=" IPV6STR, IPV62STR(it.ip6_info.ip)); this->num_ipv6_addresses_++; +#ifdef USE_WIFI_CALLBACKS + this->ip_state_callback_.call(this->wifi_sta_ip_addresses(), this->get_dns_address(0), this->get_dns_address(1)); +#endif #endif /* USE_NETWORK_IPV6 */ } else if (data->event_base == IP_EVENT && data->event_id == IP_EVENT_STA_LOST_IP) { @@ -804,6 +815,9 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) { scan_result_.emplace_back(bssid, ssid, record.primary, record.rssi, record.authmode != WIFI_AUTH_OPEN, ssid.empty()); } +#ifdef USE_WIFI_CALLBACKS + this->wifi_scan_state_callback_.call(this->scan_result_); +#endif } else if (data->event_base == WIFI_EVENT && data->event_id == WIFI_EVENT_AP_START) { ESP_LOGV(TAG, "AP start"); @@ -1088,8 +1102,6 @@ network::IPAddress WiFiComponent::wifi_dns_ip_(int num) { return network::IPAddress(dns_ip); } -} // namespace wifi -} // namespace esphome - +} // namespace esphome::wifi #endif // USE_ESP32 #endif diff --git a/esphome/components/wifi/wifi_component_libretiny.cpp b/esphome/components/wifi/wifi_component_libretiny.cpp index 98cbfddb1d..04d0d4fa85 100644 --- a/esphome/components/wifi/wifi_component_libretiny.cpp +++ b/esphome/components/wifi/wifi_component_libretiny.cpp @@ -15,8 +15,7 @@ #include "esphome/core/log.h" #include "esphome/core/util.h" -namespace esphome { -namespace wifi { +namespace esphome::wifi { static const char *const TAG = "wifi_lt"; @@ -288,7 +287,9 @@ void WiFiComponent::wifi_event_callback_(esphome_wifi_event_id_t event, esphome_ buf[it.ssid_len] = '\0'; ESP_LOGV(TAG, "Connected ssid='%s' bssid=" LOG_SECRET("%s") " channel=%u, authmode=%s", buf, format_mac_address_pretty(it.bssid).c_str(), it.channel, get_auth_mode_str(it.authmode)); - +#ifdef USE_WIFI_CALLBACKS + this->wifi_connect_state_callback_.call(this->wifi_ssid(), this->wifi_bssid()); +#endif break; } case ESPHOME_EVENT_ID_WIFI_STA_DISCONNECTED: { @@ -314,6 +315,9 @@ void WiFiComponent::wifi_event_callback_(esphome_wifi_event_id_t event, esphome_ } s_sta_connecting = false; +#ifdef USE_WIFI_CALLBACKS + this->wifi_connect_state_callback_.call("", bssid_t({0, 0, 0, 0, 0, 0})); +#endif break; } case ESPHOME_EVENT_ID_WIFI_STA_AUTHMODE_CHANGE: { @@ -335,11 +339,17 @@ void WiFiComponent::wifi_event_callback_(esphome_wifi_event_id_t event, esphome_ ESP_LOGV(TAG, "static_ip=%s gateway=%s", format_ip4_addr(WiFi.localIP()).c_str(), format_ip4_addr(WiFi.gatewayIP()).c_str()); s_sta_connecting = false; +#ifdef USE_WIFI_CALLBACKS + this->ip_state_callback_.call(this->wifi_sta_ip_addresses(), this->get_dns_address(0), this->get_dns_address(1)); +#endif break; } case ESPHOME_EVENT_ID_WIFI_STA_GOT_IP6: { // auto it = info.got_ip.ip_info; ESP_LOGV(TAG, "Got IPv6"); +#ifdef USE_WIFI_CALLBACKS + this->ip_state_callback_.call(this->wifi_sta_ip_addresses(), this->get_dns_address(0), this->get_dns_address(1)); +#endif break; } case ESPHOME_EVENT_ID_WIFI_STA_LOST_IP: { @@ -433,6 +443,9 @@ void WiFiComponent::wifi_scan_done_callback_() { } WiFi.scanDelete(); this->scan_done_ = true; +#ifdef USE_WIFI_CALLBACKS + this->wifi_scan_state_callback_.call(this->scan_result_); +#endif } #ifdef USE_WIFI_AP @@ -493,8 +506,6 @@ network::IPAddress WiFiComponent::wifi_gateway_ip_() { return {WiFi.gatewayIP()} network::IPAddress WiFiComponent::wifi_dns_ip_(int num) { return {WiFi.dnsIP(num)}; } void WiFiComponent::wifi_loop_() {} -} // namespace wifi -} // namespace esphome - +} // namespace esphome::wifi #endif // USE_LIBRETINY #endif diff --git a/esphome/components/wifi/wifi_component_pico_w.cpp b/esphome/components/wifi/wifi_component_pico_w.cpp index 91766e8ab5..326883c0c4 100644 --- a/esphome/components/wifi/wifi_component_pico_w.cpp +++ b/esphome/components/wifi/wifi_component_pico_w.cpp @@ -1,4 +1,3 @@ - #include "wifi_component.h" #ifdef USE_WIFI @@ -15,11 +14,14 @@ #include "esphome/core/log.h" #include "esphome/core/util.h" -namespace esphome { -namespace wifi { +namespace esphome::wifi { static const char *const TAG = "wifi_pico_w"; +// Track previous state for detecting changes +static bool s_sta_was_connected = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) +static bool s_sta_had_ip = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) + bool WiFiComponent::wifi_mode_(optional sta, optional ap) { if (sta.has_value()) { if (sta.value()) { @@ -51,7 +53,7 @@ bool WiFiComponent::wifi_apply_power_save_() { return ret == 0; } -// TODO: The driver doesnt seem to have an API for this +// TODO: The driver doesn't seem to have an API for this bool WiFiComponent::wifi_apply_output_power_(float output_power) { return true; } bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) { @@ -219,16 +221,61 @@ network::IPAddress WiFiComponent::wifi_dns_ip_(int num) { } void WiFiComponent::wifi_loop_() { + // Handle scan completion if (this->state_ == WIFI_COMPONENT_STATE_STA_SCANNING && !cyw43_wifi_scan_active(&cyw43_state)) { this->scan_done_ = true; ESP_LOGV(TAG, "Scan done"); +#ifdef USE_WIFI_CALLBACKS + this->wifi_scan_state_callback_.call(this->scan_result_); +#endif + } + + // Poll for connection state changes + // The arduino-pico WiFi library doesn't have event callbacks like ESP8266/ESP32, + // so we need to poll the link status to detect state changes + auto status = cyw43_tcpip_link_status(&cyw43_state, CYW43_ITF_STA); + bool is_connected = (status == CYW43_LINK_UP); + + // Detect connection state change + if (is_connected && !s_sta_was_connected) { + // Just connected + s_sta_was_connected = true; + ESP_LOGV(TAG, "Connected"); +#ifdef USE_WIFI_CALLBACKS + this->wifi_connect_state_callback_.call(this->wifi_ssid(), this->wifi_bssid()); +#endif + } else if (!is_connected && s_sta_was_connected) { + // Just disconnected + s_sta_was_connected = false; + s_sta_had_ip = false; + ESP_LOGV(TAG, "Disconnected"); +#ifdef USE_WIFI_CALLBACKS + this->wifi_connect_state_callback_.call("", bssid_t({0, 0, 0, 0, 0, 0})); +#endif + } + + // Detect IP address changes (only when connected) + if (is_connected) { + bool has_ip = false; + // Check for any IP address (IPv4 or IPv6) + for (auto addr : addrList) { + has_ip = true; + break; + } + + if (has_ip && !s_sta_had_ip) { + // Just got IP address + s_sta_had_ip = true; + ESP_LOGV(TAG, "Got IP address"); +#ifdef USE_WIFI_CALLBACKS + this->ip_state_callback_.call(this->wifi_sta_ip_addresses(), this->get_dns_address(0), this->get_dns_address(1)); +#endif + } } } void WiFiComponent::wifi_pre_setup_() {} -} // namespace wifi -} // namespace esphome - +} // namespace esphome::wifi #endif #endif diff --git a/esphome/components/wifi_info/text_sensor.py b/esphome/components/wifi_info/text_sensor.py index a4da582c55..0feee3d4a9 100644 --- a/esphome/components/wifi_info/text_sensor.py +++ b/esphome/components/wifi_info/text_sensor.py @@ -15,31 +15,27 @@ DEPENDENCIES = ["wifi"] wifi_info_ns = cg.esphome_ns.namespace("wifi_info") IPAddressWiFiInfo = wifi_info_ns.class_( - "IPAddressWiFiInfo", text_sensor.TextSensor, cg.PollingComponent + "IPAddressWiFiInfo", text_sensor.TextSensor, cg.Component ) ScanResultsWiFiInfo = wifi_info_ns.class_( - "ScanResultsWiFiInfo", text_sensor.TextSensor, cg.PollingComponent -) -SSIDWiFiInfo = wifi_info_ns.class_( - "SSIDWiFiInfo", text_sensor.TextSensor, cg.PollingComponent + "ScanResultsWiFiInfo", text_sensor.TextSensor, cg.Component ) +SSIDWiFiInfo = wifi_info_ns.class_("SSIDWiFiInfo", text_sensor.TextSensor, cg.Component) BSSIDWiFiInfo = wifi_info_ns.class_( - "BSSIDWiFiInfo", text_sensor.TextSensor, cg.PollingComponent + "BSSIDWiFiInfo", text_sensor.TextSensor, cg.Component ) MacAddressWifiInfo = wifi_info_ns.class_( "MacAddressWifiInfo", text_sensor.TextSensor, cg.Component ) DNSAddressWifiInfo = wifi_info_ns.class_( - "DNSAddressWifiInfo", text_sensor.TextSensor, cg.PollingComponent + "DNSAddressWifiInfo", text_sensor.TextSensor, cg.Component ) CONFIG_SCHEMA = cv.Schema( { cv.Optional(CONF_IP_ADDRESS): text_sensor.text_sensor_schema( IPAddressWiFiInfo, entity_category=ENTITY_CATEGORY_DIAGNOSTIC - ) - .extend(cv.polling_component_schema("1s")) - .extend( + ).extend( { cv.Optional(f"address_{x}"): text_sensor.text_sensor_schema( entity_category=ENTITY_CATEGORY_DIAGNOSTIC, @@ -49,22 +45,31 @@ CONFIG_SCHEMA = cv.Schema( ), cv.Optional(CONF_SCAN_RESULTS): text_sensor.text_sensor_schema( ScanResultsWiFiInfo, entity_category=ENTITY_CATEGORY_DIAGNOSTIC - ).extend(cv.polling_component_schema("60s")), + ), cv.Optional(CONF_SSID): text_sensor.text_sensor_schema( SSIDWiFiInfo, entity_category=ENTITY_CATEGORY_DIAGNOSTIC - ).extend(cv.polling_component_schema("1s")), + ), cv.Optional(CONF_BSSID): text_sensor.text_sensor_schema( BSSIDWiFiInfo, entity_category=ENTITY_CATEGORY_DIAGNOSTIC - ).extend(cv.polling_component_schema("1s")), + ), cv.Optional(CONF_MAC_ADDRESS): text_sensor.text_sensor_schema( MacAddressWifiInfo, entity_category=ENTITY_CATEGORY_DIAGNOSTIC ), cv.Optional(CONF_DNS_ADDRESS): text_sensor.text_sensor_schema( DNSAddressWifiInfo, entity_category=ENTITY_CATEGORY_DIAGNOSTIC - ).extend(cv.polling_component_schema("1s")), + ), } ) +# Keys that require WiFi callbacks +_NETWORK_INFO_KEYS = { + CONF_SSID, + CONF_BSSID, + CONF_IP_ADDRESS, + CONF_DNS_ADDRESS, + CONF_SCAN_RESULTS, +} + async def setup_conf(config, key): if key in config: @@ -74,6 +79,10 @@ async def setup_conf(config, key): async def to_code(config): + # Request WiFi callbacks for any sensor that needs them + if _NETWORK_INFO_KEYS.intersection(config): + wifi.request_wifi_callbacks() + await setup_conf(config, CONF_SSID) await setup_conf(config, CONF_BSSID) await setup_conf(config, CONF_MAC_ADDRESS) diff --git a/esphome/components/wifi_info/wifi_info_text_sensor.cpp b/esphome/components/wifi_info/wifi_info_text_sensor.cpp index 2612e4af8d..abd590b168 100644 --- a/esphome/components/wifi_info/wifi_info_text_sensor.cpp +++ b/esphome/components/wifi_info/wifi_info_text_sensor.cpp @@ -2,18 +2,121 @@ #ifdef USE_WIFI #include "esphome/core/log.h" -namespace esphome { -namespace wifi_info { +namespace esphome::wifi_info { static const char *const TAG = "wifi_info"; +static constexpr size_t MAX_STATE_LENGTH = 255; + +/******************** + * IPAddressWiFiInfo + *******************/ + +void IPAddressWiFiInfo::setup() { + wifi::global_wifi_component->add_on_ip_state_callback( + [this](const network::IPAddresses &ips, const network::IPAddress &dns1_ip, const network::IPAddress &dns2_ip) { + this->state_callback_(ips); + }); +} + void IPAddressWiFiInfo::dump_config() { LOG_TEXT_SENSOR("", "IP Address", this); } -void ScanResultsWiFiInfo::dump_config() { LOG_TEXT_SENSOR("", "Scan Results", this); } -void SSIDWiFiInfo::dump_config() { LOG_TEXT_SENSOR("", "SSID", this); } -void BSSIDWiFiInfo::dump_config() { LOG_TEXT_SENSOR("", "BSSID", this); } -void MacAddressWifiInfo::dump_config() { LOG_TEXT_SENSOR("", "MAC Address", this); } + +void IPAddressWiFiInfo::state_callback_(const network::IPAddresses &ips) { + this->publish_state(ips[0].str()); + uint8_t sensor = 0; + for (const auto &ip : ips) { + if (ip.is_set()) { + if (this->ip_sensors_[sensor] != nullptr) { + this->ip_sensors_[sensor]->publish_state(ip.str()); + } + sensor++; + } + } +} + +/********************* + * DNSAddressWifiInfo + ********************/ + +void DNSAddressWifiInfo::setup() { + wifi::global_wifi_component->add_on_ip_state_callback( + [this](const network::IPAddresses &ips, const network::IPAddress &dns1_ip, const network::IPAddress &dns2_ip) { + this->state_callback_(dns1_ip, dns2_ip); + }); +} + void DNSAddressWifiInfo::dump_config() { LOG_TEXT_SENSOR("", "DNS Address", this); } -} // namespace wifi_info -} // namespace esphome +void DNSAddressWifiInfo::state_callback_(const network::IPAddress &dns1_ip, const network::IPAddress &dns2_ip) { + std::string dns_results = dns1_ip.str() + " " + dns2_ip.str(); + this->publish_state(dns_results); +} + +/********************** + * ScanResultsWiFiInfo + *********************/ + +void ScanResultsWiFiInfo::setup() { + wifi::global_wifi_component->add_on_wifi_scan_state_callback( + [this](const wifi::wifi_scan_vector_t &results) { this->state_callback_(results); }); +} + +void ScanResultsWiFiInfo::dump_config() { LOG_TEXT_SENSOR("", "Scan Results", this); } + +void ScanResultsWiFiInfo::state_callback_(const wifi::wifi_scan_vector_t &results) { + std::string scan_results; + for (const auto &scan : results) { + if (scan.get_is_hidden()) + continue; + + scan_results += scan.get_ssid(); + scan_results += ": "; + scan_results += esphome::to_string(scan.get_rssi()); + scan_results += "dB\n"; + } + // There's a limit of 255 characters per state; longer states just don't get sent so we truncate it + if (scan_results.length() > MAX_STATE_LENGTH) { + scan_results.resize(MAX_STATE_LENGTH); + } + this->publish_state(scan_results); +} + +/*************** + * SSIDWiFiInfo + **************/ + +void SSIDWiFiInfo::setup() { + wifi::global_wifi_component->add_on_wifi_connect_state_callback( + [this](const std::string &ssid, const wifi::bssid_t &bssid) { this->state_callback_(ssid); }); +} + +void SSIDWiFiInfo::dump_config() { LOG_TEXT_SENSOR("", "SSID", this); } + +void SSIDWiFiInfo::state_callback_(const std::string &ssid) { this->publish_state(ssid); } + +/**************** + * BSSIDWiFiInfo + ***************/ + +void BSSIDWiFiInfo::setup() { + wifi::global_wifi_component->add_on_wifi_connect_state_callback( + [this](const std::string &ssid, const wifi::bssid_t &bssid) { this->state_callback_(bssid); }); +} + +void BSSIDWiFiInfo::dump_config() { LOG_TEXT_SENSOR("", "BSSID", this); } + +void BSSIDWiFiInfo::state_callback_(const wifi::bssid_t &bssid) { + char buf[18] = "unknown"; + if (mac_address_is_valid(bssid.data())) { + format_mac_addr_upper(bssid.data(), buf); + } + this->publish_state(buf); +} +/********************* + * MacAddressWifiInfo + ********************/ + +void MacAddressWifiInfo::dump_config() { LOG_TEXT_SENSOR("", "MAC Address", this); } + +} // namespace esphome::wifi_info #endif diff --git a/esphome/components/wifi_info/wifi_info_text_sensor.h b/esphome/components/wifi_info/wifi_info_text_sensor.h index 0814336c43..12666b4059 100644 --- a/esphome/components/wifi_info/wifi_info_text_sensor.h +++ b/esphome/components/wifi_info/wifi_info_text_sensor.h @@ -7,121 +7,54 @@ #ifdef USE_WIFI #include -namespace esphome { -namespace wifi_info { +namespace esphome::wifi_info { -static constexpr size_t MAX_STATE_LENGTH = 255; - -class IPAddressWiFiInfo : public PollingComponent, public text_sensor::TextSensor { +class IPAddressWiFiInfo : public Component, public text_sensor::TextSensor { public: - void update() override { - auto ips = wifi::global_wifi_component->wifi_sta_ip_addresses(); - if (ips != this->last_ips_) { - this->last_ips_ = ips; - this->publish_state(ips[0].str()); - uint8_t sensor = 0; - for (auto &ip : ips) { - if (ip.is_set()) { - if (this->ip_sensors_[sensor] != nullptr) { - this->ip_sensors_[sensor]->publish_state(ip.str()); - } - sensor++; - } - } - } - } - float get_setup_priority() const override { return setup_priority::AFTER_WIFI; } + void setup() override; void dump_config() override; void add_ip_sensors(uint8_t index, text_sensor::TextSensor *s) { this->ip_sensors_[index] = s; } protected: - network::IPAddresses last_ips_; + void state_callback_(const network::IPAddresses &ips); std::array ip_sensors_; }; -class DNSAddressWifiInfo : public PollingComponent, public text_sensor::TextSensor { +class DNSAddressWifiInfo : public Component, public text_sensor::TextSensor { public: - void update() override { - auto dns_one = wifi::global_wifi_component->get_dns_address(0); - auto dns_two = wifi::global_wifi_component->get_dns_address(1); - - std::string dns_results = dns_one.str() + " " + dns_two.str(); - - if (dns_results != this->last_results_) { - this->last_results_ = dns_results; - this->publish_state(dns_results); - } - } - float get_setup_priority() const override { return setup_priority::AFTER_WIFI; } + void setup() override; void dump_config() override; protected: - std::string last_results_; + void state_callback_(const network::IPAddress &dns1_ip, const network::IPAddress &dns2_ip); }; -class ScanResultsWiFiInfo : public PollingComponent, public text_sensor::TextSensor { +class ScanResultsWiFiInfo : public Component, public text_sensor::TextSensor { public: - void update() override { - std::string scan_results; - for (auto &scan : wifi::global_wifi_component->get_scan_result()) { - if (scan.get_is_hidden()) - continue; - - scan_results += scan.get_ssid(); - scan_results += ": "; - scan_results += esphome::to_string(scan.get_rssi()); - scan_results += "dB\n"; - } - - // There's a limit of 255 characters per state. - // Longer states just don't get sent so we truncate it. - if (scan_results.length() > MAX_STATE_LENGTH) { - scan_results.resize(MAX_STATE_LENGTH); - } - if (this->last_scan_results_ != scan_results) { - this->last_scan_results_ = scan_results; - this->publish_state(scan_results); - } - } + void setup() override; float get_setup_priority() const override { return setup_priority::AFTER_WIFI; } void dump_config() override; protected: - std::string last_scan_results_; + void state_callback_(const wifi::wifi_scan_vector_t &results); }; -class SSIDWiFiInfo : public PollingComponent, public text_sensor::TextSensor { +class SSIDWiFiInfo : public Component, public text_sensor::TextSensor { public: - void update() override { - std::string ssid = wifi::global_wifi_component->wifi_ssid(); - if (this->last_ssid_ != ssid) { - this->last_ssid_ = ssid; - this->publish_state(this->last_ssid_); - } - } - float get_setup_priority() const override { return setup_priority::AFTER_WIFI; } + void setup() override; void dump_config() override; protected: - std::string last_ssid_; + void state_callback_(const std::string &ssid); }; -class BSSIDWiFiInfo : public PollingComponent, public text_sensor::TextSensor { +class BSSIDWiFiInfo : public Component, public text_sensor::TextSensor { public: - void update() override { - wifi::bssid_t bssid = wifi::global_wifi_component->wifi_bssid(); - if (memcmp(bssid.data(), last_bssid_.data(), 6) != 0) { - std::copy(bssid.begin(), bssid.end(), last_bssid_.begin()); - char buf[18]; - format_mac_addr_upper(bssid.data(), buf); - this->publish_state(buf); - } - } - float get_setup_priority() const override { return setup_priority::AFTER_WIFI; } + void setup() override; void dump_config() override; protected: - wifi::bssid_t last_bssid_; + void state_callback_(const wifi::bssid_t &bssid); }; class MacAddressWifiInfo : public Component, public text_sensor::TextSensor { @@ -133,6 +66,5 @@ class MacAddressWifiInfo : public Component, public text_sensor::TextSensor { void dump_config() override; }; -} // namespace wifi_info -} // namespace esphome +} // namespace esphome::wifi_info #endif diff --git a/esphome/core/defines.h b/esphome/core/defines.h index 4b24c395b9..1373ea6366 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -210,6 +210,7 @@ #define USE_WEBSERVER_SORTING #define USE_WIFI_11KV_SUPPORT #define USE_WIFI_FAST_CONNECT +#define USE_WIFI_CALLBACKS #define USE_WIFI_RUNTIME_POWER_SAVE #define USB_HOST_MAX_REQUESTS 16 From 24217eb257728bd2125fc709b8d67261ff106b09 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 24 Nov 2025 20:58:38 -0600 Subject: [PATCH 281/415] [ltr390] Simplify mode tracking with bitmask instead of vector/function --- esphome/components/ltr390/ltr390.cpp | 55 ++++++++++++++-------------- esphome/components/ltr390/ltr390.h | 14 +++---- 2 files changed, 35 insertions(+), 34 deletions(-) diff --git a/esphome/components/ltr390/ltr390.cpp b/esphome/components/ltr390/ltr390.cpp index c1885dcb6f..ba4a7ea5cb 100644 --- a/esphome/components/ltr390/ltr390.cpp +++ b/esphome/components/ltr390/ltr390.cpp @@ -104,12 +104,17 @@ void LTR390Component::read_uvs_() { } } -void LTR390Component::read_mode_(int mode_index) { - // Set mode - LTR390MODE mode = std::get<0>(this->mode_funcs_[mode_index]); - +void LTR390Component::standby_() { std::bitset<8> ctrl = this->reg(LTR390_MAIN_CTRL).get(); - ctrl[LTR390_CTRL_MODE] = mode; + ctrl[LTR390_CTRL_EN] = false; + this->reg(LTR390_MAIN_CTRL) = ctrl.to_ulong(); + this->reading_ = false; +} + +void LTR390Component::read_mode_(LTR390MODE mode) { + // Set mode + std::bitset<8> ctrl = this->reg(LTR390_MAIN_CTRL).get(); + ctrl[LTR390_CTRL_MODE] = (mode == LTR390_MODE_UVS); ctrl[LTR390_CTRL_EN] = true; this->reg(LTR390_MAIN_CTRL) = ctrl.to_ulong(); @@ -129,21 +134,18 @@ void LTR390Component::read_mode_(int mode_index) { } // After the sensor integration time do the following - this->set_timeout(int_time + LTR390_WAKEUP_TIME + LTR390_SETTLE_TIME, [this, mode_index]() { - // Read from the sensor - std::get<1>(this->mode_funcs_[mode_index])(); - - // If there are more modes to read then begin the next - // otherwise stop - if (mode_index + 1 < (int) this->mode_funcs_.size()) { - this->read_mode_(mode_index + 1); + this->set_timeout(int_time + LTR390_WAKEUP_TIME + LTR390_SETTLE_TIME, [this, mode]() { + // Read from the sensor and continue to next mode or standby + if (mode == LTR390_MODE_ALS) { + this->read_als_(); + if (this->enabled_modes_ & ENABLED_MODE_UVS) { + this->read_mode_(LTR390_MODE_UVS); + return; + } } else { - // put sensor in standby - std::bitset<8> ctrl = this->reg(LTR390_MAIN_CTRL).get(); - ctrl[LTR390_CTRL_EN] = false; - this->reg(LTR390_MAIN_CTRL) = ctrl.to_ulong(); - this->reading_ = false; + this->read_uvs_(); } + this->standby_(); }); } @@ -172,14 +174,12 @@ void LTR390Component::setup() { // Set sensor read state this->reading_ = false; - // If we need the light sensor then add to the list + // Determine which modes are enabled based on configured sensors if (this->light_sensor_ != nullptr || this->als_sensor_ != nullptr) { - this->mode_funcs_.emplace_back(LTR390_MODE_ALS, std::bind(<R390Component::read_als_, this)); + this->enabled_modes_ |= ENABLED_MODE_ALS; } - - // If we need the UV sensor then add to the list if (this->uvi_sensor_ != nullptr || this->uv_sensor_ != nullptr) { - this->mode_funcs_.emplace_back(LTR390_MODE_UVS, std::bind(<R390Component::read_uvs_, this)); + this->enabled_modes_ |= ENABLED_MODE_UVS; } } @@ -195,10 +195,11 @@ void LTR390Component::dump_config() { } void LTR390Component::update() { - if (!this->reading_ && !mode_funcs_.empty()) { - this->reading_ = true; - this->read_mode_(0); - } + if (this->reading_ || this->enabled_modes_ == 0) + return; + + this->reading_ = true; + this->read_mode_((this->enabled_modes_ & ENABLED_MODE_ALS) ? LTR390_MODE_ALS : LTR390_MODE_UVS); } } // namespace ltr390 diff --git a/esphome/components/ltr390/ltr390.h b/esphome/components/ltr390/ltr390.h index 7db73d68ff..47884b9166 100644 --- a/esphome/components/ltr390/ltr390.h +++ b/esphome/components/ltr390/ltr390.h @@ -1,7 +1,5 @@ #pragma once -#include -#include #include "esphome/components/i2c/i2c.h" #include "esphome/components/sensor/sensor.h" #include "esphome/core/component.h" @@ -60,17 +58,19 @@ class LTR390Component : public PollingComponent, public i2c::I2CDevice { void set_uv_sensor(sensor::Sensor *uv_sensor) { this->uv_sensor_ = uv_sensor; } protected: + static constexpr uint8_t ENABLED_MODE_ALS = 1 << 0; + static constexpr uint8_t ENABLED_MODE_UVS = 1 << 1; + optional read_sensor_data_(LTR390MODE mode); void read_als_(); void read_uvs_(); - void read_mode_(int mode_index); + void read_mode_(LTR390MODE mode); + void standby_(); - bool reading_; - - // a list of modes and corresponding read functions - std::vector>> mode_funcs_; + bool reading_{false}; + uint8_t enabled_modes_{0}; LTR390GAIN gain_als_; LTR390GAIN gain_uv_; From 1c808a3375a824f37cfdb6bfb067f96975be733b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 24 Nov 2025 21:19:18 -0600 Subject: [PATCH 282/415] [ble_client] Write static BLE data directly from flash without allocation (#11826) --- esphome/components/ble_client/automation.h | 27 +++++++++++++--------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/esphome/components/ble_client/automation.h b/esphome/components/ble_client/automation.h index 9c5646b3d1..bbc2dd05e0 100644 --- a/esphome/components/ble_client/automation.h +++ b/esphome/components/ble_client/automation.h @@ -122,16 +122,19 @@ template class BLEClientWriteAction : public Action, publ void play_complex(const Ts &...x) override { this->num_running_++; this->var_ = std::make_tuple(x...); - std::vector value; + + bool result; if (this->len_ >= 0) { - // Static mode: copy from flash to vector - value.assign(this->value_.data, this->value_.data + this->len_); + // Static mode: write directly from flash pointer + result = this->write(this->value_.data, this->len_); } else { - // Template mode: call function - value = this->value_.func(x...); + // Template mode: call function and write the vector + std::vector value = this->value_.func(x...); + result = this->write(value); } + // on write failure, continue the automation chain rather than stopping so that e.g. disconnect can work. - if (!write(value)) + if (!result) this->play_next_(x...); } @@ -144,15 +147,15 @@ template class BLEClientWriteAction : public Action, publ * errors. */ // initiate the write. Return true if all went well, will be followed by a WRITE_CHAR event. - bool write(const std::vector &value) { + bool write(const uint8_t *data, size_t len) { if (this->node_state != espbt::ClientState::ESTABLISHED) { esph_log_w(Automation::TAG, "Cannot write to BLE characteristic - not connected"); return false; } - esph_log_vv(Automation::TAG, "Will write %d bytes: %s", value.size(), format_hex_pretty(value).c_str()); - esp_err_t err = esp_ble_gattc_write_char(this->parent()->get_gattc_if(), this->parent()->get_conn_id(), - this->char_handle_, value.size(), const_cast(value.data()), - this->write_type_, ESP_GATT_AUTH_REQ_NONE); + esph_log_vv(Automation::TAG, "Will write %d bytes: %s", len, format_hex_pretty(data, len).c_str()); + esp_err_t err = + esp_ble_gattc_write_char(this->parent()->get_gattc_if(), this->parent()->get_conn_id(), this->char_handle_, len, + const_cast(data), this->write_type_, ESP_GATT_AUTH_REQ_NONE); if (err != ESP_OK) { esph_log_e(Automation::TAG, "Error writing to characteristic: %s!", esp_err_to_name(err)); return false; @@ -160,6 +163,8 @@ template class BLEClientWriteAction : public Action, publ return true; } + bool write(const std::vector &value) { return this->write(value.data(), value.size()); } + void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *param) override { switch (event) { From 46a26560fd32eceedc547b154018ddad4deefd8a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 24 Nov 2025 21:21:56 -0600 Subject: [PATCH 283/415] [template.alarm_control_panel] Replace std::map with FixedVector for heap and flash savings (#11893) --- .../template/alarm_control_panel/__init__.py | 6 +- .../template_alarm_control_panel.cpp | 35 +++-- .../template_alarm_control_panel.h | 20 ++- ...late_alarm_control_panel_many_sensors.yaml | 136 ++++++++++++++++++ ...mplate_alarm_control_panel_many_sensors.py | 118 +++++++++++++++ 5 files changed, 296 insertions(+), 19 deletions(-) create mode 100644 tests/integration/fixtures/template_alarm_control_panel_many_sensors.yaml create mode 100644 tests/integration/test_template_alarm_control_panel_many_sensors.py diff --git a/esphome/components/template/alarm_control_panel/__init__.py b/esphome/components/template/alarm_control_panel/__init__.py index 5d2421fcbc..256c7f276a 100644 --- a/esphome/components/template/alarm_control_panel/__init__.py +++ b/esphome/components/template/alarm_control_panel/__init__.py @@ -137,7 +137,11 @@ async def to_code(config): cg.add(var.set_arming_night_time(config[CONF_ARMING_NIGHT_TIME])) supports_arm_night = True - for sensor in config.get(CONF_BINARY_SENSORS, []): + if sensors := config.get(CONF_BINARY_SENSORS, []): + # Initialize FixedVector with the exact number of sensors + cg.add(var.init_sensors(len(sensors))) + + for sensor in sensors: bs = await cg.get_variable(sensor[CONF_INPUT]) flags = BinarySensorFlags[FLAG_NORMAL] diff --git a/esphome/components/template/alarm_control_panel/template_alarm_control_panel.cpp b/esphome/components/template/alarm_control_panel/template_alarm_control_panel.cpp index af662a05a0..f025435261 100644 --- a/esphome/components/template/alarm_control_panel/template_alarm_control_panel.cpp +++ b/esphome/components/template/alarm_control_panel/template_alarm_control_panel.cpp @@ -20,10 +20,13 @@ void TemplateAlarmControlPanel::add_sensor(binary_sensor::BinarySensor *sensor, // Save the flags and type. Assign a store index for the per sensor data type. SensorDataStore sd; sd.last_chime_state = false; - this->sensor_map_[sensor].flags = flags; - this->sensor_map_[sensor].type = type; + AlarmSensor alarm_sensor; + alarm_sensor.sensor = sensor; + alarm_sensor.info.flags = flags; + alarm_sensor.info.type = type; + alarm_sensor.info.store_index = this->next_store_index_++; + this->sensors_.push_back(alarm_sensor); this->sensor_data_.push_back(sd); - this->sensor_map_[sensor].store_index = this->next_store_index_++; }; static const LogString *sensor_type_to_string(AlarmSensorType type) { @@ -45,7 +48,7 @@ void TemplateAlarmControlPanel::dump_config() { ESP_LOGCONFIG(TAG, "TemplateAlarmControlPanel:\n" " Current State: %s\n" - " Number of Codes: %u\n" + " Number of Codes: %zu\n" " Requires Code To Arm: %s\n" " Arming Away Time: %" PRIu32 "s\n" " Arming Home Time: %" PRIu32 "s\n" @@ -58,7 +61,8 @@ void TemplateAlarmControlPanel::dump_config() { (this->arming_home_time_ / 1000), (this->arming_night_time_ / 1000), (this->pending_time_ / 1000), (this->trigger_time_ / 1000), this->get_supported_features()); #ifdef USE_BINARY_SENSOR - for (auto const &[sensor, info] : this->sensor_map_) { + for (const auto &alarm_sensor : this->sensors_) { + const uint16_t flags = alarm_sensor.info.flags; ESP_LOGCONFIG(TAG, " Binary Sensor:\n" " Name: %s\n" @@ -67,11 +71,10 @@ void TemplateAlarmControlPanel::dump_config() { " Armed night bypass: %s\n" " Auto bypass: %s\n" " Chime mode: %s", - sensor->get_name().c_str(), LOG_STR_ARG(sensor_type_to_string(info.type)), - TRUEFALSE(info.flags & BINARY_SENSOR_MODE_BYPASS_ARMED_HOME), - TRUEFALSE(info.flags & BINARY_SENSOR_MODE_BYPASS_ARMED_NIGHT), - TRUEFALSE(info.flags & BINARY_SENSOR_MODE_BYPASS_AUTO), - TRUEFALSE(info.flags & BINARY_SENSOR_MODE_CHIME)); + alarm_sensor.sensor->get_name().c_str(), LOG_STR_ARG(sensor_type_to_string(alarm_sensor.info.type)), + TRUEFALSE(flags & BINARY_SENSOR_MODE_BYPASS_ARMED_HOME), + TRUEFALSE(flags & BINARY_SENSOR_MODE_BYPASS_ARMED_NIGHT), + TRUEFALSE(flags & BINARY_SENSOR_MODE_BYPASS_AUTO), TRUEFALSE(flags & BINARY_SENSOR_MODE_CHIME)); } #endif } @@ -121,7 +124,9 @@ void TemplateAlarmControlPanel::loop() { #ifdef USE_BINARY_SENSOR // Test all of the sensors regardless of the alarm panel state - for (auto const &[sensor, info] : this->sensor_map_) { + for (const auto &alarm_sensor : this->sensors_) { + const auto &info = alarm_sensor.info; + auto *sensor = alarm_sensor.sensor; // Check for chime zones if (info.flags & BINARY_SENSOR_MODE_CHIME) { // Look for the transition from closed to open @@ -242,11 +247,11 @@ void TemplateAlarmControlPanel::arm_(optional code, alarm_control_p void TemplateAlarmControlPanel::bypass_before_arming() { #ifdef USE_BINARY_SENSOR - for (auto const &[sensor, info] : this->sensor_map_) { + for (const auto &alarm_sensor : this->sensors_) { // Check for faulted bypass_auto sensors and remove them from monitoring - if ((info.flags & BINARY_SENSOR_MODE_BYPASS_AUTO) && (sensor->state)) { - ESP_LOGW(TAG, "'%s' is faulted and will be automatically bypassed", sensor->get_name().c_str()); - this->bypassed_sensor_indicies_.push_back(info.store_index); + if ((alarm_sensor.info.flags & BINARY_SENSOR_MODE_BYPASS_AUTO) && (alarm_sensor.sensor->state)) { + ESP_LOGW(TAG, "'%s' is faulted and will be automatically bypassed", alarm_sensor.sensor->get_name().c_str()); + this->bypassed_sensor_indicies_.push_back(alarm_sensor.info.store_index); } } #endif diff --git a/esphome/components/template/alarm_control_panel/template_alarm_control_panel.h b/esphome/components/template/alarm_control_panel/template_alarm_control_panel.h index 202dc7c13f..80ce34b8ae 100644 --- a/esphome/components/template/alarm_control_panel/template_alarm_control_panel.h +++ b/esphome/components/template/alarm_control_panel/template_alarm_control_panel.h @@ -1,11 +1,12 @@ #pragma once #include -#include +#include #include "esphome/core/automation.h" #include "esphome/core/component.h" #include "esphome/core/defines.h" +#include "esphome/core/helpers.h" #include "esphome/components/alarm_control_panel/alarm_control_panel.h" @@ -49,6 +50,13 @@ struct SensorInfo { uint8_t store_index; }; +#ifdef USE_BINARY_SENSOR +struct AlarmSensor { + binary_sensor::BinarySensor *sensor; + SensorInfo info; +}; +#endif + class TemplateAlarmControlPanel final : public alarm_control_panel::AlarmControlPanel, public Component { public: TemplateAlarmControlPanel(); @@ -63,6 +71,12 @@ class TemplateAlarmControlPanel final : public alarm_control_panel::AlarmControl void bypass_before_arming(); #ifdef USE_BINARY_SENSOR + /** Initialize the sensors vector with the specified capacity. + * + * @param capacity The number of sensors to allocate space for. + */ + void init_sensors(size_t capacity) { this->sensors_.init(capacity); } + /** Add a binary_sensor to the alarm_panel. * * @param sensor The BinarySensor instance. @@ -122,8 +136,8 @@ class TemplateAlarmControlPanel final : public alarm_control_panel::AlarmControl protected: void control(const alarm_control_panel::AlarmControlPanelCall &call) override; #ifdef USE_BINARY_SENSOR - // This maps a binary sensor to its alarm specific info - std::map sensor_map_; + // List of binary sensors with their alarm-specific info + FixedVector sensors_; // a list of automatically bypassed sensors std::vector bypassed_sensor_indicies_; #endif diff --git a/tests/integration/fixtures/template_alarm_control_panel_many_sensors.yaml b/tests/integration/fixtures/template_alarm_control_panel_many_sensors.yaml new file mode 100644 index 0000000000..836d3f11d5 --- /dev/null +++ b/tests/integration/fixtures/template_alarm_control_panel_many_sensors.yaml @@ -0,0 +1,136 @@ +esphome: + name: template-alarm-many-sensors + friendly_name: "Template Alarm Control Panel with Many Sensors" + +logger: + +host: + +api: + +binary_sensor: + - platform: template + id: sensor1 + name: "Door 1" + - platform: template + id: sensor2 + name: "Door 2" + - platform: template + id: sensor3 + name: "Window 1" + - platform: template + id: sensor4 + name: "Window 2" + - platform: template + id: sensor5 + name: "Motion 1" + - platform: template + id: sensor6 + name: "Motion 2" + - platform: template + id: sensor7 + name: "Glass Break 1" + - platform: template + id: sensor8 + name: "Glass Break 2" + - platform: template + id: sensor9 + name: "Smoke Detector" + - platform: template + id: sensor10 + name: "CO Detector" + +alarm_control_panel: + - platform: template + id: test_alarm + name: "Test Alarm" + codes: + - "1234" + requires_code_to_arm: true + arming_away_time: 5s + arming_home_time: 3s + arming_night_time: 3s + pending_time: 10s + trigger_time: 300s + restore_mode: ALWAYS_DISARMED + binary_sensors: + - input: sensor1 + bypass_armed_home: false + bypass_armed_night: false + bypass_auto: true + chime: true + trigger_mode: DELAYED + - input: sensor2 + bypass_armed_home: false + bypass_armed_night: false + bypass_auto: true + chime: true + trigger_mode: DELAYED + - input: sensor3 + bypass_armed_home: true + bypass_armed_night: false + bypass_auto: false + chime: false + trigger_mode: DELAYED + - input: sensor4 + bypass_armed_home: true + bypass_armed_night: false + bypass_auto: false + chime: false + trigger_mode: DELAYED + - input: sensor5 + bypass_armed_home: false + bypass_armed_night: true + bypass_auto: false + chime: false + trigger_mode: INSTANT + - input: sensor6 + bypass_armed_home: false + bypass_armed_night: true + bypass_auto: false + chime: false + trigger_mode: INSTANT + - input: sensor7 + bypass_armed_home: false + bypass_armed_night: false + bypass_auto: false + chime: false + trigger_mode: INSTANT + - input: sensor8 + bypass_armed_home: false + bypass_armed_night: false + bypass_auto: false + chime: false + trigger_mode: INSTANT + - input: sensor9 + bypass_armed_home: false + bypass_armed_night: false + bypass_auto: false + chime: false + trigger_mode: INSTANT_ALWAYS + - input: sensor10 + bypass_armed_home: false + bypass_armed_night: false + bypass_auto: false + chime: false + trigger_mode: INSTANT_ALWAYS + on_disarmed: + - logger.log: "Alarm disarmed" + on_arming: + - logger.log: "Alarm arming" + on_armed_away: + - logger.log: "Alarm armed away" + on_armed_home: + - logger.log: "Alarm armed home" + on_armed_night: + - logger.log: "Alarm armed night" + on_pending: + - logger.log: "Alarm pending" + on_triggered: + - logger.log: "Alarm triggered" + on_cleared: + - logger.log: "Alarm cleared" + on_chime: + - logger.log: "Chime activated" + on_ready: + - logger.log: "Sensors ready state changed" diff --git a/tests/integration/test_template_alarm_control_panel_many_sensors.py b/tests/integration/test_template_alarm_control_panel_many_sensors.py new file mode 100644 index 0000000000..856815c731 --- /dev/null +++ b/tests/integration/test_template_alarm_control_panel_many_sensors.py @@ -0,0 +1,118 @@ +"""Integration test for template alarm control panel with many sensors.""" + +from __future__ import annotations + +import aioesphomeapi +from aioesphomeapi.model import APIIntEnum +import pytest + +from .state_utils import InitialStateHelper +from .types import APIClientConnectedFactory, RunCompiledFunction + + +class EspHomeACPFeatures(APIIntEnum): + """ESPHome AlarmControlPanel feature numbers.""" + + ARM_HOME = 1 + ARM_AWAY = 2 + ARM_NIGHT = 4 + TRIGGER = 8 + ARM_CUSTOM_BYPASS = 16 + ARM_VACATION = 32 + + +@pytest.mark.asyncio +async def test_template_alarm_control_panel_many_sensors( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """Test template alarm control panel with 10 binary sensors using FixedVector.""" + async with run_compiled(yaml_config), api_client_connected() as client: + # Get entity info first + entities, _ = await client.list_entities_services() + + # Find the alarm control panel and binary sensors + alarm_info: aioesphomeapi.AlarmControlPanelInfo | None = None + binary_sensors: list[aioesphomeapi.BinarySensorInfo] = [] + + for entity in entities: + if isinstance(entity, aioesphomeapi.AlarmControlPanelInfo): + alarm_info = entity + elif isinstance(entity, aioesphomeapi.BinarySensorInfo): + binary_sensors.append(entity) + + assert alarm_info is not None, "Alarm control panel entity info not found" + assert alarm_info.name == "Test Alarm" + assert alarm_info.requires_code is True + assert alarm_info.requires_code_to_arm is True + + # Verify we have 10 binary sensors + assert len(binary_sensors) == 10, ( + f"Expected 10 binary sensors, got {len(binary_sensors)}" + ) + + # Verify sensor names + expected_sensor_names = { + "Door 1", + "Door 2", + "Window 1", + "Window 2", + "Motion 1", + "Motion 2", + "Glass Break 1", + "Glass Break 2", + "Smoke Detector", + "CO Detector", + } + actual_sensor_names = {sensor.name for sensor in binary_sensors} + assert actual_sensor_names == expected_sensor_names, ( + f"Sensor names mismatch. Expected: {expected_sensor_names}, " + f"Got: {actual_sensor_names}" + ) + + # Use InitialStateHelper to wait for all initial states + state_helper = InitialStateHelper(entities) + + def on_state(state: aioesphomeapi.EntityState) -> None: + # We'll receive subsequent states here after initial states + pass + + client.subscribe_states(state_helper.on_state_wrapper(on_state)) + + # Wait for all initial states + await state_helper.wait_for_initial_states(timeout=5.0) + + # Verify the alarm state is disarmed initially + alarm_state = state_helper.initial_states.get(alarm_info.key) + assert alarm_state is not None, "Alarm control panel initial state not received" + assert isinstance(alarm_state, aioesphomeapi.AlarmControlPanelEntityState) + assert alarm_state.state == aioesphomeapi.AlarmControlPanelState.DISARMED, ( + f"Expected initial state DISARMED, got {alarm_state.state}" + ) + + # Verify all 10 binary sensors have initial states + binary_sensor_states = [ + state_helper.initial_states.get(sensor.key) for sensor in binary_sensors + ] + assert all(state is not None for state in binary_sensor_states), ( + "Not all binary sensors have initial states" + ) + + # Verify all binary sensor states are BinarySensorState type + for i, state in enumerate(binary_sensor_states): + assert isinstance(state, aioesphomeapi.BinarySensorState), ( + f"Binary sensor {i} state is not BinarySensorState: {type(state)}" + ) + + # Verify supported features + expected_features = ( + EspHomeACPFeatures.ARM_HOME + | EspHomeACPFeatures.ARM_AWAY + | EspHomeACPFeatures.ARM_NIGHT + | EspHomeACPFeatures.TRIGGER + ) + assert alarm_info.supported_features == expected_features, ( + f"Expected supported_features={expected_features} (ARM_HOME|ARM_AWAY|ARM_NIGHT|TRIGGER), " + f"got {alarm_info.supported_features}" + ) From b06c730a2657f885deefb64ca77ca5e2827602b1 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 24 Nov 2025 21:34:36 -0600 Subject: [PATCH 284/415] [web_server] Consolidate turn_on/turn_off handlers to eliminate duplicate lambdas --- esphome/components/web_server/web_server.cpp | 65 +++++++++++--------- 1 file changed, 36 insertions(+), 29 deletions(-) diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index cc51463fe7..6bf6524fbc 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -690,8 +690,14 @@ void WebServer::handle_fan_request(AsyncWebServerRequest *request, const UrlMatc } else if (match.method_equals("toggle")) { this->defer([obj]() { obj->toggle().perform(); }); request->send(200); - } else if (match.method_equals("turn_on") || match.method_equals("turn_off")) { - auto call = match.method_equals("turn_on") ? obj->turn_on() : obj->turn_off(); + } else { + bool is_on = match.method_equals("turn_on"); + bool is_off = match.method_equals("turn_off"); + if (!is_on && !is_off) { + request->send(404); + return; + } + auto call = is_on ? obj->turn_on() : obj->turn_off(); parse_int_param_(request, "speed_level", call, &decltype(call)::set_speed); @@ -715,8 +721,6 @@ void WebServer::handle_fan_request(AsyncWebServerRequest *request, const UrlMatc } this->defer([call]() mutable { call.perform(); }); request->send(200); - } else { - request->send(404); } return; } @@ -766,32 +770,35 @@ void WebServer::handle_light_request(AsyncWebServerRequest *request, const UrlMa } else if (match.method_equals("toggle")) { this->defer([obj]() { obj->toggle().perform(); }); request->send(200); - } else if (match.method_equals("turn_on")) { - auto call = obj->turn_on(); - - // Parse color parameters - parse_light_param_(request, "brightness", call, &decltype(call)::set_brightness, 255.0f); - parse_light_param_(request, "r", call, &decltype(call)::set_red, 255.0f); - parse_light_param_(request, "g", call, &decltype(call)::set_green, 255.0f); - parse_light_param_(request, "b", call, &decltype(call)::set_blue, 255.0f); - parse_light_param_(request, "white_value", call, &decltype(call)::set_white, 255.0f); - parse_light_param_(request, "color_temp", call, &decltype(call)::set_color_temperature); - - // Parse timing parameters - parse_light_param_uint_(request, "flash", call, &decltype(call)::set_flash_length, 1000); - parse_light_param_uint_(request, "transition", call, &decltype(call)::set_transition_length, 1000); - - parse_string_param_(request, "effect", call, &decltype(call)::set_effect); - - this->defer([call]() mutable { call.perform(); }); - request->send(200); - } else if (match.method_equals("turn_off")) { - auto call = obj->turn_off(); - parse_light_param_uint_(request, "transition", call, &decltype(call)::set_transition_length, 1000); - this->defer([call]() mutable { call.perform(); }); - request->send(200); } else { - request->send(404); + bool is_on = match.method_equals("turn_on"); + bool is_off = match.method_equals("turn_off"); + if (!is_on && !is_off) { + request->send(404); + return; + } + auto call = is_on ? obj->turn_on() : obj->turn_off(); + + if (is_on) { + // Parse color parameters + parse_light_param_(request, "brightness", call, &decltype(call)::set_brightness, 255.0f); + parse_light_param_(request, "r", call, &decltype(call)::set_red, 255.0f); + parse_light_param_(request, "g", call, &decltype(call)::set_green, 255.0f); + parse_light_param_(request, "b", call, &decltype(call)::set_blue, 255.0f); + parse_light_param_(request, "white_value", call, &decltype(call)::set_white, 255.0f); + parse_light_param_(request, "color_temp", call, &decltype(call)::set_color_temperature); + + // Parse timing parameters + parse_light_param_uint_(request, "flash", call, &decltype(call)::set_flash_length, 1000); + } + parse_light_param_uint_(request, "transition", call, &decltype(call)::set_transition_length, 1000); + + if (is_on) { + parse_string_param_(request, "effect", call, &decltype(call)::set_effect); + } + + this->defer([call]() mutable { call.perform(); }); + request->send(200); } return; } From 66a871840e1f0b6ba37b03f833b49e6bb73afaaf Mon Sep 17 00:00:00 2001 From: bdm310 Date: Mon, 24 Nov 2025 22:14:23 -0800 Subject: [PATCH 285/415] Add more lvgl arc update parameters (#12066) --- esphome/components/lvgl/widgets/arc.py | 54 +++++++++++++++++++------ tests/components/lvgl/lvgl-package.yaml | 12 ++++++ 2 files changed, 53 insertions(+), 13 deletions(-) diff --git a/esphome/components/lvgl/widgets/arc.py b/esphome/components/lvgl/widgets/arc.py index ef4da0d815..21530441f8 100644 --- a/esphome/components/lvgl/widgets/arc.py +++ b/esphome/components/lvgl/widgets/arc.py @@ -20,7 +20,13 @@ from ..defines import ( CONF_START_ANGLE, literal, ) -from ..lv_validation import get_start_value, lv_angle_degrees, lv_float, lv_int +from ..lv_validation import ( + get_start_value, + lv_angle_degrees, + lv_float, + lv_int, + lv_positive_int, +) from ..lvcode import lv, lv_expr, lv_obj from ..types import LvNumber, NumberType from . import Widget @@ -36,13 +42,20 @@ ARC_SCHEMA = cv.Schema( cv.Optional(CONF_ROTATION, default=0.0): lv_angle_degrees, cv.Optional(CONF_ADJUSTABLE, default=False): bool, cv.Optional(CONF_MODE, default="NORMAL"): ARC_MODES.one_of, - cv.Optional(CONF_CHANGE_RATE, default=720): cv.uint16_t, + cv.Optional(CONF_CHANGE_RATE, default=720): lv_positive_int, } ) ARC_MODIFY_SCHEMA = cv.Schema( { cv.Optional(CONF_VALUE): lv_float, + cv.Optional(CONF_MIN_VALUE): lv_int, + cv.Optional(CONF_MAX_VALUE): lv_int, + cv.Optional(CONF_START_ANGLE): lv_angle_degrees, + cv.Optional(CONF_END_ANGLE): lv_angle_degrees, + cv.Optional(CONF_ROTATION): lv_angle_degrees, + cv.Optional(CONF_MODE): ARC_MODES.one_of, + cv.Optional(CONF_CHANGE_RATE): lv_positive_int, } ) @@ -58,17 +71,34 @@ class ArcType(NumberType): ) async def to_code(self, w: Widget, config): - if CONF_MIN_VALUE in config: + if CONF_MIN_VALUE in config and CONF_MAX_VALUE in config: max_value = await lv_int.process(config[CONF_MAX_VALUE]) min_value = await lv_int.process(config[CONF_MIN_VALUE]) lv.arc_set_range(w.obj, min_value, max_value) - start = await lv_angle_degrees.process(config[CONF_START_ANGLE]) - end = await lv_angle_degrees.process(config[CONF_END_ANGLE]) - rotation = await lv_angle_degrees.process(config[CONF_ROTATION]) - lv.arc_set_bg_angles(w.obj, start, end) - lv.arc_set_rotation(w.obj, rotation) - lv.arc_set_mode(w.obj, literal(config[CONF_MODE])) - lv.arc_set_change_rate(w.obj, config[CONF_CHANGE_RATE]) + elif CONF_MIN_VALUE in config: + max_value = w.get_property(CONF_MAX_VALUE) + min_value = await lv_int.process(config[CONF_MIN_VALUE]) + lv.arc_set_range(w.obj, min_value, max_value) + elif CONF_MAX_VALUE in config: + max_value = await lv_int.process(config[CONF_MAX_VALUE]) + min_value = w.get_property(CONF_MIN_VALUE) + lv.arc_set_range(w.obj, min_value, max_value) + + await w.set_property( + CONF_START_ANGLE, + await lv_angle_degrees.process(config.get(CONF_START_ANGLE)), + ) + await w.set_property( + CONF_END_ANGLE, await lv_angle_degrees.process(config.get(CONF_END_ANGLE)) + ) + await w.set_property( + CONF_ROTATION, await lv_angle_degrees.process(config.get(CONF_ROTATION)) + ) + await w.set_property(CONF_MODE, config) + await w.set_property( + CONF_CHANGE_RATE, + await lv_positive_int.process(config.get(CONF_CHANGE_RATE)), + ) if CONF_ADJUSTABLE in config: if not config[CONF_ADJUSTABLE]: @@ -78,9 +108,7 @@ class ArcType(NumberType): # For some reason arc does not get automatically added to the default group lv.group_add_obj(lv_expr.group_get_default(), w.obj) - value = await get_start_value(config) - if value is not None: - lv.arc_set_value(w.obj, value) + await w.set_property(CONF_VALUE, await get_start_value(config)) arc_spec = ArcType() diff --git a/tests/components/lvgl/lvgl-package.yaml b/tests/components/lvgl/lvgl-package.yaml index 5839643638..d54aef8b4a 100644 --- a/tests/components/lvgl/lvgl-package.yaml +++ b/tests/components/lvgl/lvgl-package.yaml @@ -781,6 +781,18 @@ lvgl: arc_color: 0xFFFF00 focused: arc_color: 0x808080 + on_click: + then: + - lvgl.arc.update: + id: lv_arc_1 + value: !lambda return (int)((float)rand() / RAND_MAX * 100); + min_value: !lambda return (int)((float)rand() / RAND_MAX * 100); + max_value: !lambda return (int)((float)rand() / RAND_MAX * 100); + start_angle: !lambda return (int)((float)rand() / RAND_MAX * 100); + end_angle: !lambda return (int)((float)rand() / RAND_MAX * 100); + rotation: !lambda return (int)((float)rand() / RAND_MAX * 100); + change_rate: !lambda return (uint)((float)rand() / RAND_MAX * 100); + mode: NORMAL - bar: id: bar_id align: top_mid From 18c97a08c38193e4a2807c254a4ff52f265f2f28 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 25 Nov 2025 01:47:06 -0600 Subject: [PATCH 286/415] [esp8266] Use C++17 nested namespaces and constexpr (#12096) --- esphome/components/esp8266/core.h | 4 +--- esphome/components/esp8266/gpio.cpp | 9 +++++---- esphome/components/esp8266/gpio.h | 6 ++---- esphome/components/esp8266/preferences.cpp | 20 ++++++++++---------- esphome/components/esp8266/preferences.h | 6 ++---- 5 files changed, 20 insertions(+), 25 deletions(-) diff --git a/esphome/components/esp8266/core.h b/esphome/components/esp8266/core.h index ac33305669..1abe67be86 100644 --- a/esphome/components/esp8266/core.h +++ b/esphome/components/esp8266/core.h @@ -7,8 +7,6 @@ extern const uint8_t ESPHOME_ESP8266_GPIO_INITIAL_MODE[16]; extern const uint8_t ESPHOME_ESP8266_GPIO_INITIAL_LEVEL[16]; -namespace esphome { -namespace esp8266 {} // namespace esp8266 -} // namespace esphome +namespace esphome::esp8266 {} // namespace esphome::esp8266 #endif // USE_ESP8266 diff --git a/esphome/components/esp8266/gpio.cpp b/esphome/components/esp8266/gpio.cpp index ee3683c67d..17a495bc1d 100644 --- a/esphome/components/esp8266/gpio.cpp +++ b/esphome/components/esp8266/gpio.cpp @@ -3,8 +3,7 @@ #include "gpio.h" #include "esphome/core/log.h" -namespace esphome { -namespace esp8266 { +namespace esphome::esp8266 { static const char *const TAG = "esp8266"; @@ -110,9 +109,11 @@ void ESP8266GPIOPin::digital_write(bool value) { } void ESP8266GPIOPin::detach_interrupt() const { detachInterrupt(pin_); } -} // namespace esp8266 +} // namespace esphome::esp8266 -using namespace esp8266; +namespace esphome { + +using esp8266::ISRPinArg; bool IRAM_ATTR ISRInternalGPIOPin::digital_read() { auto *arg = reinterpret_cast(this->arg_); diff --git a/esphome/components/esp8266/gpio.h b/esphome/components/esp8266/gpio.h index a1b6d79b3b..213a5c54be 100644 --- a/esphome/components/esp8266/gpio.h +++ b/esphome/components/esp8266/gpio.h @@ -5,8 +5,7 @@ #include "esphome/core/hal.h" #include -namespace esphome { -namespace esp8266 { +namespace esphome::esp8266 { class ESP8266GPIOPin : public InternalGPIOPin { public: @@ -33,7 +32,6 @@ class ESP8266GPIOPin : public InternalGPIOPin { gpio::Flags flags_{}; }; -} // namespace esp8266 -} // namespace esphome +} // namespace esphome::esp8266 #endif // USE_ESP8266 diff --git a/esphome/components/esp8266/preferences.cpp b/esphome/components/esp8266/preferences.cpp index a26e9cc498..197d244dc4 100644 --- a/esphome/components/esp8266/preferences.cpp +++ b/esphome/components/esp8266/preferences.cpp @@ -15,24 +15,24 @@ extern "C" { #include #include -namespace esphome { -namespace esp8266 { +namespace esphome::esp8266 { static const char *const TAG = "esp8266.preferences"; -static bool s_prevent_write = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) 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 const uint32_t ESP_RTC_USER_MEM_START = 0x60001200; +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; + #define ESP_RTC_USER_MEM ((uint32_t *) ESP_RTC_USER_MEM_START) -static const uint32_t ESP_RTC_USER_MEM_SIZE_WORDS = 128; -static const uint32_t ESP_RTC_USER_MEM_SIZE_BYTES = ESP_RTC_USER_MEM_SIZE_WORDS * 4; #ifdef USE_ESP8266_PREFERENCES_FLASH -static const uint32_t ESP8266_FLASH_STORAGE_SIZE = 128; +static constexpr uint32_t ESP8266_FLASH_STORAGE_SIZE = 128; #else -static const uint32_t ESP8266_FLASH_STORAGE_SIZE = 64; +static constexpr uint32_t ESP8266_FLASH_STORAGE_SIZE = 64; #endif static inline bool esp_rtc_user_mem_read(uint32_t index, uint32_t *dest) { @@ -284,10 +284,10 @@ void setup_preferences() { } void preferences_prevent_write(bool prevent) { s_prevent_write = prevent; } -} // namespace esp8266 +} // namespace esphome::esp8266 +namespace esphome { ESPPreferences *global_preferences; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) - } // namespace esphome #endif // USE_ESP8266 diff --git a/esphome/components/esp8266/preferences.h b/esphome/components/esp8266/preferences.h index edec915794..16cf80a129 100644 --- a/esphome/components/esp8266/preferences.h +++ b/esphome/components/esp8266/preferences.h @@ -2,13 +2,11 @@ #ifdef USE_ESP8266 -namespace esphome { -namespace esp8266 { +namespace esphome::esp8266 { void setup_preferences(); void preferences_prevent_write(bool prevent); -} // namespace esp8266 -} // namespace esphome +} // namespace esphome::esp8266 #endif // USE_ESP8266 From 697c5f424ebf0aa9c07693cce2c1c79675b164f4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 25 Nov 2025 02:17:53 -0600 Subject: [PATCH 287/415] [api] Use const char* pointers for light effects to eliminate heap allocations (#12090) --- esphome/components/api/api.proto | 2 +- esphome/components/api/api_connection.cpp | 10 +++++++--- esphome/components/api/api_pb2.cpp | 10 +++++----- esphome/components/api/api_pb2.h | 2 +- esphome/components/api/api_pb2_dump.cpp | 2 +- 5 files changed, 15 insertions(+), 11 deletions(-) diff --git a/esphome/components/api/api.proto b/esphome/components/api/api.proto index 26d1fa6876..74a8e8ff7f 100644 --- a/esphome/components/api/api.proto +++ b/esphome/components/api/api.proto @@ -518,7 +518,7 @@ message ListEntitiesLightResponse { bool legacy_supports_color_temperature = 8 [deprecated=true]; float min_mireds = 9; float max_mireds = 10; - repeated string effects = 11; + repeated string effects = 11 [(container_pointer_no_template) = "FixedVector"]; bool disabled_by_default = 13; string icon = 14 [(field_ifdef) = "USE_ENTITY_ICON"]; EntityCategory entity_category = 15; diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index ebfc641537..12cbbb991d 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -484,12 +484,16 @@ uint16_t APIConnection::try_send_light_info(EntityBase *entity, APIConnection *c msg.min_mireds = traits.get_min_mireds(); msg.max_mireds = traits.get_max_mireds(); } + FixedVector effects_list; if (light->supports_effects()) { - msg.effects.emplace_back("None"); - for (auto *effect : light->get_effects()) { - msg.effects.emplace_back(effect->get_name()); + auto &light_effects = light->get_effects(); + effects_list.init(light_effects.size() + 1); + effects_list.push_back("None"); + for (auto *effect : light_effects) { + effects_list.push_back(effect->get_name()); } } + msg.effects = &effects_list; return fill_and_encode_entity_info(light, msg, ListEntitiesLightResponse::MESSAGE_TYPE, conn, remaining_size, is_single); } diff --git a/esphome/components/api/api_pb2.cpp b/esphome/components/api/api_pb2.cpp index d52135a566..c131815456 100644 --- a/esphome/components/api/api_pb2.cpp +++ b/esphome/components/api/api_pb2.cpp @@ -476,8 +476,8 @@ void ListEntitiesLightResponse::encode(ProtoWriteBuffer buffer) const { } buffer.encode_float(9, this->min_mireds); buffer.encode_float(10, this->max_mireds); - for (auto &it : this->effects) { - buffer.encode_string(11, it, true); + for (const char *it : *this->effects) { + buffer.encode_string(11, it, strlen(it), true); } buffer.encode_bool(13, this->disabled_by_default); #ifdef USE_ENTITY_ICON @@ -499,9 +499,9 @@ void ListEntitiesLightResponse::calculate_size(ProtoSize &size) const { } size.add_float(1, this->min_mireds); size.add_float(1, this->max_mireds); - if (!this->effects.empty()) { - for (const auto &it : this->effects) { - size.add_length_force(1, it.size()); + if (!this->effects->empty()) { + for (const char *it : *this->effects) { + size.add_length_force(1, strlen(it)); } } size.add_bool(1, this->disabled_by_default); diff --git a/esphome/components/api/api_pb2.h b/esphome/components/api/api_pb2.h index b19e92d4ff..93ece74d85 100644 --- a/esphome/components/api/api_pb2.h +++ b/esphome/components/api/api_pb2.h @@ -793,7 +793,7 @@ class ListEntitiesLightResponse final : public InfoResponseProtoMessage { const light::ColorModeMask *supported_color_modes{}; float min_mireds{0.0f}; float max_mireds{0.0f}; - std::vector effects{}; + const FixedVector *effects{}; void encode(ProtoWriteBuffer buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP diff --git a/esphome/components/api/api_pb2_dump.cpp b/esphome/components/api/api_pb2_dump.cpp index ea752ba3ba..a985e052ac 100644 --- a/esphome/components/api/api_pb2_dump.cpp +++ b/esphome/components/api/api_pb2_dump.cpp @@ -924,7 +924,7 @@ void ListEntitiesLightResponse::dump_to(std::string &out) const { } dump_field(out, "min_mireds", this->min_mireds); dump_field(out, "max_mireds", this->max_mireds); - for (const auto &it : this->effects) { + for (const auto &it : *this->effects) { dump_field(out, "effects", it, 4); } dump_field(out, "disabled_by_default", this->disabled_by_default); From c30b92019347b681698551857e06eddac406a1d7 Mon Sep 17 00:00:00 2001 From: Edward Firmo <94725493+edwardtfn@users.noreply.github.com> Date: Tue, 25 Nov 2025 13:48:32 +0100 Subject: [PATCH 288/415] [nextion] Do not set alternative baud rate when not specified or `<= 0` (#12097) --- esphome/components/nextion/nextion_upload_arduino.cpp | 3 +++ esphome/components/nextion/nextion_upload_idf.cpp | 3 +++ 2 files changed, 6 insertions(+) diff --git a/esphome/components/nextion/nextion_upload_arduino.cpp b/esphome/components/nextion/nextion_upload_arduino.cpp index b4d217d7aa..baea938729 100644 --- a/esphome/components/nextion/nextion_upload_arduino.cpp +++ b/esphome/components/nextion/nextion_upload_arduino.cpp @@ -174,6 +174,9 @@ bool Nextion::upload_tft(uint32_t baud_rate, bool exit_reparse) { // Check if baud rate is supported this->original_baud_rate_ = this->parent_->get_baud_rate(); + if (baud_rate <= 0) { + baud_rate = this->original_baud_rate_; + } ESP_LOGD(TAG, "Baud rate: %" PRIu32, baud_rate); // Define the configuration for the HTTP client diff --git a/esphome/components/nextion/nextion_upload_idf.cpp b/esphome/components/nextion/nextion_upload_idf.cpp index 3b0d65643d..942e3dd6c3 100644 --- a/esphome/components/nextion/nextion_upload_idf.cpp +++ b/esphome/components/nextion/nextion_upload_idf.cpp @@ -177,6 +177,9 @@ bool Nextion::upload_tft(uint32_t baud_rate, bool exit_reparse) { // Check if baud rate is supported this->original_baud_rate_ = this->parent_->get_baud_rate(); + if (baud_rate <= 0) { + baud_rate = this->original_baud_rate_; + } ESP_LOGD(TAG, "Baud rate: %" PRIu32, baud_rate); // Define the configuration for the HTTP client From cdf27f144766759efc1bb809a5f7631cc8a7cf85 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Tue, 25 Nov 2025 11:14:53 -0500 Subject: [PATCH 289/415] [esp32] Fix platformio flash size print (#12099) Co-authored-by: J. Nick Koston --- esphome/components/esp32/__init__.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index 59c6029334..d372af3e6a 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -854,6 +854,10 @@ def _configure_lwip_max_sockets(conf: dict) -> None: async def to_code(config): cg.add_platformio_option("board", config[CONF_BOARD]) cg.add_platformio_option("board_upload.flash_size", config[CONF_FLASH_SIZE]) + cg.add_platformio_option( + "board_upload.maximum_size", + int(config[CONF_FLASH_SIZE].removesuffix("MB")) * 1024 * 1024, + ) cg.set_cpp_standard("gnu++20") cg.add_build_flag("-DUSE_ESP32") cg.add_define("ESPHOME_BOARD", config[CONF_BOARD]) From a571033b43f3418c54f42ef078b357b90b6f8bed Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 25 Nov 2025 10:30:01 -0600 Subject: [PATCH 290/415] [script] Fix script.wait hanging when triggered from on_boot (#12102) --- esphome/components/script/script.h | 7 +- .../fixtures/script_wait_on_boot.yaml | 54 ++++++++ tests/integration/test_script_wait_on_boot.py | 130 ++++++++++++++++++ 3 files changed, 190 insertions(+), 1 deletion(-) create mode 100644 tests/integration/fixtures/script_wait_on_boot.yaml create mode 100644 tests/integration/test_script_wait_on_boot.py diff --git a/esphome/components/script/script.h b/esphome/components/script/script.h index d60ed657f7..3a0823f3cc 100644 --- a/esphome/components/script/script.h +++ b/esphome/components/script/script.h @@ -278,7 +278,12 @@ template class ScriptWaitAction : public Action, void setup() override { // Start with loop disabled - only enable when there's work to do - this->disable_loop(); + // IMPORTANT: Only disable if num_running_ is 0, otherwise play_complex() was already + // called before our setup() (e.g., from on_boot trigger at same priority level) + // and we must not undo its enable_loop() call + if (this->num_running_ == 0) { + this->disable_loop(); + } } void play_complex(const Ts &...x) override { diff --git a/tests/integration/fixtures/script_wait_on_boot.yaml b/tests/integration/fixtures/script_wait_on_boot.yaml new file mode 100644 index 0000000000..8736b02294 --- /dev/null +++ b/tests/integration/fixtures/script_wait_on_boot.yaml @@ -0,0 +1,54 @@ +esphome: + name: test-script-wait-on-boot + on_boot: + # Use default priority (600.0) which is same as ScriptWaitAction's setup priority + # This tests the race condition where on_boot runs before ScriptWaitAction::setup() + then: + - logger.log: "=== on_boot: Starting boot sequence ===" + - script.execute: show_start_page + - script.wait: show_start_page + - logger.log: "=== on_boot: First script completed, starting second ===" + - script.execute: flip_thru_pages + - script.wait: flip_thru_pages + - logger.log: "=== on_boot: All boot scripts completed successfully ===" + +host: + +api: + actions: + # Manual trigger for additional testing + - action: test_script_wait + then: + - logger.log: "=== Manual test: Starting ===" + - script.execute: show_start_page + - script.wait: show_start_page + - logger.log: "=== Manual test: First script completed ===" + - script.execute: flip_thru_pages + - script.wait: flip_thru_pages + - logger.log: "=== Manual test: All completed ===" + +logger: + level: DEBUG + +script: + # First script - simulates display initialization + - id: show_start_page + mode: single + then: + - logger.log: "show_start_page: Starting" + - delay: 100ms + - logger.log: "show_start_page: After delay 1" + - delay: 100ms + - logger.log: "show_start_page: Completed" + + # Second script - simulates page flip sequence + - id: flip_thru_pages + mode: single + then: + - logger.log: "flip_thru_pages: Starting" + - delay: 50ms + - logger.log: "flip_thru_pages: Page 1" + - delay: 50ms + - logger.log: "flip_thru_pages: Page 2" + - delay: 50ms + - logger.log: "flip_thru_pages: Completed" diff --git a/tests/integration/test_script_wait_on_boot.py b/tests/integration/test_script_wait_on_boot.py new file mode 100644 index 0000000000..478090f782 --- /dev/null +++ b/tests/integration/test_script_wait_on_boot.py @@ -0,0 +1,130 @@ +"""Integration test for script.wait during on_boot (issue #12043). + +This test verifies that script.wait works correctly when triggered from on_boot. +The issue was that ScriptWaitAction::setup() unconditionally disabled the loop, +even if play_complex() had already been called (from an on_boot trigger at the +same priority level) and enabled it. + +The race condition occurs because: +1. on_boot's default priority is 600.0 (setup_priority::DATA) +2. ScriptWaitAction's default setup priority is also DATA (600.0) +3. When they have the same priority, if on_boot runs first and triggers a script, + ScriptWaitAction::play_complex() enables the loop +4. Then ScriptWaitAction::setup() runs and unconditionally disables the loop +5. The wait never completes because the loop is disabled + +The fix adds a conditional check (like WaitUntilAction has) to only disable the +loop in setup() if num_running_ is 0. +""" + +from __future__ import annotations + +import asyncio +import re + +import pytest + +from .types import APIClientConnectedFactory, RunCompiledFunction + + +@pytest.mark.asyncio +async def test_script_wait_on_boot( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """Test that script.wait works correctly when triggered from on_boot. + + This reproduces issue #12043 where script.wait would hang forever when + triggered from on_boot due to a race condition in ScriptWaitAction::setup(). + """ + test_complete = asyncio.Event() + + # Track progress through the boot sequence + boot_started = False + first_script_started = False + first_script_completed = False + first_wait_returned = False + second_script_started = False + second_script_completed = False + all_completed = False + + # Patterns for boot sequence logs + boot_start_pattern = re.compile(r"on_boot: Starting boot sequence") + show_start_pattern = re.compile(r"show_start_page: Starting") + show_complete_pattern = re.compile(r"show_start_page: Completed") + first_wait_pattern = re.compile(r"on_boot: First script completed") + flip_start_pattern = re.compile(r"flip_thru_pages: Starting") + flip_complete_pattern = re.compile(r"flip_thru_pages: Completed") + all_complete_pattern = re.compile(r"on_boot: All boot scripts completed") + + def check_output(line: str) -> None: + """Check log output for boot sequence progress.""" + nonlocal boot_started, first_script_started, first_script_completed + nonlocal first_wait_returned, second_script_started, second_script_completed + nonlocal all_completed + + if boot_start_pattern.search(line): + boot_started = True + elif show_start_pattern.search(line): + first_script_started = True + elif show_complete_pattern.search(line): + first_script_completed = True + elif first_wait_pattern.search(line): + first_wait_returned = True + elif flip_start_pattern.search(line): + second_script_started = True + elif flip_complete_pattern.search(line): + second_script_completed = True + elif all_complete_pattern.search(line): + all_completed = True + test_complete.set() + + 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 == "test-script-wait-on-boot" + + # Wait for on_boot sequence to complete + # The boot sequence should complete automatically + # Timeout is generous to allow for delays in the scripts + try: + await asyncio.wait_for(test_complete.wait(), timeout=5.0) + except TimeoutError: + # Build a detailed error message showing where the boot sequence got stuck + progress = [] + if boot_started: + progress.append("boot started") + if first_script_started: + progress.append("show_start_page started") + if first_script_completed: + progress.append("show_start_page completed") + if first_wait_returned: + progress.append("first script.wait returned") + if second_script_started: + progress.append("flip_thru_pages started") + if second_script_completed: + progress.append("flip_thru_pages completed") + + if not first_wait_returned and first_script_completed: + pytest.fail( + f"Test timed out - script.wait hung after show_start_page completed! " + f"This is the issue #12043 bug. Progress: {', '.join(progress)}" + ) + else: + pytest.fail( + f"Test timed out. Progress: {', '.join(progress) if progress else 'none'}" + ) + + # Verify the complete boot sequence executed in order + assert boot_started, "on_boot did not start" + assert first_script_started, "show_start_page did not start" + assert first_script_completed, "show_start_page did not complete" + assert first_wait_returned, "First script.wait did not return" + assert second_script_started, "flip_thru_pages did not start" + assert second_script_completed, "flip_thru_pages did not complete" + assert all_completed, "Boot sequence did not complete" From cf8c2056444bf5b6f02accf6a8480b913d0f1c67 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 25 Nov 2025 12:15:45 -0600 Subject: [PATCH 291/415] [core] Reduce flash size by combining set_name() and set_object_id() calls (#11941) --- esphome/core/entity_base.cpp | 6 ++++++ esphome/core/entity_base.h | 3 +++ esphome/core/entity_helpers.py | 6 ++---- .../binary_sensor/test_binary_sensor.py | 2 +- tests/component_tests/button/test_button.py | 2 +- tests/component_tests/text/test_text.py | 2 +- .../text_sensor/test_text_sensor.py | 15 ++++++++++++--- tests/unit_tests/core/test_entity_helpers.py | 13 ++++++++++--- 8 files changed, 36 insertions(+), 13 deletions(-) diff --git a/esphome/core/entity_base.cpp b/esphome/core/entity_base.cpp index 4883c72cf1..046f99d8cc 100644 --- a/esphome/core/entity_base.cpp +++ b/esphome/core/entity_base.cpp @@ -74,6 +74,12 @@ void EntityBase::set_object_id(const char *object_id) { this->calc_object_id_(); } +void EntityBase::set_name_and_object_id(const char *name, const char *object_id) { + this->set_name(name); + this->object_id_c_str_ = object_id; + this->calc_object_id_(); +} + // Calculate Object ID Hash from Entity Name void EntityBase::calc_object_id_() { this->object_id_hash_ = diff --git a/esphome/core/entity_base.h b/esphome/core/entity_base.h index 2b52d66f76..aa9b92877a 100644 --- a/esphome/core/entity_base.h +++ b/esphome/core/entity_base.h @@ -41,6 +41,9 @@ class EntityBase { std::string get_object_id() const; void set_object_id(const char *object_id); + // Set both name and object_id in one call (reduces generated code size) + void set_name_and_object_id(const char *name, const char *object_id); + // Get the unique Object ID of this Entity uint32_t get_object_id_hash(); diff --git a/esphome/core/entity_helpers.py b/esphome/core/entity_helpers.py index 9b4786f835..f360b4d809 100644 --- a/esphome/core/entity_helpers.py +++ b/esphome/core/entity_helpers.py @@ -84,8 +84,6 @@ async def setup_entity(var: MockObj, config: ConfigType, platform: str) -> None: # Get device name for object ID calculation device_name = device_id_obj.id - add(var.set_name(config[CONF_NAME])) - # Calculate base object_id using the same logic as C++ # This must match the C++ behavior in esphome/core/entity_base.cpp base_object_id = get_base_entity_object_id( @@ -97,8 +95,8 @@ async def setup_entity(var: MockObj, config: ConfigType, platform: str) -> None: "Entity has empty name, using '%s' as object_id base", base_object_id ) - # Set the object ID - add(var.set_object_id(base_object_id)) + # Set both name and object_id in one call to reduce generated code size + add(var.set_name_and_object_id(config[CONF_NAME], base_object_id)) _LOGGER.debug( "Setting object_id '%s' for entity '%s' on platform '%s'", base_object_id, diff --git a/tests/component_tests/binary_sensor/test_binary_sensor.py b/tests/component_tests/binary_sensor/test_binary_sensor.py index 32d74027ba..86e0705023 100644 --- a/tests/component_tests/binary_sensor/test_binary_sensor.py +++ b/tests/component_tests/binary_sensor/test_binary_sensor.py @@ -29,7 +29,7 @@ def test_binary_sensor_sets_mandatory_fields(generate_main): ) # Then - assert 'bs_1->set_name("test bs1");' in main_cpp + assert 'bs_1->set_name_and_object_id("test bs1", "test_bs1");' in main_cpp assert "bs_1->set_pin(" in main_cpp diff --git a/tests/component_tests/button/test_button.py b/tests/component_tests/button/test_button.py index 512ef42b44..b21665288c 100644 --- a/tests/component_tests/button/test_button.py +++ b/tests/component_tests/button/test_button.py @@ -26,7 +26,7 @@ def test_button_sets_mandatory_fields(generate_main): main_cpp = generate_main("tests/component_tests/button/test_button.yaml") # Then - assert 'wol_1->set_name("wol_test_1");' in main_cpp + assert 'wol_1->set_name_and_object_id("wol_test_1", "wol_test_1");' in main_cpp assert "wol_2->set_macaddr(18, 52, 86, 120, 144, 171);" in main_cpp diff --git a/tests/component_tests/text/test_text.py b/tests/component_tests/text/test_text.py index 99ddd78ee7..bfc3131f6d 100644 --- a/tests/component_tests/text/test_text.py +++ b/tests/component_tests/text/test_text.py @@ -25,7 +25,7 @@ def test_text_sets_mandatory_fields(generate_main): main_cpp = generate_main("tests/component_tests/text/test_text.yaml") # Then - assert 'it_1->set_name("test 1 text");' in main_cpp + assert 'it_1->set_name_and_object_id("test 1 text", "test_1_text");' in main_cpp def test_text_config_value_internal_set(generate_main): diff --git a/tests/component_tests/text_sensor/test_text_sensor.py b/tests/component_tests/text_sensor/test_text_sensor.py index 1c4ef6633d..934ee67cef 100644 --- a/tests/component_tests/text_sensor/test_text_sensor.py +++ b/tests/component_tests/text_sensor/test_text_sensor.py @@ -25,9 +25,18 @@ def test_text_sensor_sets_mandatory_fields(generate_main): main_cpp = generate_main("tests/component_tests/text_sensor/test_text_sensor.yaml") # Then - assert 'ts_1->set_name("Template Text Sensor 1");' in main_cpp - assert 'ts_2->set_name("Template Text Sensor 2");' in main_cpp - assert 'ts_3->set_name("Template Text Sensor 3");' in main_cpp + assert ( + 'ts_1->set_name_and_object_id("Template Text Sensor 1", "template_text_sensor_1");' + in main_cpp + ) + assert ( + 'ts_2->set_name_and_object_id("Template Text Sensor 2", "template_text_sensor_2");' + in main_cpp + ) + assert ( + 'ts_3->set_name_and_object_id("Template Text Sensor 3", "template_text_sensor_3");' + in main_cpp + ) def test_text_sensor_config_value_internal_set(generate_main): diff --git a/tests/unit_tests/core/test_entity_helpers.py b/tests/unit_tests/core/test_entity_helpers.py index 9ba5367413..01de0f27f9 100644 --- a/tests/unit_tests/core/test_entity_helpers.py +++ b/tests/unit_tests/core/test_entity_helpers.py @@ -27,8 +27,13 @@ from esphome.helpers import sanitize, snake_case from .common import load_config_from_fixture -# Pre-compiled regex pattern for extracting object IDs from expressions +# Pre-compiled regex patterns for extracting object IDs from expressions +# Matches both old format: .set_object_id("obj_id") +# and new format: .set_name_and_object_id("name", "obj_id") OBJECT_ID_PATTERN = re.compile(r'\.set_object_id\(["\'](.*?)["\']\)') +COMBINED_PATTERN = re.compile( + r'\.set_name_and_object_id\(["\'].*?["\']\s*,\s*["\'](.*?)["\']\)' +) FIXTURES_DIR = Path(__file__).parent.parent / "fixtures" / "core" / "entity_helpers" @@ -273,8 +278,10 @@ def setup_test_environment() -> Generator[list[str], None, None]: def extract_object_id_from_expressions(expressions: list[str]) -> str | None: """Extract the object ID that was set from the generated expressions.""" for expr in expressions: - # Look for set_object_id calls with regex to handle various formats - # Matches: var.set_object_id("temperature_2") or var.set_object_id('temperature_2') + # First try new combined format: .set_name_and_object_id("name", "obj_id") + if match := COMBINED_PATTERN.search(expr): + return match.group(1) + # Fall back to old format: .set_object_id("obj_id") if match := OBJECT_ID_PATTERN.search(expr): return match.group(1) return None From 8c5985f68a4cc8e14e84b90577e98255ac1a51e1 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 25 Nov 2025 12:16:02 -0600 Subject: [PATCH 292/415] [web_server] Consolidate turn_on/turn_off handlers to eliminate duplicate lambdas (#12094) --- esphome/components/web_server/web_server.cpp | 65 +++++++++++--------- 1 file changed, 36 insertions(+), 29 deletions(-) diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index cc51463fe7..6bf6524fbc 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -690,8 +690,14 @@ void WebServer::handle_fan_request(AsyncWebServerRequest *request, const UrlMatc } else if (match.method_equals("toggle")) { this->defer([obj]() { obj->toggle().perform(); }); request->send(200); - } else if (match.method_equals("turn_on") || match.method_equals("turn_off")) { - auto call = match.method_equals("turn_on") ? obj->turn_on() : obj->turn_off(); + } else { + bool is_on = match.method_equals("turn_on"); + bool is_off = match.method_equals("turn_off"); + if (!is_on && !is_off) { + request->send(404); + return; + } + auto call = is_on ? obj->turn_on() : obj->turn_off(); parse_int_param_(request, "speed_level", call, &decltype(call)::set_speed); @@ -715,8 +721,6 @@ void WebServer::handle_fan_request(AsyncWebServerRequest *request, const UrlMatc } this->defer([call]() mutable { call.perform(); }); request->send(200); - } else { - request->send(404); } return; } @@ -766,32 +770,35 @@ void WebServer::handle_light_request(AsyncWebServerRequest *request, const UrlMa } else if (match.method_equals("toggle")) { this->defer([obj]() { obj->toggle().perform(); }); request->send(200); - } else if (match.method_equals("turn_on")) { - auto call = obj->turn_on(); - - // Parse color parameters - parse_light_param_(request, "brightness", call, &decltype(call)::set_brightness, 255.0f); - parse_light_param_(request, "r", call, &decltype(call)::set_red, 255.0f); - parse_light_param_(request, "g", call, &decltype(call)::set_green, 255.0f); - parse_light_param_(request, "b", call, &decltype(call)::set_blue, 255.0f); - parse_light_param_(request, "white_value", call, &decltype(call)::set_white, 255.0f); - parse_light_param_(request, "color_temp", call, &decltype(call)::set_color_temperature); - - // Parse timing parameters - parse_light_param_uint_(request, "flash", call, &decltype(call)::set_flash_length, 1000); - parse_light_param_uint_(request, "transition", call, &decltype(call)::set_transition_length, 1000); - - parse_string_param_(request, "effect", call, &decltype(call)::set_effect); - - this->defer([call]() mutable { call.perform(); }); - request->send(200); - } else if (match.method_equals("turn_off")) { - auto call = obj->turn_off(); - parse_light_param_uint_(request, "transition", call, &decltype(call)::set_transition_length, 1000); - this->defer([call]() mutable { call.perform(); }); - request->send(200); } else { - request->send(404); + bool is_on = match.method_equals("turn_on"); + bool is_off = match.method_equals("turn_off"); + if (!is_on && !is_off) { + request->send(404); + return; + } + auto call = is_on ? obj->turn_on() : obj->turn_off(); + + if (is_on) { + // Parse color parameters + parse_light_param_(request, "brightness", call, &decltype(call)::set_brightness, 255.0f); + parse_light_param_(request, "r", call, &decltype(call)::set_red, 255.0f); + parse_light_param_(request, "g", call, &decltype(call)::set_green, 255.0f); + parse_light_param_(request, "b", call, &decltype(call)::set_blue, 255.0f); + parse_light_param_(request, "white_value", call, &decltype(call)::set_white, 255.0f); + parse_light_param_(request, "color_temp", call, &decltype(call)::set_color_temperature); + + // Parse timing parameters + parse_light_param_uint_(request, "flash", call, &decltype(call)::set_flash_length, 1000); + } + parse_light_param_uint_(request, "transition", call, &decltype(call)::set_transition_length, 1000); + + if (is_on) { + parse_string_param_(request, "effect", call, &decltype(call)::set_effect); + } + + this->defer([call]() mutable { call.perform(); }); + request->send(200); } return; } From 310693467819523eea3a53b2a1681db50aec3e36 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 25 Nov 2025 12:16:27 -0600 Subject: [PATCH 293/415] [esp32_ble] Optimize name storage to reduce RAM and eliminate heap allocations (#12071) --- esphome/components/esp32_ble/ble.cpp | 31 ++++++++++++++++++---------- esphome/components/esp32_ble/ble.h | 6 ++---- esphome/core/application.h | 8 ++++--- esphome/core/helpers.cpp | 10 ++++++--- esphome/core/helpers.h | 11 ++++++++++ 5 files changed, 45 insertions(+), 21 deletions(-) diff --git a/esphome/components/esp32_ble/ble.cpp b/esphome/components/esp32_ble/ble.cpp index d0bfb6f843..a0ed9ee90c 100644 --- a/esphome/components/esp32_ble/ble.cpp +++ b/esphome/components/esp32_ble/ble.cpp @@ -256,29 +256,38 @@ bool ESP32BLE::ble_setup_() { } #endif - std::string name; - if (this->name_.has_value()) { - name = this->name_.value(); + const char *device_name; + std::string name_with_suffix; + + if (this->name_ != nullptr) { if (App.is_name_add_mac_suffix_enabled()) { + // MAC address length: 12 hex chars + null terminator + constexpr size_t mac_address_len = 13; // MAC address suffix length (last 6 characters of 12-char MAC address string) constexpr size_t mac_address_suffix_len = 6; - const std::string mac_addr = get_mac_address(); - const char *mac_suffix_ptr = mac_addr.c_str() + mac_address_suffix_len; - name = make_name_with_suffix(name, '-', mac_suffix_ptr, mac_address_suffix_len); + char mac_addr[mac_address_len]; + get_mac_address_into_buffer(mac_addr); + const char *mac_suffix_ptr = mac_addr + mac_address_suffix_len; + name_with_suffix = + make_name_with_suffix(this->name_, strlen(this->name_), '-', mac_suffix_ptr, mac_address_suffix_len); + device_name = name_with_suffix.c_str(); + } else { + device_name = this->name_; } } else { - name = App.get_name(); - if (name.length() > 20) { + name_with_suffix = App.get_name(); + if (name_with_suffix.length() > 20) { if (App.is_name_add_mac_suffix_enabled()) { // Keep first 13 chars and last 7 chars (MAC suffix), remove middle - name.erase(13, name.length() - 20); + name_with_suffix.erase(13, name_with_suffix.length() - 20); } else { - name.resize(20); + name_with_suffix.resize(20); } } + device_name = name_with_suffix.c_str(); } - err = esp_ble_gap_set_device_name(name.c_str()); + err = esp_ble_gap_set_device_name(device_name); if (err != ESP_OK) { ESP_LOGE(TAG, "esp_ble_gap_set_device_name failed: %d", err); return false; diff --git a/esphome/components/esp32_ble/ble.h b/esphome/components/esp32_ble/ble.h index 2fb60bb822..393ec2e911 100644 --- a/esphome/components/esp32_ble/ble.h +++ b/esphome/components/esp32_ble/ble.h @@ -112,7 +112,7 @@ class ESP32BLE : public Component { void loop() override; void dump_config() override; float get_setup_priority() const override; - void set_name(const std::string &name) { this->name_ = name; } + void set_name(const char *name) { this->name_ = name; } #ifdef USE_ESP32_BLE_ADVERTISING void advertising_start(); @@ -191,13 +191,11 @@ class ESP32BLE : public Component { esphome::LockFreeQueue ble_events_; esphome::EventPool ble_event_pool_; - // optional (typically 16+ bytes on 32-bit, aligned to 4 bytes) - optional name_; - // 4-byte aligned members #ifdef USE_ESP32_BLE_ADVERTISING BLEAdvertising *advertising_{}; // 4 bytes (pointer) #endif + const char *name_{nullptr}; // 4 bytes (pointer to string literal in flash) esp_ble_io_cap_t io_cap_{ESP_IO_CAP_NONE}; // 4 bytes (enum) uint32_t advertising_cycle_time_{}; // 4 bytes diff --git a/esphome/core/application.h b/esphome/core/application.h index dae44d8902..14e800342e 100644 --- a/esphome/core/application.h +++ b/esphome/core/application.h @@ -105,11 +105,13 @@ class Application { arch_init(); this->name_add_mac_suffix_ = name_add_mac_suffix; if (name_add_mac_suffix) { + // MAC address length: 12 hex chars + null terminator + constexpr size_t mac_address_len = 13; // MAC address suffix length (last 6 characters of 12-char MAC address string) constexpr size_t mac_address_suffix_len = 6; - const std::string mac_addr = get_mac_address(); - // Use pointer + offset to avoid substr() allocation - const char *mac_suffix_ptr = mac_addr.c_str() + mac_address_suffix_len; + char mac_addr[mac_address_len]; + get_mac_address_into_buffer(mac_addr); + const char *mac_suffix_ptr = mac_addr + mac_address_suffix_len; this->name_ = make_name_with_suffix(name, '-', mac_suffix_ptr, mac_address_suffix_len); if (!friendly_name.empty()) { this->friendly_name_ = make_name_with_suffix(friendly_name, ' ', mac_suffix_ptr, mac_address_suffix_len); diff --git a/esphome/core/helpers.cpp b/esphome/core/helpers.cpp index 50af71649c..1f675563c7 100644 --- a/esphome/core/helpers.cpp +++ b/esphome/core/helpers.cpp @@ -238,9 +238,9 @@ std::string str_sprintf(const char *fmt, ...) { // Maximum size for name with suffix: 120 (max friendly name) + 1 (separator) + 6 (MAC suffix) + 1 (null term) static constexpr size_t MAX_NAME_WITH_SUFFIX_SIZE = 128; -std::string make_name_with_suffix(const std::string &name, char sep, const char *suffix_ptr, size_t suffix_len) { +std::string make_name_with_suffix(const char *name, size_t name_len, char sep, const char *suffix_ptr, + size_t suffix_len) { char buffer[MAX_NAME_WITH_SUFFIX_SIZE]; - size_t name_len = name.size(); size_t total_len = name_len + 1 + suffix_len; // Silently truncate if needed: prioritize keeping the full suffix @@ -252,13 +252,17 @@ std::string make_name_with_suffix(const std::string &name, char sep, const char total_len = name_len + 1 + suffix_len; } - memcpy(buffer, name.c_str(), name_len); + memcpy(buffer, name, name_len); buffer[name_len] = sep; memcpy(buffer + name_len + 1, suffix_ptr, suffix_len); buffer[total_len] = '\0'; return std::string(buffer, total_len); } +std::string make_name_with_suffix(const std::string &name, char sep, const char *suffix_ptr, size_t suffix_len) { + return make_name_with_suffix(name.c_str(), name.size(), sep, suffix_ptr, suffix_len); +} + // Parsing & formatting size_t parse_hex(const char *str, size_t length, uint8_t *data, size_t count) { diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index d8c1f4647e..a43c55e06b 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -512,6 +512,17 @@ std::string __attribute__((format(printf, 1, 2))) str_sprintf(const char *fmt, . /// @return The concatenated string: name + sep + suffix std::string make_name_with_suffix(const std::string &name, char sep, const char *suffix_ptr, size_t suffix_len); +/// Optimized string concatenation: name + separator + suffix (const char* overload) +/// Uses a fixed stack buffer to avoid heap allocations. +/// @param name The base name string +/// @param name_len Length of the name +/// @param sep Single character separator +/// @param suffix_ptr Pointer to the suffix characters +/// @param suffix_len Length of the suffix +/// @return The concatenated string: name + sep + suffix +std::string make_name_with_suffix(const char *name, size_t name_len, char sep, const char *suffix_ptr, + size_t suffix_len); + ///@} /// @name Parsing & formatting From 6ca0cd1e8b3f6b1622acfdcd0b8ed6a4c84d1a0b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 25 Nov 2025 12:16:48 -0600 Subject: [PATCH 294/415] [ltr390] Simplify mode tracking with bitmask instead of vector/function (#12093) --- esphome/components/ltr390/ltr390.cpp | 55 ++++++++++++++-------------- esphome/components/ltr390/ltr390.h | 14 +++---- 2 files changed, 35 insertions(+), 34 deletions(-) diff --git a/esphome/components/ltr390/ltr390.cpp b/esphome/components/ltr390/ltr390.cpp index c1885dcb6f..ba4a7ea5cb 100644 --- a/esphome/components/ltr390/ltr390.cpp +++ b/esphome/components/ltr390/ltr390.cpp @@ -104,12 +104,17 @@ void LTR390Component::read_uvs_() { } } -void LTR390Component::read_mode_(int mode_index) { - // Set mode - LTR390MODE mode = std::get<0>(this->mode_funcs_[mode_index]); - +void LTR390Component::standby_() { std::bitset<8> ctrl = this->reg(LTR390_MAIN_CTRL).get(); - ctrl[LTR390_CTRL_MODE] = mode; + ctrl[LTR390_CTRL_EN] = false; + this->reg(LTR390_MAIN_CTRL) = ctrl.to_ulong(); + this->reading_ = false; +} + +void LTR390Component::read_mode_(LTR390MODE mode) { + // Set mode + std::bitset<8> ctrl = this->reg(LTR390_MAIN_CTRL).get(); + ctrl[LTR390_CTRL_MODE] = (mode == LTR390_MODE_UVS); ctrl[LTR390_CTRL_EN] = true; this->reg(LTR390_MAIN_CTRL) = ctrl.to_ulong(); @@ -129,21 +134,18 @@ void LTR390Component::read_mode_(int mode_index) { } // After the sensor integration time do the following - this->set_timeout(int_time + LTR390_WAKEUP_TIME + LTR390_SETTLE_TIME, [this, mode_index]() { - // Read from the sensor - std::get<1>(this->mode_funcs_[mode_index])(); - - // If there are more modes to read then begin the next - // otherwise stop - if (mode_index + 1 < (int) this->mode_funcs_.size()) { - this->read_mode_(mode_index + 1); + this->set_timeout(int_time + LTR390_WAKEUP_TIME + LTR390_SETTLE_TIME, [this, mode]() { + // Read from the sensor and continue to next mode or standby + if (mode == LTR390_MODE_ALS) { + this->read_als_(); + if (this->enabled_modes_ & ENABLED_MODE_UVS) { + this->read_mode_(LTR390_MODE_UVS); + return; + } } else { - // put sensor in standby - std::bitset<8> ctrl = this->reg(LTR390_MAIN_CTRL).get(); - ctrl[LTR390_CTRL_EN] = false; - this->reg(LTR390_MAIN_CTRL) = ctrl.to_ulong(); - this->reading_ = false; + this->read_uvs_(); } + this->standby_(); }); } @@ -172,14 +174,12 @@ void LTR390Component::setup() { // Set sensor read state this->reading_ = false; - // If we need the light sensor then add to the list + // Determine which modes are enabled based on configured sensors if (this->light_sensor_ != nullptr || this->als_sensor_ != nullptr) { - this->mode_funcs_.emplace_back(LTR390_MODE_ALS, std::bind(<R390Component::read_als_, this)); + this->enabled_modes_ |= ENABLED_MODE_ALS; } - - // If we need the UV sensor then add to the list if (this->uvi_sensor_ != nullptr || this->uv_sensor_ != nullptr) { - this->mode_funcs_.emplace_back(LTR390_MODE_UVS, std::bind(<R390Component::read_uvs_, this)); + this->enabled_modes_ |= ENABLED_MODE_UVS; } } @@ -195,10 +195,11 @@ void LTR390Component::dump_config() { } void LTR390Component::update() { - if (!this->reading_ && !mode_funcs_.empty()) { - this->reading_ = true; - this->read_mode_(0); - } + if (this->reading_ || this->enabled_modes_ == 0) + return; + + this->reading_ = true; + this->read_mode_((this->enabled_modes_ & ENABLED_MODE_ALS) ? LTR390_MODE_ALS : LTR390_MODE_UVS); } } // namespace ltr390 diff --git a/esphome/components/ltr390/ltr390.h b/esphome/components/ltr390/ltr390.h index 7db73d68ff..47884b9166 100644 --- a/esphome/components/ltr390/ltr390.h +++ b/esphome/components/ltr390/ltr390.h @@ -1,7 +1,5 @@ #pragma once -#include -#include #include "esphome/components/i2c/i2c.h" #include "esphome/components/sensor/sensor.h" #include "esphome/core/component.h" @@ -60,17 +58,19 @@ class LTR390Component : public PollingComponent, public i2c::I2CDevice { void set_uv_sensor(sensor::Sensor *uv_sensor) { this->uv_sensor_ = uv_sensor; } protected: + static constexpr uint8_t ENABLED_MODE_ALS = 1 << 0; + static constexpr uint8_t ENABLED_MODE_UVS = 1 << 1; + optional read_sensor_data_(LTR390MODE mode); void read_als_(); void read_uvs_(); - void read_mode_(int mode_index); + void read_mode_(LTR390MODE mode); + void standby_(); - bool reading_; - - // a list of modes and corresponding read functions - std::vector>> mode_funcs_; + bool reading_{false}; + uint8_t enabled_modes_{0}; LTR390GAIN gain_als_; LTR390GAIN gain_uv_; From dec323e786314b9fc1d3cdddd8c8153ab237e490 Mon Sep 17 00:00:00 2001 From: Nikolai Ryzhkov Date: Tue, 25 Nov 2025 19:27:35 +0100 Subject: [PATCH 295/415] [sht4x] Read and store a serial number of SHT4x sensors (#12089) Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Co-authored-by: J. Nick Koston --- esphome/components/sht4x/sht4x.cpp | 23 ++++++++++++++++++++++- esphome/components/sht4x/sht4x.h | 2 ++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/esphome/components/sht4x/sht4x.cpp b/esphome/components/sht4x/sht4x.cpp index 617b19ef3e..9d29746f0b 100644 --- a/esphome/components/sht4x/sht4x.cpp +++ b/esphome/components/sht4x/sht4x.cpp @@ -7,6 +7,7 @@ namespace sht4x { static const char *const TAG = "sht4x"; static const uint8_t MEASURECOMMANDS[] = {0xFD, 0xF6, 0xE0}; +static const uint8_t SERIAL_NUMBER_COMMAND = 0x89; void SHT4XComponent::start_heater_() { uint8_t cmd[] = {MEASURECOMMANDS[this->heater_command_]}; @@ -17,6 +18,17 @@ void SHT4XComponent::start_heater_() { } } +void SHT4XComponent::read_serial_number_() { + uint16_t buffer[2]; + if (!this->get_8bit_register(SERIAL_NUMBER_COMMAND, buffer, 2, 1)) { + ESP_LOGE(TAG, "Get serial number failed"); + this->serial_number_ = 0; + return; + } + this->serial_number_ = (uint32_t(buffer[0]) << 16) | (uint32_t(buffer[1])); + ESP_LOGD(TAG, "Serial number: %08" PRIx32, this->serial_number_); +} + void SHT4XComponent::setup() { auto err = this->write(nullptr, 0); if (err != i2c::ERROR_OK) { @@ -24,6 +36,8 @@ void SHT4XComponent::setup() { return; } + this->read_serial_number_(); + if (std::isfinite(this->duty_cycle_) && this->duty_cycle_ > 0.0f) { uint32_t heater_interval = static_cast(static_cast(this->heater_time_) / this->duty_cycle_); ESP_LOGD(TAG, "Heater interval: %" PRIu32, heater_interval); @@ -54,11 +68,18 @@ void SHT4XComponent::setup() { } void SHT4XComponent::dump_config() { - ESP_LOGCONFIG(TAG, "SHT4x:"); + ESP_LOGCONFIG(TAG, + "SHT4x:\n" + " Serial number: %08" PRIx32, + this->serial_number_); + LOG_I2C_DEVICE(this); if (this->is_failed()) { ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL); } + if (this->serial_number_ == 0) { + ESP_LOGW(TAG, "Get serial number failed"); + } } void SHT4XComponent::update() { diff --git a/esphome/components/sht4x/sht4x.h b/esphome/components/sht4x/sht4x.h index accc7323be..aec0f3d7f8 100644 --- a/esphome/components/sht4x/sht4x.h +++ b/esphome/components/sht4x/sht4x.h @@ -36,7 +36,9 @@ class SHT4XComponent : public PollingComponent, public sensirion_common::Sensiri float duty_cycle_; void start_heater_(); + void read_serial_number_(); uint8_t heater_command_; + uint32_t serial_number_; sensor::Sensor *temp_sensor_{nullptr}; sensor::Sensor *humidity_sensor_{nullptr}; From b6be5e3eda156568c114089b22a9ba1e989a6f12 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Wed, 26 Nov 2025 05:06:42 +1000 Subject: [PATCH 296/415] [lvgl] Allow multiple widgets per grid cell (#12091) --- esphome/components/lvgl/layout.py | 9 ++++++++- tests/components/lvgl/lvgl-package.yaml | 5 +++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/esphome/components/lvgl/layout.py b/esphome/components/lvgl/layout.py index a6aa816fda..caa503ef0d 100644 --- a/esphome/components/lvgl/layout.py +++ b/esphome/components/lvgl/layout.py @@ -36,6 +36,8 @@ from .defines import ( ) from .lv_validation import padding, size +CONF_MULTIPLE_WIDGETS_PER_CELL = "multiple_widgets_per_cell" + cell_alignments = LV_CELL_ALIGNMENTS.one_of grid_alignments = LV_GRID_ALIGNMENTS.one_of flex_alignments = LV_FLEX_ALIGNMENTS.one_of @@ -220,6 +222,7 @@ class GridLayout(Layout): cv.Optional(CONF_GRID_ROW_ALIGN): grid_alignments, cv.Optional(CONF_PAD_ROW): padding, cv.Optional(CONF_PAD_COLUMN): padding, + cv.Optional(CONF_MULTIPLE_WIDGETS_PER_CELL, default=False): cv.boolean, }, { cv.Optional(CONF_GRID_CELL_ROW_POS): cv.positive_int, @@ -263,6 +266,7 @@ class GridLayout(Layout): # should be guaranteed to be a dict at this point assert isinstance(layout, dict) assert layout.get(CONF_TYPE).lower() == TYPE_GRID + allow_multiple = layout.get(CONF_MULTIPLE_WIDGETS_PER_CELL, False) rows = len(layout[CONF_GRID_ROWS]) columns = len(layout[CONF_GRID_COLUMNS]) used_cells = [[None] * columns for _ in range(rows)] @@ -299,7 +303,10 @@ class GridLayout(Layout): f"exceeds grid size {rows}x{columns}", [CONF_WIDGETS, index], ) - if used_cells[row + i][column + j] is not None: + if ( + not allow_multiple + and used_cells[row + i][column + j] is not None + ): raise cv.Invalid( f"Cell span {row + i}/{column + j} already occupied by widget at index {used_cells[row + i][column + j]}", [CONF_WIDGETS, index], diff --git a/tests/components/lvgl/lvgl-package.yaml b/tests/components/lvgl/lvgl-package.yaml index d54aef8b4a..708dfa2cb1 100644 --- a/tests/components/lvgl/lvgl-package.yaml +++ b/tests/components/lvgl/lvgl-package.yaml @@ -893,6 +893,7 @@ lvgl: grid_columns: [40, fr(1), fr(1)] pad_row: 6px pad_column: 0 + multiple_widgets_per_cell: true widgets: - image: grid_cell_row_pos: 0 @@ -917,6 +918,10 @@ lvgl: grid_cell_row_pos: 1 grid_cell_column_pos: 0 text: "Grid cell 1/0" + - label: + grid_cell_row_pos: 1 + grid_cell_column_pos: 0 + text: "Duplicate for 1/0" - label: styles: bdr_style grid_cell_row_pos: 1 From 70df4ecaa93d7c6bacfada4980df9034dfecc06d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 25 Nov 2025 13:35:40 -0600 Subject: [PATCH 297/415] Bump actions/setup-python from 6.0.0 to 6.1.0 (#12106) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/ci-api-proto.yml | 2 +- .github/workflows/ci-clang-tidy-hash.yml | 2 +- .github/workflows/ci-docker.yml | 2 +- .github/workflows/ci.yml | 4 ++-- .github/workflows/release.yml | 4 ++-- .github/workflows/sync-device-classes.yml | 2 +- 6 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/ci-api-proto.yml b/.github/workflows/ci-api-proto.yml index b377ca76d8..2bee5ed211 100644 --- a/.github/workflows/ci-api-proto.yml +++ b/.github/workflows/ci-api-proto.yml @@ -23,7 +23,7 @@ jobs: - name: Checkout uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 - name: Set up Python - uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0 + uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0 with: python-version: "3.11" diff --git a/.github/workflows/ci-clang-tidy-hash.yml b/.github/workflows/ci-clang-tidy-hash.yml index 9556b99015..1826ed27cf 100644 --- a/.github/workflows/ci-clang-tidy-hash.yml +++ b/.github/workflows/ci-clang-tidy-hash.yml @@ -24,7 +24,7 @@ jobs: uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 - name: Set up Python - uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0 + uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0 with: python-version: "3.11" diff --git a/.github/workflows/ci-docker.yml b/.github/workflows/ci-docker.yml index 5287d92b10..c76d9cf2a5 100644 --- a/.github/workflows/ci-docker.yml +++ b/.github/workflows/ci-docker.yml @@ -45,7 +45,7 @@ jobs: steps: - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 - name: Set up Python - uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0 + uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0 with: python-version: "3.11" - name: Set up Docker Buildx diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9c2fab0912..9cfc02d5cf 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -42,7 +42,7 @@ jobs: run: echo key="${{ hashFiles('requirements.txt', 'requirements_test.txt', '.pre-commit-config.yaml') }}" >> $GITHUB_OUTPUT - name: Set up Python ${{ env.DEFAULT_PYTHON }} id: python - uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0 + uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0 with: python-version: ${{ env.DEFAULT_PYTHON }} - name: Restore Python virtual environment @@ -240,7 +240,7 @@ jobs: uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 - name: Set up Python 3.13 id: python - uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0 + uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0 with: python-version: "3.13" - name: Restore Python virtual environment diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 497ecd29e7..1ff810d869 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -62,7 +62,7 @@ jobs: steps: - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 - name: Set up Python - uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0 + uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0 with: python-version: "3.x" - name: Build @@ -94,7 +94,7 @@ jobs: steps: - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 - name: Set up Python - uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0 + uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0 with: python-version: "3.11" diff --git a/.github/workflows/sync-device-classes.yml b/.github/workflows/sync-device-classes.yml index 8f95fa68ee..baaa29df2c 100644 --- a/.github/workflows/sync-device-classes.yml +++ b/.github/workflows/sync-device-classes.yml @@ -22,7 +22,7 @@ jobs: path: lib/home-assistant - name: Setup Python - uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0 + uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0 with: python-version: 3.13 From ae60b5e6a133b4df3266831078fc1df3976fa314 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 25 Nov 2025 14:27:49 -0600 Subject: [PATCH 298/415] Bump actions/setup-python from 6.0.0 to 6.1.0 in /.github/actions/restore-python (#12108) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/actions/restore-python/action.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/actions/restore-python/action.yml b/.github/actions/restore-python/action.yml index f314e79ad9..c4ac3d1a9e 100644 --- a/.github/actions/restore-python/action.yml +++ b/.github/actions/restore-python/action.yml @@ -17,7 +17,7 @@ runs: steps: - name: Set up Python ${{ inputs.python-version }} id: python - uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0 + uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0 with: python-version: ${{ inputs.python-version }} - name: Restore Python virtual environment From 50bdcdee0c851810940652913d12dfaa38330e6e Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 26 Nov 2025 12:39:41 +1300 Subject: [PATCH 299/415] Add developer-breaking-change labelling (#12095) --- .github/PULL_REQUEST_TEMPLATE.md | 1 + .github/workflows/auto-label-pr.yml | 2 ++ 2 files changed, 3 insertions(+) diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 28437e6302..41dd02458e 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -7,6 +7,7 @@ - [ ] Bugfix (non-breaking change which fixes an issue) - [ ] New feature (non-breaking change which adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) +- [ ] Developer breaking change (an API change that could break external components) - [ ] Code quality improvements to existing code or addition of tests - [ ] Other diff --git a/.github/workflows/auto-label-pr.yml b/.github/workflows/auto-label-pr.yml index 998f3315c6..d09072d814 100644 --- a/.github/workflows/auto-label-pr.yml +++ b/.github/workflows/auto-label-pr.yml @@ -68,6 +68,7 @@ jobs: 'bugfix', 'new-feature', 'breaking-change', + 'developer-breaking-change', 'code-quality' ]; @@ -367,6 +368,7 @@ jobs: { pattern: /- \[x\] Bugfix \(non-breaking change which fixes an issue\)/i, label: 'bugfix' }, { pattern: /- \[x\] New feature \(non-breaking change which adds functionality\)/i, label: 'new-feature' }, { pattern: /- \[x\] Breaking change \(fix or feature that would cause existing functionality to not work as expected\)/i, label: 'breaking-change' }, + { pattern: /- \[x\] Developer breaking change \(an API change that could break external components\)/i, label: 'developer-breaking-change' }, { pattern: /- \[x\] Code quality improvements to existing code or addition of tests/i, label: 'code-quality' } ]; From ffae3501ab00c1fc06dd7d395dee64b95154fe0c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 25 Nov 2025 17:44:50 -0600 Subject: [PATCH 300/415] [core] Replace seq<>/gens<> with std::index_sequence for code clarity (#11921) --- esphome/components/api/user_services.h | 10 ++++--- esphome/components/script/script.h | 12 ++++----- esphome/core/automation.h | 36 +++++++++++++++++++------- 3 files changed, 39 insertions(+), 19 deletions(-) diff --git a/esphome/components/api/user_services.h b/esphome/components/api/user_services.h index 501b702e6b..d9c13c520b 100644 --- a/esphome/components/api/user_services.h +++ b/esphome/components/api/user_services.h @@ -51,13 +51,14 @@ template class UserServiceBase : public UserServiceDescriptor { return false; if (req.args.size() != sizeof...(Ts)) return false; - this->execute_(req.args, typename gens::type()); + this->execute_(req.args, std::make_index_sequence{}); return true; } protected: virtual void execute(Ts... x) = 0; - template void execute_(const ArgsContainer &args, seq type) { + template + void execute_(const ArgsContainer &args, std::index_sequence type) { this->execute((get_execute_arg_value(args[S]))...); } @@ -95,13 +96,14 @@ template class UserServiceDynamic : public UserServiceDescriptor return false; if (req.args.size() != sizeof...(Ts)) return false; - this->execute_(req.args, typename gens::type()); + this->execute_(req.args, std::make_index_sequence{}); return true; } protected: virtual void execute(Ts... x) = 0; - template void execute_(const ArgsContainer &args, seq type) { + template + void execute_(const ArgsContainer &args, std::index_sequence type) { this->execute((get_execute_arg_value(args[S]))...); } diff --git a/esphome/components/script/script.h b/esphome/components/script/script.h index 3a0823f3cc..cd1a084f16 100644 --- a/esphome/components/script/script.h +++ b/esphome/components/script/script.h @@ -46,14 +46,14 @@ template class Script : public ScriptLogger, public Trigger &tuple) { - this->execute_tuple_(tuple, typename gens::type()); + this->execute_tuple_(tuple, std::make_index_sequence{}); } // Internal function to give scripts readable names. void set_name(const LogString *name) { name_ = name; } protected: - template void execute_tuple_(const std::tuple &tuple, seq /*unused*/) { + template void execute_tuple_(const std::tuple &tuple, std::index_sequence /*unused*/) { this->execute(std::get(tuple)...); } @@ -157,7 +157,7 @@ template class QueueingScript : public Script, public Com const size_t queue_capacity = static_cast(this->max_runs_ - 1); auto tuple_ptr = std::move(this->var_queue_[this->queue_front_]); this->queue_front_ = (this->queue_front_ + 1) % queue_capacity; - this->trigger_tuple_(*tuple_ptr, typename gens::type()); + this->trigger_tuple_(*tuple_ptr, std::make_index_sequence{}); } } @@ -174,7 +174,7 @@ template class QueueingScript : public Script, public Com } } - template void trigger_tuple_(const std::tuple &tuple, seq /*unused*/) { + template void trigger_tuple_(const std::tuple &tuple, std::index_sequence /*unused*/) { this->trigger(std::get(tuple)...); } @@ -313,7 +313,7 @@ template class ScriptWaitAction : public Action, // play_next_() can trigger more items to be queued if (!this->param_queue_.empty()) { auto ¶ms = this->param_queue_.front(); - this->play_next_tuple_(params, typename gens::type()); + this->play_next_tuple_(params, std::make_index_sequence{}); this->param_queue_.pop_front(); } else { // Queue is now empty - disable loop until next play_complex @@ -330,7 +330,7 @@ template class ScriptWaitAction : public Action, } protected: - template void play_next_tuple_(const std::tuple &tuple, seq /*unused*/) { + template void play_next_tuple_(const std::tuple &tuple, std::index_sequence /*unused*/) { this->play_next_(std::get(tuple)...); } diff --git a/esphome/core/automation.h b/esphome/core/automation.h index 33e08c9c1c..dacadd35e8 100644 --- a/esphome/core/automation.h +++ b/esphome/core/automation.h @@ -11,10 +11,26 @@ namespace esphome { +// C++20 std::index_sequence is now used for tuple unpacking +// Legacy seq<>/gens<> pattern deprecated but kept for backwards compatibility // https://stackoverflow.com/questions/7858817/unpacking-a-tuple-to-call-a-matching-function-pointer/7858971#7858971 -template struct seq {}; // NOLINT -template struct gens : gens {}; // NOLINT -template struct gens<0, S...> { using type = seq; }; // NOLINT +// Remove before 2026.6.0 +// NOLINTBEGIN(readability-identifier-naming) +#if defined(__GNUC__) || defined(__clang__) +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wdeprecated-declarations" +#endif + +template struct ESPDEPRECATED("Use std::index_sequence instead. Removed in 2026.6.0", "2025.12.0") seq {}; +template +struct ESPDEPRECATED("Use std::make_index_sequence instead. Removed in 2026.6.0", "2025.12.0") gens + : gens {}; +template struct gens<0, S...> { using type = seq; }; + +#if defined(__GNUC__) || defined(__clang__) +#pragma GCC diagnostic pop +#endif +// NOLINTEND(readability-identifier-naming) #define TEMPLATABLE_VALUE_(type, name) \ protected: \ @@ -152,11 +168,11 @@ template class Condition { /// Call check with a tuple of values as parameter. bool check_tuple(const std::tuple &tuple) { - return this->check_tuple_(tuple, typename gens::type()); + return this->check_tuple_(tuple, std::make_index_sequence{}); } protected: - template bool check_tuple_(const std::tuple &tuple, seq /*unused*/) { + template bool check_tuple_(const std::tuple &tuple, std::index_sequence /*unused*/) { return this->check(std::get(tuple)...); } }; @@ -231,11 +247,11 @@ template class Action { } } } - template void play_next_tuple_(const std::tuple &tuple, seq /*unused*/) { + template void play_next_tuple_(const std::tuple &tuple, std::index_sequence /*unused*/) { this->play_next_(std::get(tuple)...); } void play_next_tuple_(const std::tuple &tuple) { - this->play_next_tuple_(tuple, typename gens::type()); + this->play_next_tuple_(tuple, std::make_index_sequence{}); } virtual void stop() {} @@ -277,7 +293,9 @@ template class ActionList { if (this->actions_begin_ != nullptr) this->actions_begin_->play_complex(x...); } - void play_tuple(const std::tuple &tuple) { this->play_tuple_(tuple, typename gens::type()); } + void play_tuple(const std::tuple &tuple) { + this->play_tuple_(tuple, std::make_index_sequence{}); + } void stop() { if (this->actions_begin_ != nullptr) this->actions_begin_->stop_complex(); @@ -298,7 +316,7 @@ template class ActionList { } protected: - template void play_tuple_(const std::tuple &tuple, seq /*unused*/) { + template void play_tuple_(const std::tuple &tuple, std::index_sequence /*unused*/) { this->play(std::get(tuple)...); } From bda17180df0c9ca735d25dc52def20b83b2465f6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 25 Nov 2025 17:48:08 -0600 Subject: [PATCH 301/415] [core] Deduplicate identical stateless lambdas to reduce flash usage (#11918) --- esphome/cpp_generator.py | 177 ++++++++++++++- tests/component_tests/text/test_text.py | 19 +- tests/unit_tests/test_lambda_dedup.py | 286 ++++++++++++++++++++++++ 3 files changed, 471 insertions(+), 11 deletions(-) create mode 100644 tests/unit_tests/test_lambda_dedup.py diff --git a/esphome/cpp_generator.py b/esphome/cpp_generator.py index 6f1af01a5b..4f91696ca1 100644 --- a/esphome/cpp_generator.py +++ b/esphome/cpp_generator.py @@ -19,11 +19,21 @@ from esphome.core import ( TimePeriodNanoseconds, TimePeriodSeconds, ) +from esphome.coroutine import CoroPriority, coroutine_with_priority from esphome.helpers import cpp_string_escape, indent_all_but_first_and_last from esphome.types import Expression, SafeExpType, TemplateArgsType from esphome.util import OrderedDict from esphome.yaml_util import ESPHomeDataBase +# Keys for lambda deduplication storage in CORE.data +_KEY_LAMBDA_DEDUP = "lambda_dedup" +_KEY_LAMBDA_DEDUP_DECLARATIONS = "lambda_dedup_declarations" + +# Regex patterns for static variable detection (compiled once) +_RE_CPP_SINGLE_LINE_COMMENT = re.compile(r"//.*?$", re.MULTILINE) +_RE_CPP_MULTI_LINE_COMMENT = re.compile(r"/\*.*?\*/", re.DOTALL) +_RE_STATIC_VARIABLE = re.compile(r"\bstatic\s+(?!cast|assert|pointer_cast)\w+\s+\w+") + class RawExpression(Expression): __slots__ = ("text",) @@ -188,7 +198,7 @@ class LambdaExpression(Expression): def __init__( self, parts, parameters, capture: str = "=", return_type=None, source=None - ): + ) -> None: self.parts = parts if not isinstance(parameters, ParameterListExpression): parameters = ParameterListExpression(*parameters) @@ -197,16 +207,21 @@ class LambdaExpression(Expression): self.capture = capture self.return_type = safe_exp(return_type) if return_type is not None else None - def __str__(self): + def format_body(self) -> str: + """Format the lambda body with source directive and content.""" + body = "" + if self.source is not None: + body += f"{self.source.as_line_directive}\n" + body += self.content + return body + + def __str__(self) -> str: # Stateless lambdas (empty capture) implicitly convert to function pointers # when assigned to function pointer types - no unary + needed cpp = f"[{self.capture}]({self.parameters})" if self.return_type is not None: cpp += f" -> {self.return_type}" - cpp += " {\n" - if self.source is not None: - cpp += f"{self.source.as_line_directive}\n" - cpp += f"{self.content}\n}}" + cpp += f" {{\n{self.format_body()}\n}}" return indent_all_but_first_and_last(cpp) @property @@ -214,6 +229,37 @@ class LambdaExpression(Expression): return "".join(str(part) for part in self.parts) +class SharedFunctionLambdaExpression(LambdaExpression): + """A lambda expression that references a shared deduplicated function. + + This class wraps a function pointer but maintains the LambdaExpression + interface so calling code works unchanged. + """ + + __slots__ = ("_func_name",) + + def __init__( + self, + func_name: str, + parameters: TemplateArgsType, + return_type: SafeExpType | None = None, + ) -> None: + # Initialize parent with empty parts since we're just a function reference + super().__init__( + [], parameters, capture="", return_type=return_type, source=None + ) + self._func_name = func_name + + def __str__(self) -> str: + # Just return the function name - it's already a function pointer + return self._func_name + + @property + def content(self) -> str: + # No content, just a function reference + return "" + + # pylint: disable=abstract-method class Literal(Expression, metaclass=abc.ABCMeta): __slots__ = () @@ -583,6 +629,25 @@ def add_global(expression: SafeExpType | Statement, prepend: bool = False): CORE.add_global(expression, prepend) +@coroutine_with_priority(CoroPriority.FINAL) +async def flush_lambda_dedup_declarations() -> None: + """Flush all deferred lambda deduplication declarations to global scope. + + This is a coroutine that runs with FINAL priority (after all components) + to ensure all referenced variables are declared before the shared + lambda functions that use them. + """ + if _KEY_LAMBDA_DEDUP_DECLARATIONS not in CORE.data: + return + + declarations = CORE.data[_KEY_LAMBDA_DEDUP_DECLARATIONS] + for func_declaration in declarations: + add_global(RawStatement(func_declaration)) + + # Clear the list so we don't add them again + CORE.data[_KEY_LAMBDA_DEDUP_DECLARATIONS] = [] + + def add_library(name: str, version: str | None, repository: str | None = None): """Add a library to the codegen library storage. @@ -656,6 +721,93 @@ async def get_variable_with_full_id(id_: ID) -> tuple[ID, "MockObj"]: return await CORE.get_variable_with_full_id(id_) +def _has_static_variables(code: str) -> bool: + """Check if code contains static variable definitions. + + Static variables in lambdas should not be deduplicated because each lambda + instance should have its own static variable state. + + Args: + code: The lambda body code to check + + Returns: + True if code contains static variable definitions + """ + # Remove C++ comments to avoid false positives + # Remove single-line comments (// ...) + code_no_comments = _RE_CPP_SINGLE_LINE_COMMENT.sub("", code) + # Remove multi-line comments (/* ... */) + code_no_comments = _RE_CPP_MULTI_LINE_COMMENT.sub("", code_no_comments) + + # Match: static + # But not: static_cast, static_assert, static_pointer_cast + return bool(_RE_STATIC_VARIABLE.search(code_no_comments)) + + +def _get_shared_lambda_name(lambda_expr: LambdaExpression) -> str | None: + """Get the shared function name for a lambda expression. + + If an identical lambda was already generated, returns the existing shared + function name. Otherwise, creates a new shared function and returns its name. + + Lambdas with static variables are not deduplicated to preserve their + independent state. + + Args: + lambda_expr: The lambda expression to deduplicate + + Returns: + The name of the shared function for this lambda (either existing or newly created), + or None if the lambda should not be deduplicated (e.g., contains static variables) + """ + # Create a unique key from the lambda content, parameters, and return type + content = lambda_expr.content + + # Don't deduplicate lambdas with static variables - each instance needs its own state + if _has_static_variables(content): + return None + param_str = str(lambda_expr.parameters) + return_str = ( + str(lambda_expr.return_type) if lambda_expr.return_type is not None else "void" + ) + + # Use tuple of (content, params, return_type) as key + lambda_key = (content, param_str, return_str) + + # Initialize deduplication storage in CORE.data if not exists + if _KEY_LAMBDA_DEDUP not in CORE.data: + CORE.data[_KEY_LAMBDA_DEDUP] = {} + # Register the flush job to run after all components (FINAL priority) + # This ensures all variables are declared before shared lambda functions + CORE.add_job(flush_lambda_dedup_declarations) + + lambda_cache = CORE.data[_KEY_LAMBDA_DEDUP] + + # Check if we've seen this lambda before + if lambda_key in lambda_cache: + # Return name of existing shared function + return lambda_cache[lambda_key] + + # First occurrence - create a shared function + # Use the cache size as the function number + func_name = f"shared_lambda_{len(lambda_cache)}" + + # Build the function declaration using lambda's body formatting + func_declaration = ( + f"{return_str} {func_name}({param_str}) {{\n{lambda_expr.format_body()}\n}}" + ) + + # Store the declaration to be added later (after all variable declarations) + # We can't add it immediately because it might reference variables not yet declared + CORE.data.setdefault(_KEY_LAMBDA_DEDUP_DECLARATIONS, []).append(func_declaration) + + # Store in cache + lambda_cache[lambda_key] = func_name + + # Return the function name (this is the first occurrence, but we still generate shared function) + return func_name + + async def process_lambda( value: Lambda, parameters: TemplateArgsType, @@ -713,6 +865,19 @@ async def process_lambda( location.line += value.content_offset else: location = None + + # Lambda deduplication: Only deduplicate stateless lambdas (empty capture). + # Stateful lambdas cannot be shared as they capture different contexts. + # Lambdas with static variables are also not deduplicated to preserve independent state. + if capture == "": + lambda_expr = LambdaExpression( + parts, parameters, capture, return_type, location + ) + func_name = _get_shared_lambda_name(lambda_expr) + if func_name is not None: + # Return a shared function reference instead of inline lambda + return SharedFunctionLambdaExpression(func_name, parameters, return_type) + return LambdaExpression(parts, parameters, capture, return_type, location) diff --git a/tests/component_tests/text/test_text.py b/tests/component_tests/text/test_text.py index bfc3131f6d..56dee205b4 100644 --- a/tests/component_tests/text/test_text.py +++ b/tests/component_tests/text/test_text.py @@ -1,4 +1,6 @@ -"""Tests for the binary sensor component.""" +"""Tests for the text component.""" + +from esphome.core import CORE def test_text_is_setup(generate_main): @@ -56,15 +58,22 @@ def test_text_config_value_mode_set(generate_main): assert "it_3->traits.set_mode(text::TEXT_MODE_PASSWORD);" in main_cpp -def test_text_config_lamda_is_set(generate_main): +def test_text_config_lambda_is_set(generate_main) -> None: """ - Test if lambda is set for lambda mode (optimized with stateless lambda) + Test if lambda is set for lambda mode (optimized with stateless lambda and deduplication) """ # Given # When main_cpp = generate_main("tests/component_tests/text/test_text.yaml") + # Get both global and main sections to find the shared lambda definition + full_cpp = CORE.cpp_global_section + main_cpp + # Then - assert "it_4->set_template([]() -> esphome::optional {" in main_cpp - assert 'return std::string{"Hello"};' in main_cpp + # Lambda is deduplicated into a shared function (reference in main section) + assert "it_4->set_template(shared_lambda_" in main_cpp + # Lambda body should be in the code somewhere + assert 'return std::string{"Hello"};' in full_cpp + # Verify the shared lambda function is defined (in global section) + assert "esphome::optional shared_lambda_" in full_cpp diff --git a/tests/unit_tests/test_lambda_dedup.py b/tests/unit_tests/test_lambda_dedup.py new file mode 100644 index 0000000000..bbf5f02e6d --- /dev/null +++ b/tests/unit_tests/test_lambda_dedup.py @@ -0,0 +1,286 @@ +"""Tests for lambda deduplication in cpp_generator.""" + +from esphome import cpp_generator as cg +from esphome.core import CORE + + +def test_deduplicate_identical_lambdas() -> None: + """Test that identical stateless lambdas are deduplicated.""" + # Create two identical lambda expressions + lambda1 = cg.LambdaExpression( + parts=["return 42;"], + parameters=[], + capture="", + return_type=cg.RawExpression("int"), + ) + + lambda2 = cg.LambdaExpression( + parts=["return 42;"], + parameters=[], + capture="", + return_type=cg.RawExpression("int"), + ) + + # Try to deduplicate them + func_name1 = cg._get_shared_lambda_name(lambda1) + func_name2 = cg._get_shared_lambda_name(lambda2) + + # Both should get the same function name (deduplication happened) + assert func_name1 == func_name2 + assert func_name1 == "shared_lambda_0" + + +def test_different_lambdas_not_deduplicated() -> None: + """Test that different lambdas get different function names.""" + lambda1 = cg.LambdaExpression( + parts=["return 42;"], + parameters=[], + capture="", + return_type=cg.RawExpression("int"), + ) + + lambda2 = cg.LambdaExpression( + parts=["return 24;"], # Different content + parameters=[], + capture="", + return_type=cg.RawExpression("int"), + ) + + func_name1 = cg._get_shared_lambda_name(lambda1) + func_name2 = cg._get_shared_lambda_name(lambda2) + + # Different lambdas should get different function names + assert func_name1 != func_name2 + assert func_name1 == "shared_lambda_0" + assert func_name2 == "shared_lambda_1" + + +def test_different_return_types_not_deduplicated() -> None: + """Test that lambdas with different return types are not deduplicated.""" + lambda1 = cg.LambdaExpression( + parts=["return 42;"], + parameters=[], + capture="", + return_type=cg.RawExpression("int"), + ) + + lambda2 = cg.LambdaExpression( + parts=["return 42;"], # Same content + parameters=[], + capture="", + return_type=cg.RawExpression("float"), # Different return type + ) + + func_name1 = cg._get_shared_lambda_name(lambda1) + func_name2 = cg._get_shared_lambda_name(lambda2) + + # Different return types = different functions + assert func_name1 != func_name2 + + +def test_different_parameters_not_deduplicated() -> None: + """Test that lambdas with different parameters are not deduplicated.""" + lambda1 = cg.LambdaExpression( + parts=["return x;"], + parameters=[("int", "x")], + capture="", + return_type=cg.RawExpression("int"), + ) + + lambda2 = cg.LambdaExpression( + parts=["return x;"], # Same content + parameters=[("float", "x")], # Different parameter type + capture="", + return_type=cg.RawExpression("int"), + ) + + func_name1 = cg._get_shared_lambda_name(lambda1) + func_name2 = cg._get_shared_lambda_name(lambda2) + + # Different parameters = different functions + assert func_name1 != func_name2 + + +def test_flush_lambda_dedup_declarations() -> None: + """Test that deferred declarations are properly stored for later flushing.""" + # Create a lambda which will create a deferred declaration + lambda1 = cg.LambdaExpression( + parts=["return 42;"], + parameters=[], + capture="", + return_type=cg.RawExpression("int"), + ) + + cg._get_shared_lambda_name(lambda1) + + # Check that declaration was stored + assert cg._KEY_LAMBDA_DEDUP_DECLARATIONS in CORE.data + assert len(CORE.data[cg._KEY_LAMBDA_DEDUP_DECLARATIONS]) == 1 + + # Verify the declaration content is correct + declaration = CORE.data[cg._KEY_LAMBDA_DEDUP_DECLARATIONS][0] + assert "shared_lambda_0" in declaration + assert "return 42;" in declaration + + # Note: The actual flushing happens via CORE.add_job with FINAL priority + # during real code generation, so we don't test that here + + +def test_shared_function_lambda_expression() -> None: + """Test SharedFunctionLambdaExpression behaves correctly.""" + shared_lambda = cg.SharedFunctionLambdaExpression( + func_name="shared_lambda_0", + parameters=[], + return_type=cg.RawExpression("int"), + ) + + # Should output just the function name + assert str(shared_lambda) == "shared_lambda_0" + + # Should have empty capture (stateless) + assert shared_lambda.capture == "" + + # Should have empty content (just a reference) + assert shared_lambda.content == "" + + +def test_lambda_deduplication_counter() -> None: + """Test that lambda counter increments correctly.""" + # Create 3 different lambdas + for i in range(3): + lambda_expr = cg.LambdaExpression( + parts=[f"return {i};"], + parameters=[], + capture="", + return_type=cg.RawExpression("int"), + ) + func_name = cg._get_shared_lambda_name(lambda_expr) + assert func_name == f"shared_lambda_{i}" + + +def test_lambda_format_body() -> None: + """Test that format_body correctly formats lambda body with source.""" + # Without source + lambda1 = cg.LambdaExpression( + parts=["return 42;"], + parameters=[], + capture="", + return_type=None, + source=None, + ) + assert lambda1.format_body() == "return 42;" + + # With source would need a proper source object, skip for now + + +def test_stateful_lambdas_not_deduplicated() -> None: + """Test that stateful lambdas (non-empty capture) are not deduplicated.""" + # _get_shared_lambda_name is only called for stateless lambdas (capture == "") + # Stateful lambdas bypass deduplication entirely in process_lambda + + # Verify that a stateful lambda would NOT get deduplicated + # by checking it's not in the stateless dedup cache + stateful_lambda = cg.LambdaExpression( + parts=["return x + y;"], + parameters=[], + capture="=", # Non-empty capture means stateful + return_type=cg.RawExpression("int"), + ) + + # Stateful lambdas should NOT be passed to _get_shared_lambda_name + # This is enforced by the `if capture == ""` check in process_lambda + # We verify the lambda has a non-empty capture + assert stateful_lambda.capture != "" + assert stateful_lambda.capture == "=" + + +def test_static_variable_detection() -> None: + """Test detection of static variables in lambda code.""" + # Should detect static variables + assert cg._has_static_variables("static int counter = 0;") + assert cg._has_static_variables("static bool flag = false; return flag;") + assert cg._has_static_variables(" static float value = 1.0; ") + + # Should NOT detect static_cast, static_assert, etc. (with underscores) + assert not cg._has_static_variables("return static_cast(value);") + assert not cg._has_static_variables("static_assert(sizeof(int) == 4);") + assert not cg._has_static_variables("auto ptr = static_pointer_cast(bar);") + + # Edge case: 'cast', 'assert', 'pointer_cast' are NOT C++ keywords + # Someone could use them as type names, but we should NOT flag them + # because they're not actually static variables with state + # NOTE: These are valid C++ but extremely unlikely in ESPHome lambdas + assert not cg._has_static_variables("static cast obj;") # 'cast' as type name + assert not cg._has_static_variables("static assert value;") # 'assert' as type name + assert not cg._has_static_variables( + "static pointer_cast ptr;" + ) # 'pointer_cast' as type + + # Should NOT detect in comments + assert not cg._has_static_variables("// static int x = 0;\nreturn 42;") + assert not cg._has_static_variables("/* static int y = 0; */ return 42;") + + # Should detect even with comments elsewhere + assert cg._has_static_variables("// comment\nstatic int x = 0;\nreturn x;") + + # Should NOT detect non-static code + assert not cg._has_static_variables("int counter = 0; return counter++;") + assert not cg._has_static_variables("return 42;") + + # Should handle newlines between static and type/variable + assert cg._has_static_variables("static int\nfoo = 0;") + assert cg._has_static_variables("static\nint\nbar = 0;") + assert cg._has_static_variables( + "static int \n foo = 0;" + ) # Mixed spaces/newlines + + +def test_lambdas_with_static_not_deduplicated() -> None: + """Test that lambdas with static variables are not deduplicated.""" + # Two identical lambdas with static variables + lambda1 = cg.LambdaExpression( + parts=["static int counter = 0; return counter++;"], + parameters=[], + capture="", + return_type=cg.RawExpression("int"), + ) + + lambda2 = cg.LambdaExpression( + parts=["static int counter = 0; return counter++;"], + parameters=[], + capture="", + return_type=cg.RawExpression("int"), + ) + + # Should return None (not deduplicated) + func_name1 = cg._get_shared_lambda_name(lambda1) + func_name2 = cg._get_shared_lambda_name(lambda2) + + assert func_name1 is None + assert func_name2 is None + + +def test_lambdas_without_static_still_deduplicated() -> None: + """Test that lambdas without static variables are still deduplicated.""" + # Two identical lambdas WITHOUT static variables + lambda1 = cg.LambdaExpression( + parts=["int counter = 0; return counter++;"], # No static + parameters=[], + capture="", + return_type=cg.RawExpression("int"), + ) + + lambda2 = cg.LambdaExpression( + parts=["int counter = 0; return counter++;"], # No static + parameters=[], + capture="", + return_type=cg.RawExpression("int"), + ) + + # Should be deduplicated (same function name) + func_name1 = cg._get_shared_lambda_name(lambda1) + func_name2 = cg._get_shared_lambda_name(lambda2) + + assert func_name1 is not None + assert func_name2 is not None + assert func_name1 == func_name2 From 03a8ef71ff4224ad57315402bfedc897906d6038 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 25 Nov 2025 18:37:49 -0600 Subject: [PATCH 302/415] [esp32_ble_client] Replace std::string with char[18] for BLE address storage (#12070) --- esphome/components/alpha3/alpha3.cpp | 18 ++--- .../components/am43/sensor/am43_sensor.cpp | 13 ++-- esphome/components/anova/anova.cpp | 9 +-- esphome/components/ble_client/automation.h | 2 +- esphome/components/ble_client/ble_client.cpp | 2 +- .../ble_client/output/ble_binary_output.cpp | 4 +- .../ble_client/sensor/ble_rssi_sensor.cpp | 6 +- .../ble_client/sensor/ble_sensor.cpp | 2 +- .../text_sensor/ble_text_sensor.cpp | 2 +- .../bluetooth_proxy/bluetooth_connection.cpp | 41 +++++----- .../bluetooth_proxy/bluetooth_proxy.cpp | 11 ++- .../esp32_ble_client/ble_characteristic.cpp | 6 +- .../esp32_ble_client/ble_client_base.cpp | 75 +++++++++---------- .../esp32_ble_client/ble_client_base.h | 16 ++-- .../esp32_ble_client/ble_service.cpp | 4 +- .../display/pvvx_display.cpp | 36 +++++---- 16 files changed, 115 insertions(+), 132 deletions(-) diff --git a/esphome/components/alpha3/alpha3.cpp b/esphome/components/alpha3/alpha3.cpp index 344f2d5a03..f22a8e2444 100644 --- a/esphome/components/alpha3/alpha3.cpp +++ b/esphome/components/alpha3/alpha3.cpp @@ -56,13 +56,13 @@ bool Alpha3::is_current_response_type_(const uint8_t *response_type) { void Alpha3::handle_geni_response_(const uint8_t *response, uint16_t length) { if (this->response_offset_ >= this->response_length_) { - ESP_LOGD(TAG, "[%s] GENI response begin", this->parent_->address_str().c_str()); + ESP_LOGD(TAG, "[%s] GENI response begin", this->parent_->address_str()); if (length < GENI_RESPONSE_HEADER_LENGTH) { - ESP_LOGW(TAG, "[%s] response to short", this->parent_->address_str().c_str()); + ESP_LOGW(TAG, "[%s] response too short", this->parent_->address_str()); return; } if (response[0] != 36 || response[2] != 248 || response[3] != 231 || response[4] != 10) { - ESP_LOGW(TAG, "[%s] response bytes %d %d %d %d %d don't match GENI HEADER", this->parent_->address_str().c_str(), + ESP_LOGW(TAG, "[%s] response bytes %d %d %d %d %d don't match GENI HEADER", this->parent_->address_str(), response[0], response[1], response[2], response[3], response[4]); return; } @@ -77,11 +77,11 @@ void Alpha3::handle_geni_response_(const uint8_t *response, uint16_t length) { }; if (this->is_current_response_type_(GENI_RESPONSE_TYPE_FLOW_HEAD)) { - ESP_LOGD(TAG, "[%s] FLOW HEAD Response", this->parent_->address_str().c_str()); + ESP_LOGD(TAG, "[%s] FLOW HEAD Response", this->parent_->address_str()); extract_publish_sensor_value(GENI_RESPONSE_FLOW_OFFSET, this->flow_sensor_, 3600.0F); extract_publish_sensor_value(GENI_RESPONSE_HEAD_OFFSET, this->head_sensor_, .0001F); } else if (this->is_current_response_type_(GENI_RESPONSE_TYPE_POWER)) { - ESP_LOGD(TAG, "[%s] POWER Response", this->parent_->address_str().c_str()); + ESP_LOGD(TAG, "[%s] POWER Response", this->parent_->address_str()); extract_publish_sensor_value(GENI_RESPONSE_POWER_OFFSET, this->power_sensor_, 1.0F); extract_publish_sensor_value(GENI_RESPONSE_CURRENT_OFFSET, this->current_sensor_, 1.0F); extract_publish_sensor_value(GENI_RESPONSE_MOTOR_SPEED_OFFSET, this->speed_sensor_, 1.0F); @@ -100,7 +100,7 @@ void Alpha3::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc if (param->open.status == ESP_GATT_OK) { this->response_offset_ = 0; this->response_length_ = 0; - ESP_LOGI(TAG, "[%s] connection open", this->parent_->address_str().c_str()); + ESP_LOGI(TAG, "[%s] connection open", this->parent_->address_str()); } break; } @@ -132,7 +132,7 @@ void Alpha3::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc case ESP_GATTC_SEARCH_CMPL_EVT: { auto *chr = this->parent_->get_characteristic(ALPHA3_GENI_SERVICE_UUID, ALPHA3_GENI_CHARACTERISTIC_UUID); if (chr == nullptr) { - ESP_LOGE(TAG, "[%s] No GENI service found at device, not an Alpha3..?", this->parent_->address_str().c_str()); + ESP_LOGE(TAG, "[%s] No GENI service found at device, not an Alpha3..?", this->parent_->address_str()); break; } auto status = esp_ble_gattc_register_for_notify(this->parent_->get_gattc_if(), this->parent_->get_remote_bda(), @@ -164,12 +164,12 @@ void Alpha3::send_request_(uint8_t *request, size_t len) { esp_ble_gattc_write_char(this->parent_->get_gattc_if(), this->parent_->get_conn_id(), this->geni_handle_, len, request, ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE); if (status) - ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str().c_str(), status); + ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str(), status); } void Alpha3::update() { if (this->node_state != espbt::ClientState::ESTABLISHED) { - ESP_LOGW(TAG, "[%s] Cannot poll, not connected", this->parent_->address_str().c_str()); + ESP_LOGW(TAG, "[%s] Cannot poll, not connected", this->parent_->address_str()); return; } diff --git a/esphome/components/am43/sensor/am43_sensor.cpp b/esphome/components/am43/sensor/am43_sensor.cpp index 4cc99001ae..b2bc3254e2 100644 --- a/esphome/components/am43/sensor/am43_sensor.cpp +++ b/esphome/components/am43/sensor/am43_sensor.cpp @@ -44,11 +44,9 @@ void Am43::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_i auto *chr = this->parent_->get_characteristic(AM43_SERVICE_UUID, AM43_CHARACTERISTIC_UUID); if (chr == nullptr) { if (this->parent_->get_characteristic(AM43_TUYA_SERVICE_UUID, AM43_TUYA_CHARACTERISTIC_UUID) != nullptr) { - ESP_LOGE(TAG, "[%s] Detected a Tuya AM43 which is not supported, sorry.", - this->parent_->address_str().c_str()); + ESP_LOGE(TAG, "[%s] Detected a Tuya AM43 which is not supported, sorry.", this->parent_->address_str()); } else { - ESP_LOGE(TAG, "[%s] No control service found at device, not an AM43..?", - this->parent_->address_str().c_str()); + ESP_LOGE(TAG, "[%s] No control service found at device, not an AM43..?", this->parent_->address_str()); } break; } @@ -82,8 +80,7 @@ void Am43::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_i this->char_handle_, packet->length, packet->data, ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE); if (status) { - ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str().c_str(), - status); + ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str(), status); } } this->current_sensor_ = 0; @@ -97,7 +94,7 @@ void Am43::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_i void Am43::update() { if (this->node_state != espbt::ClientState::ESTABLISHED) { - ESP_LOGW(TAG, "[%s] Cannot poll, not connected", this->parent_->address_str().c_str()); + ESP_LOGW(TAG, "[%s] Cannot poll, not connected", this->parent_->address_str()); return; } if (this->current_sensor_ == 0) { @@ -107,7 +104,7 @@ void Am43::update() { esp_ble_gattc_write_char(this->parent_->get_gattc_if(), this->parent_->get_conn_id(), this->char_handle_, packet->length, packet->data, ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE); if (status) { - ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str().c_str(), status); + ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str(), status); } } this->current_sensor_++; diff --git a/esphome/components/anova/anova.cpp b/esphome/components/anova/anova.cpp index d0e8f6827f..2693224a97 100644 --- a/esphome/components/anova/anova.cpp +++ b/esphome/components/anova/anova.cpp @@ -42,7 +42,7 @@ void Anova::control(const ClimateCall &call) { esp_ble_gattc_write_char(this->parent_->get_gattc_if(), this->parent_->get_conn_id(), this->char_handle_, pkt->length, pkt->data, ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE); if (status) { - ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str().c_str(), status); + ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str(), status); } } if (call.get_target_temperature().has_value()) { @@ -51,7 +51,7 @@ void Anova::control(const ClimateCall &call) { esp_ble_gattc_write_char(this->parent_->get_gattc_if(), this->parent_->get_conn_id(), this->char_handle_, pkt->length, pkt->data, ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE); if (status) { - ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str().c_str(), status); + ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str(), status); } } } @@ -124,8 +124,7 @@ void Anova::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_ esp_ble_gattc_write_char(this->parent_->get_gattc_if(), this->parent_->get_conn_id(), this->char_handle_, pkt->length, pkt->data, ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE); if (status) { - ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str().c_str(), - status); + ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str(), status); } } } @@ -150,7 +149,7 @@ void Anova::update() { esp_ble_gattc_write_char(this->parent_->get_gattc_if(), this->parent_->get_conn_id(), this->char_handle_, pkt->length, pkt->data, ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE); if (status) { - ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str().c_str(), status); + ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str(), status); } this->current_request_++; } diff --git a/esphome/components/ble_client/automation.h b/esphome/components/ble_client/automation.h index bbc2dd05e0..788eac4a57 100644 --- a/esphome/components/ble_client/automation.h +++ b/esphome/components/ble_client/automation.h @@ -198,7 +198,7 @@ template class BLEClientWriteAction : public Action, publ } this->node_state = espbt::ClientState::ESTABLISHED; esph_log_d(Automation::TAG, "Found characteristic %s on device %s", this->char_uuid_.to_string().c_str(), - ble_client_->address_str().c_str()); + ble_client_->address_str()); break; } default: diff --git a/esphome/components/ble_client/ble_client.cpp b/esphome/components/ble_client/ble_client.cpp index 5cf096c9d4..b8968fe4ba 100644 --- a/esphome/components/ble_client/ble_client.cpp +++ b/esphome/components/ble_client/ble_client.cpp @@ -39,7 +39,7 @@ void BLEClient::set_enabled(bool enabled) { return; this->enabled = enabled; if (!enabled) { - ESP_LOGI(TAG, "[%s] Disabling BLE client.", this->address_str().c_str()); + ESP_LOGI(TAG, "[%s] Disabling BLE client.", this->address_str()); this->disconnect(); } } diff --git a/esphome/components/ble_client/output/ble_binary_output.cpp b/esphome/components/ble_client/output/ble_binary_output.cpp index ce67193be7..84558717f8 100644 --- a/esphome/components/ble_client/output/ble_binary_output.cpp +++ b/esphome/components/ble_client/output/ble_binary_output.cpp @@ -14,7 +14,7 @@ void BLEBinaryOutput::dump_config() { " MAC address : %s\n" " Service UUID : %s\n" " Characteristic UUID: %s", - this->parent_->address_str().c_str(), this->service_uuid_.to_string().c_str(), + this->parent_->address_str(), this->service_uuid_.to_string().c_str(), this->char_uuid_.to_string().c_str()); LOG_BINARY_OUTPUT(this); } @@ -44,7 +44,7 @@ void BLEBinaryOutput::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_i } this->node_state = espbt::ClientState::ESTABLISHED; ESP_LOGD(TAG, "Found characteristic %s on device %s", this->char_uuid_.to_string().c_str(), - this->parent()->address_str().c_str()); + this->parent()->address_str()); this->node_state = espbt::ClientState::ESTABLISHED; break; } diff --git a/esphome/components/ble_client/sensor/ble_rssi_sensor.cpp b/esphome/components/ble_client/sensor/ble_rssi_sensor.cpp index 663c52ac10..4edcbd3877 100644 --- a/esphome/components/ble_client/sensor/ble_rssi_sensor.cpp +++ b/esphome/components/ble_client/sensor/ble_rssi_sensor.cpp @@ -19,7 +19,7 @@ void BLEClientRSSISensor::loop() { void BLEClientRSSISensor::dump_config() { LOG_SENSOR("", "BLE Client RSSI Sensor", this); - ESP_LOGCONFIG(TAG, " MAC address : %s", this->parent()->address_str().c_str()); + ESP_LOGCONFIG(TAG, " MAC address : %s", this->parent()->address_str()); LOG_UPDATE_INTERVAL(this); } @@ -69,10 +69,10 @@ void BLEClientRSSISensor::update() { this->get_rssi_(); } void BLEClientRSSISensor::get_rssi_() { - ESP_LOGV(TAG, "requesting rssi from %s", this->parent()->address_str().c_str()); + ESP_LOGV(TAG, "requesting rssi from %s", this->parent()->address_str()); auto status = esp_ble_gap_read_rssi(this->parent()->get_remote_bda()); if (status != ESP_OK) { - ESP_LOGW(TAG, "esp_ble_gap_read_rssi error, address=%s, status=%d", this->parent()->address_str().c_str(), status); + ESP_LOGW(TAG, "esp_ble_gap_read_rssi error, address=%s, status=%d", this->parent()->address_str(), status); this->status_set_warning(); this->publish_state(NAN); } diff --git a/esphome/components/ble_client/sensor/ble_sensor.cpp b/esphome/components/ble_client/sensor/ble_sensor.cpp index 61685c0566..8e3e483003 100644 --- a/esphome/components/ble_client/sensor/ble_sensor.cpp +++ b/esphome/components/ble_client/sensor/ble_sensor.cpp @@ -25,7 +25,7 @@ void BLESensor::dump_config() { " Characteristic UUID: %s\n" " Descriptor UUID : %s\n" " Notifications : %s", - this->parent()->address_str().c_str(), this->service_uuid_.to_string().c_str(), + this->parent()->address_str(), this->service_uuid_.to_string().c_str(), this->char_uuid_.to_string().c_str(), this->descr_uuid_.to_string().c_str(), YESNO(this->notify_)); LOG_UPDATE_INTERVAL(this); } diff --git a/esphome/components/ble_client/text_sensor/ble_text_sensor.cpp b/esphome/components/ble_client/text_sensor/ble_text_sensor.cpp index b7a6d154db..bb771aed99 100644 --- a/esphome/components/ble_client/text_sensor/ble_text_sensor.cpp +++ b/esphome/components/ble_client/text_sensor/ble_text_sensor.cpp @@ -28,7 +28,7 @@ void BLETextSensor::dump_config() { " Characteristic UUID: %s\n" " Descriptor UUID : %s\n" " Notifications : %s", - this->parent()->address_str().c_str(), this->service_uuid_.to_string().c_str(), + this->parent()->address_str(), this->service_uuid_.to_string().c_str(), this->char_uuid_.to_string().c_str(), this->descr_uuid_.to_string().c_str(), YESNO(this->notify_)); LOG_UPDATE_INTERVAL(this); } diff --git a/esphome/components/bluetooth_proxy/bluetooth_connection.cpp b/esphome/components/bluetooth_proxy/bluetooth_connection.cpp index fcc344dda9..1d6f7e23b3 100644 --- a/esphome/components/bluetooth_proxy/bluetooth_connection.cpp +++ b/esphome/components/bluetooth_proxy/bluetooth_connection.cpp @@ -196,8 +196,8 @@ void BluetoothConnection::send_service_for_discovery_() { if (service_status != ESP_GATT_OK || service_count == 0) { ESP_LOGE(TAG, "[%d] [%s] esp_ble_gattc_get_service %s, status=%d, service_count=%d, offset=%d", - this->connection_index_, this->address_str().c_str(), - service_status != ESP_GATT_OK ? "error" : "missing", service_status, service_count, this->send_service_); + this->connection_index_, this->address_str(), service_status != ESP_GATT_OK ? "error" : "missing", + service_status, service_count, this->send_service_); this->send_service_ = DONE_SENDING_SERVICES; return; } @@ -312,13 +312,13 @@ void BluetoothConnection::send_service_for_discovery_() { if (resp.services.size() > 1) { resp.services.pop_back(); ESP_LOGD(TAG, "[%d] [%s] Service %d would exceed limit (current: %d + service: %d > %d), sending current batch", - this->connection_index_, this->address_str().c_str(), this->send_service_, current_size, service_size, + this->connection_index_, this->address_str(), this->send_service_, current_size, service_size, MAX_PACKET_SIZE); // Don't increment send_service_ - we'll retry this service in next batch } else { // This single service is too large, but we have to send it anyway ESP_LOGV(TAG, "[%d] [%s] Service %d is too large (%d bytes) but sending anyway", this->connection_index_, - this->address_str().c_str(), this->send_service_, service_size); + this->address_str(), this->send_service_, service_size); // Increment so we don't get stuck this->send_service_++; } @@ -337,21 +337,20 @@ void BluetoothConnection::send_service_for_discovery_() { } void BluetoothConnection::log_connection_error_(const char *operation, esp_gatt_status_t status) { - ESP_LOGE(TAG, "[%d] [%s] %s error, status=%d", this->connection_index_, this->address_str().c_str(), operation, - status); + ESP_LOGE(TAG, "[%d] [%s] %s error, status=%d", this->connection_index_, this->address_str(), operation, status); } void BluetoothConnection::log_connection_warning_(const char *operation, esp_err_t err) { - ESP_LOGW(TAG, "[%d] [%s] %s failed, err=%d", this->connection_index_, this->address_str().c_str(), operation, err); + ESP_LOGW(TAG, "[%d] [%s] %s failed, err=%d", this->connection_index_, this->address_str(), operation, err); } void BluetoothConnection::log_gatt_not_connected_(const char *action, const char *type) { - ESP_LOGW(TAG, "[%d] [%s] Cannot %s GATT %s, not connected.", this->connection_index_, this->address_str().c_str(), - action, type); + ESP_LOGW(TAG, "[%d] [%s] Cannot %s GATT %s, not connected.", this->connection_index_, this->address_str(), action, + type); } void BluetoothConnection::log_gatt_operation_error_(const char *operation, uint16_t handle, esp_gatt_status_t status) { - ESP_LOGW(TAG, "[%d] [%s] Error %s for handle 0x%2X, status=%d", this->connection_index_, this->address_str().c_str(), + ESP_LOGW(TAG, "[%d] [%s] Error %s for handle 0x%2X, status=%d", this->connection_index_, this->address_str(), operation, handle, status); } @@ -372,14 +371,14 @@ bool BluetoothConnection::gattc_event_handler(esp_gattc_cb_event_t event, esp_ga case ESP_GATTC_DISCONNECT_EVT: { // Don't reset connection yet - wait for CLOSE_EVT to ensure controller has freed resources // This prevents race condition where we mark slot as free before controller cleanup is complete - ESP_LOGD(TAG, "[%d] [%s] Disconnect, reason=0x%02x", this->connection_index_, this->address_str_.c_str(), + ESP_LOGD(TAG, "[%d] [%s] Disconnect, reason=0x%02x", this->connection_index_, this->address_str_, param->disconnect.reason); // Send disconnection notification but don't free the slot yet this->proxy_->send_device_connection(this->address_, false, 0, param->disconnect.reason); break; } case ESP_GATTC_CLOSE_EVT: { - ESP_LOGD(TAG, "[%d] [%s] Close, reason=0x%02x, freeing slot", this->connection_index_, this->address_str_.c_str(), + ESP_LOGD(TAG, "[%d] [%s] Close, reason=0x%02x, freeing slot", this->connection_index_, this->address_str_, param->close.reason); // Now the GATT connection is fully closed and controller resources are freed // Safe to mark the connection slot as available @@ -463,7 +462,7 @@ bool BluetoothConnection::gattc_event_handler(esp_gattc_cb_event_t event, esp_ga break; } case ESP_GATTC_NOTIFY_EVT: { - ESP_LOGV(TAG, "[%d] [%s] ESP_GATTC_NOTIFY_EVT: handle=0x%2X", this->connection_index_, this->address_str_.c_str(), + ESP_LOGV(TAG, "[%d] [%s] ESP_GATTC_NOTIFY_EVT: handle=0x%2X", this->connection_index_, this->address_str_, param->notify.handle); api::BluetoothGATTNotifyDataResponse resp; resp.address = this->address_; @@ -502,8 +501,7 @@ esp_err_t BluetoothConnection::read_characteristic(uint16_t handle) { return ESP_GATT_NOT_CONNECTED; } - ESP_LOGV(TAG, "[%d] [%s] Reading GATT characteristic handle %d", this->connection_index_, this->address_str_.c_str(), - handle); + ESP_LOGV(TAG, "[%d] [%s] Reading GATT characteristic handle %d", this->connection_index_, this->address_str_, handle); esp_err_t err = esp_ble_gattc_read_char(this->gattc_if_, this->conn_id_, handle, ESP_GATT_AUTH_REQ_NONE); return this->check_and_log_error_("esp_ble_gattc_read_char", err); @@ -515,8 +513,7 @@ esp_err_t BluetoothConnection::write_characteristic(uint16_t handle, const uint8 this->log_gatt_not_connected_("write", "characteristic"); return ESP_GATT_NOT_CONNECTED; } - ESP_LOGV(TAG, "[%d] [%s] Writing GATT characteristic handle %d", this->connection_index_, this->address_str_.c_str(), - handle); + ESP_LOGV(TAG, "[%d] [%s] Writing GATT characteristic handle %d", this->connection_index_, this->address_str_, handle); // ESP-IDF's API requires a non-const uint8_t* but it doesn't modify the data // The BTC layer immediately copies the data to its own buffer (see btc_gattc.c) @@ -532,8 +529,7 @@ esp_err_t BluetoothConnection::read_descriptor(uint16_t handle) { this->log_gatt_not_connected_("read", "descriptor"); return ESP_GATT_NOT_CONNECTED; } - ESP_LOGV(TAG, "[%d] [%s] Reading GATT descriptor handle %d", this->connection_index_, this->address_str_.c_str(), - handle); + ESP_LOGV(TAG, "[%d] [%s] Reading GATT descriptor handle %d", this->connection_index_, this->address_str_, handle); esp_err_t err = esp_ble_gattc_read_char_descr(this->gattc_if_, this->conn_id_, handle, ESP_GATT_AUTH_REQ_NONE); return this->check_and_log_error_("esp_ble_gattc_read_char_descr", err); @@ -544,8 +540,7 @@ esp_err_t BluetoothConnection::write_descriptor(uint16_t handle, const uint8_t * this->log_gatt_not_connected_("write", "descriptor"); return ESP_GATT_NOT_CONNECTED; } - ESP_LOGV(TAG, "[%d] [%s] Writing GATT descriptor handle %d", this->connection_index_, this->address_str_.c_str(), - handle); + ESP_LOGV(TAG, "[%d] [%s] Writing GATT descriptor handle %d", this->connection_index_, this->address_str_, handle); // ESP-IDF's API requires a non-const uint8_t* but it doesn't modify the data // The BTC layer immediately copies the data to its own buffer (see btc_gattc.c) @@ -564,13 +559,13 @@ esp_err_t BluetoothConnection::notify_characteristic(uint16_t handle, bool enabl if (enable) { ESP_LOGV(TAG, "[%d] [%s] Registering for GATT characteristic notifications handle %d", this->connection_index_, - this->address_str_.c_str(), handle); + this->address_str_, handle); esp_err_t err = esp_ble_gattc_register_for_notify(this->gattc_if_, this->remote_bda_, handle); return this->check_and_log_error_("esp_ble_gattc_register_for_notify", err); } ESP_LOGV(TAG, "[%d] [%s] Unregistering for GATT characteristic notifications handle %d", this->connection_index_, - this->address_str_.c_str(), handle); + this->address_str_, handle); esp_err_t err = esp_ble_gattc_unregister_for_notify(this->gattc_if_, this->remote_bda_, handle); return this->check_and_log_error_("esp_ble_gattc_unregister_for_notify", err); } diff --git a/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp b/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp index 34e0aa93a3..71f8da75a7 100644 --- a/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp +++ b/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp @@ -47,12 +47,11 @@ void BluetoothProxy::send_bluetooth_scanner_state_(esp32_ble_tracker::ScannerSta void BluetoothProxy::log_connection_request_ignored_(BluetoothConnection *connection, espbt::ClientState state) { ESP_LOGW(TAG, "[%d] [%s] Connection request ignored, state: %s", connection->get_connection_index(), - connection->address_str().c_str(), espbt::client_state_to_string(state)); + connection->address_str(), espbt::client_state_to_string(state)); } void BluetoothProxy::log_connection_info_(BluetoothConnection *connection, const char *message) { - ESP_LOGI(TAG, "[%d] [%s] Connecting %s", connection->get_connection_index(), connection->address_str().c_str(), - message); + ESP_LOGI(TAG, "[%d] [%s] Connecting %s", connection->get_connection_index(), connection->address_str(), message); } void BluetoothProxy::log_not_connected_gatt_(const char *action, const char *type) { @@ -186,7 +185,7 @@ void BluetoothProxy::bluetooth_device_request(const api::BluetoothDeviceRequest } if (!msg.has_address_type) { ESP_LOGE(TAG, "[%d] [%s] Missing address type in connect request", connection->get_connection_index(), - connection->address_str().c_str()); + connection->address_str()); this->send_device_connection(msg.address, false); return; } @@ -199,7 +198,7 @@ void BluetoothProxy::bluetooth_device_request(const api::BluetoothDeviceRequest } else if (connection->state() == espbt::ClientState::CONNECTING) { if (connection->disconnect_pending()) { ESP_LOGW(TAG, "[%d] [%s] Connection request while pending disconnect, cancelling pending disconnect", - connection->get_connection_index(), connection->address_str().c_str()); + connection->get_connection_index(), connection->address_str()); connection->cancel_pending_disconnect(); return; } @@ -339,7 +338,7 @@ void BluetoothProxy::bluetooth_gatt_send_services(const api::BluetoothGATTGetSer return; } if (!connection->service_count_) { - ESP_LOGW(TAG, "[%d] [%s] No GATT services found", connection->connection_index_, connection->address_str().c_str()); + ESP_LOGW(TAG, "[%d] [%s] No GATT services found", connection->connection_index_, connection->address_str()); this->send_gatt_services_done(msg.address); return; } diff --git a/esphome/components/esp32_ble_client/ble_characteristic.cpp b/esphome/components/esp32_ble_client/ble_characteristic.cpp index 36229c23c3..e0d0174c57 100644 --- a/esphome/components/esp32_ble_client/ble_characteristic.cpp +++ b/esphome/components/esp32_ble_client/ble_characteristic.cpp @@ -38,7 +38,7 @@ void BLECharacteristic::parse_descriptors() { } if (status != ESP_GATT_OK) { ESP_LOGW(TAG, "[%d] [%s] esp_ble_gattc_get_all_descr error, status=%d", - this->service->client->get_connection_index(), this->service->client->address_str().c_str(), status); + this->service->client->get_connection_index(), this->service->client->address_str(), status); break; } if (count == 0) { @@ -51,7 +51,7 @@ void BLECharacteristic::parse_descriptors() { desc->characteristic = this; this->descriptors.push_back(desc); ESP_LOGV(TAG, "[%d] [%s] descriptor %s, handle 0x%x", this->service->client->get_connection_index(), - this->service->client->address_str().c_str(), desc->uuid.to_string().c_str(), desc->handle); + this->service->client->address_str(), desc->uuid.to_string().c_str(), desc->handle); offset++; } } @@ -84,7 +84,7 @@ esp_err_t BLECharacteristic::write_value(uint8_t *new_val, int16_t new_val_size, new_val, write_type, ESP_GATT_AUTH_REQ_NONE); if (status) { ESP_LOGW(TAG, "[%d] [%s] Error sending write value to BLE gattc server, status=%d", - this->service->client->get_connection_index(), this->service->client->address_str().c_str(), status); + this->service->client->get_connection_index(), this->service->client->address_str(), status); } return status; } diff --git a/esphome/components/esp32_ble_client/ble_client_base.cpp b/esphome/components/esp32_ble_client/ble_client_base.cpp index 18321ef91c..07e88c7528 100644 --- a/esphome/components/esp32_ble_client/ble_client_base.cpp +++ b/esphome/components/esp32_ble_client/ble_client_base.cpp @@ -41,7 +41,7 @@ void BLEClientBase::setup() { } void BLEClientBase::set_state(espbt::ClientState st) { - ESP_LOGV(TAG, "[%d] [%s] Set state %d", this->connection_index_, this->address_str_.c_str(), (int) st); + ESP_LOGV(TAG, "[%d] [%s] Set state %d", this->connection_index_, this->address_str_, (int) st); ESPBTClient::set_state(st); } @@ -71,7 +71,7 @@ void BLEClientBase::dump_config() { ESP_LOGCONFIG(TAG, " Address: %s\n" " Auto-Connect: %s", - this->address_str().c_str(), TRUEFALSE(this->auto_connect_)); + this->address_str(), TRUEFALSE(this->auto_connect_)); ESP_LOGCONFIG(TAG, " State: %s", espbt::client_state_to_string(this->state())); if (this->status_ == ESP_GATT_NO_RESOURCES) { ESP_LOGE(TAG, " Failed due to no resources. Try to reduce number of BLE clients in config."); @@ -104,12 +104,11 @@ void BLEClientBase::connect() { // Prevent duplicate connection attempts if (this->state_ == espbt::ClientState::CONNECTING || this->state_ == espbt::ClientState::CONNECTED || this->state_ == espbt::ClientState::ESTABLISHED) { - ESP_LOGW(TAG, "[%d] [%s] Connection already in progress, state=%s", this->connection_index_, - this->address_str_.c_str(), espbt::client_state_to_string(this->state_)); + ESP_LOGW(TAG, "[%d] [%s] Connection already in progress, state=%s", this->connection_index_, this->address_str_, + espbt::client_state_to_string(this->state_)); return; } - ESP_LOGI(TAG, "[%d] [%s] 0x%02x Connecting", this->connection_index_, this->address_str_.c_str(), - this->remote_addr_type_); + ESP_LOGI(TAG, "[%d] [%s] 0x%02x Connecting", this->connection_index_, this->address_str_, this->remote_addr_type_); this->paired_ = false; // Enable loop for state processing this->enable_loop(); @@ -135,13 +134,13 @@ esp_err_t BLEClientBase::pair() { return esp_ble_set_encryption(this->remote_bda void BLEClientBase::disconnect() { if (this->state_ == espbt::ClientState::IDLE || this->state_ == espbt::ClientState::DISCONNECTING) { - ESP_LOGI(TAG, "[%d] [%s] Disconnect requested, but already %s", this->connection_index_, this->address_str_.c_str(), + ESP_LOGI(TAG, "[%d] [%s] Disconnect requested, but already %s", this->connection_index_, this->address_str_, espbt::client_state_to_string(this->state_)); return; } if (this->state_ == espbt::ClientState::CONNECTING || this->conn_id_ == UNSET_CONN_ID) { ESP_LOGD(TAG, "[%d] [%s] Disconnect before connected, disconnect scheduled", this->connection_index_, - this->address_str_.c_str()); + this->address_str_); this->want_disconnect_ = true; return; } @@ -150,8 +149,7 @@ void BLEClientBase::disconnect() { void BLEClientBase::unconditional_disconnect() { // Disconnect without checking the state. - ESP_LOGI(TAG, "[%d] [%s] Disconnecting (conn_id: %d).", this->connection_index_, this->address_str_.c_str(), - this->conn_id_); + ESP_LOGI(TAG, "[%d] [%s] Disconnecting (conn_id: %d).", this->connection_index_, this->address_str_, this->conn_id_); if (this->state_ == espbt::ClientState::DISCONNECTING) { this->log_error_("Already disconnecting"); return; @@ -192,24 +190,23 @@ void BLEClientBase::release_services() { } void BLEClientBase::log_event_(const char *name) { - ESP_LOGD(TAG, "[%d] [%s] %s", this->connection_index_, this->address_str_.c_str(), name); + ESP_LOGD(TAG, "[%d] [%s] %s", this->connection_index_, this->address_str_, name); } void BLEClientBase::log_gattc_event_(const char *name) { - ESP_LOGD(TAG, "[%d] [%s] ESP_GATTC_%s_EVT", this->connection_index_, this->address_str_.c_str(), name); + ESP_LOGD(TAG, "[%d] [%s] ESP_GATTC_%s_EVT", this->connection_index_, this->address_str_, name); } void BLEClientBase::log_gattc_warning_(const char *operation, esp_gatt_status_t status) { - ESP_LOGW(TAG, "[%d] [%s] %s error, status=%d", this->connection_index_, this->address_str_.c_str(), operation, - status); + ESP_LOGW(TAG, "[%d] [%s] %s error, status=%d", this->connection_index_, this->address_str_, operation, status); } void BLEClientBase::log_gattc_warning_(const char *operation, esp_err_t err) { - ESP_LOGW(TAG, "[%d] [%s] %s error, status=%d", this->connection_index_, this->address_str_.c_str(), operation, err); + ESP_LOGW(TAG, "[%d] [%s] %s error, status=%d", this->connection_index_, this->address_str_, operation, err); } void BLEClientBase::log_connection_params_(const char *param_type) { - ESP_LOGD(TAG, "[%d] [%s] %s conn params", this->connection_index_, this->address_str_.c_str(), param_type); + ESP_LOGD(TAG, "[%d] [%s] %s conn params", this->connection_index_, this->address_str_, param_type); } void BLEClientBase::handle_connection_result_(esp_err_t ret) { @@ -220,15 +217,15 @@ void BLEClientBase::handle_connection_result_(esp_err_t ret) { } void BLEClientBase::log_error_(const char *message) { - ESP_LOGE(TAG, "[%d] [%s] %s", this->connection_index_, this->address_str_.c_str(), message); + ESP_LOGE(TAG, "[%d] [%s] %s", this->connection_index_, this->address_str_, message); } void BLEClientBase::log_error_(const char *message, int code) { - ESP_LOGE(TAG, "[%d] [%s] %s=%d", this->connection_index_, this->address_str_.c_str(), message, code); + ESP_LOGE(TAG, "[%d] [%s] %s=%d", this->connection_index_, this->address_str_, message, code); } void BLEClientBase::log_warning_(const char *message) { - ESP_LOGW(TAG, "[%d] [%s] %s", this->connection_index_, this->address_str_.c_str(), message); + ESP_LOGW(TAG, "[%d] [%s] %s", this->connection_index_, this->address_str_, message); } void BLEClientBase::update_conn_params_(uint16_t min_interval, uint16_t max_interval, uint16_t latency, @@ -264,13 +261,13 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_ if (event != ESP_GATTC_REG_EVT && esp_gattc_if != ESP_GATT_IF_NONE && esp_gattc_if != this->gattc_if_) return false; - ESP_LOGV(TAG, "[%d] [%s] gattc_event_handler: event=%d gattc_if=%d", this->connection_index_, - this->address_str_.c_str(), event, esp_gattc_if); + ESP_LOGV(TAG, "[%d] [%s] gattc_event_handler: event=%d gattc_if=%d", this->connection_index_, this->address_str_, + event, esp_gattc_if); switch (event) { case ESP_GATTC_REG_EVT: { if (param->reg.status == ESP_GATT_OK) { - ESP_LOGV(TAG, "[%d] [%s] gattc registered app id %d", this->connection_index_, this->address_str_.c_str(), + ESP_LOGV(TAG, "[%d] [%s] gattc registered app id %d", this->connection_index_, this->address_str_, this->app_id); this->gattc_if_ = esp_gattc_if; } else { @@ -292,7 +289,7 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_ // arriving after we've already transitioned to IDLE state. if (this->state_ == espbt::ClientState::IDLE) { ESP_LOGD(TAG, "[%d] [%s] ESP_GATTC_OPEN_EVT in IDLE state (status=%d), ignoring", this->connection_index_, - this->address_str_.c_str(), param->open.status); + this->address_str_, param->open.status); break; } @@ -301,7 +298,7 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_ // because it means we have a bad assumption about how the // ESP BT stack works. ESP_LOGE(TAG, "[%d] [%s] ESP_GATTC_OPEN_EVT in %s state (status=%d)", this->connection_index_, - this->address_str_.c_str(), espbt::client_state_to_string(this->state_), param->open.status); + this->address_str_, espbt::client_state_to_string(this->state_), param->open.status); } if (param->open.status != ESP_GATT_OK && param->open.status != ESP_GATT_ALREADY_OPEN) { this->log_gattc_warning_("Connection open", param->open.status); @@ -318,7 +315,7 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_ } // MTU negotiation already started in ESP_GATTC_CONNECT_EVT this->set_state(espbt::ClientState::CONNECTED); - ESP_LOGI(TAG, "[%d] [%s] Connection open", this->connection_index_, this->address_str_.c_str()); + ESP_LOGI(TAG, "[%d] [%s] Connection open", this->connection_index_, this->address_str_); if (this->connection_type_ == espbt::ConnectionType::V3_WITH_CACHE) { // Cached connections already connected with medium parameters, no update needed // only set our state, subclients might have more stuff to do yet. @@ -354,8 +351,8 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_ this->state_ == espbt::ClientState::CONNECTED) { this->log_warning_("Remote closed during discovery"); } else { - ESP_LOGD(TAG, "[%d] [%s] ESP_GATTC_DISCONNECT_EVT, reason 0x%02x", this->connection_index_, - this->address_str_.c_str(), param->disconnect.reason); + ESP_LOGD(TAG, "[%d] [%s] ESP_GATTC_DISCONNECT_EVT, reason 0x%02x", this->connection_index_, this->address_str_, + param->disconnect.reason); } this->release_services(); this->set_state(espbt::ClientState::IDLE); @@ -366,12 +363,12 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_ if (this->conn_id_ != param->cfg_mtu.conn_id) return false; if (param->cfg_mtu.status != ESP_GATT_OK) { - ESP_LOGW(TAG, "[%d] [%s] cfg_mtu failed, mtu %d, status %d", this->connection_index_, - this->address_str_.c_str(), param->cfg_mtu.mtu, param->cfg_mtu.status); + ESP_LOGW(TAG, "[%d] [%s] cfg_mtu failed, mtu %d, status %d", this->connection_index_, this->address_str_, + param->cfg_mtu.mtu, param->cfg_mtu.status); // No state change required here - disconnect event will follow if needed. break; } - ESP_LOGD(TAG, "[%d] [%s] cfg_mtu status %d, mtu %d", this->connection_index_, this->address_str_.c_str(), + ESP_LOGD(TAG, "[%d] [%s] cfg_mtu status %d, mtu %d", this->connection_index_, this->address_str_, param->cfg_mtu.status, param->cfg_mtu.mtu); this->mtu_ = param->cfg_mtu.mtu; break; @@ -415,14 +412,14 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_ } else if (this->connection_type_ != espbt::ConnectionType::V3_WITH_CACHE) { #ifdef USE_ESP32_BLE_DEVICE for (auto &svc : this->services_) { - ESP_LOGV(TAG, "[%d] [%s] Service UUID: %s", this->connection_index_, this->address_str_.c_str(), + ESP_LOGV(TAG, "[%d] [%s] Service UUID: %s", this->connection_index_, this->address_str_, svc->uuid.to_string().c_str()); - ESP_LOGV(TAG, "[%d] [%s] start_handle: 0x%x end_handle: 0x%x", this->connection_index_, - this->address_str_.c_str(), svc->start_handle, svc->end_handle); + ESP_LOGV(TAG, "[%d] [%s] start_handle: 0x%x end_handle: 0x%x", this->connection_index_, this->address_str_, + svc->start_handle, svc->end_handle); } #endif } - ESP_LOGI(TAG, "[%d] [%s] Service discovery complete", this->connection_index_, this->address_str_.c_str()); + ESP_LOGI(TAG, "[%d] [%s] Service discovery complete", this->connection_index_, this->address_str_); this->state_ = espbt::ClientState::ESTABLISHED; break; } @@ -503,7 +500,7 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_ default: // ideally would check all other events for matching conn_id - ESP_LOGD(TAG, "[%d] [%s] Event %d", this->connection_index_, this->address_str_.c_str(), event); + ESP_LOGD(TAG, "[%d] [%s] Event %d", this->connection_index_, this->address_str_, event); break; } return true; @@ -520,7 +517,7 @@ void BLEClientBase::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_ case ESP_GAP_BLE_SEC_REQ_EVT: if (!this->check_addr(param->ble_security.auth_cmpl.bd_addr)) return; - ESP_LOGV(TAG, "[%d] [%s] ESP_GAP_BLE_SEC_REQ_EVT %x", this->connection_index_, this->address_str_.c_str(), event); + ESP_LOGV(TAG, "[%d] [%s] ESP_GAP_BLE_SEC_REQ_EVT %x", this->connection_index_, this->address_str_, event); esp_ble_gap_security_rsp(param->ble_security.ble_req.bd_addr, true); break; // This event is sent once authentication has completed @@ -529,13 +526,13 @@ void BLEClientBase::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_ return; esp_bd_addr_t bd_addr; memcpy(bd_addr, param->ble_security.auth_cmpl.bd_addr, sizeof(esp_bd_addr_t)); - ESP_LOGI(TAG, "[%d] [%s] auth complete addr: %s", this->connection_index_, this->address_str_.c_str(), + ESP_LOGI(TAG, "[%d] [%s] auth complete addr: %s", this->connection_index_, this->address_str_, format_hex(bd_addr, 6).c_str()); if (!param->ble_security.auth_cmpl.success) { this->log_error_("auth fail reason", param->ble_security.auth_cmpl.fail_reason); } else { this->paired_ = true; - ESP_LOGD(TAG, "[%d] [%s] auth success type = %d mode = %d", this->connection_index_, this->address_str_.c_str(), + ESP_LOGD(TAG, "[%d] [%s] auth success type = %d mode = %d", this->connection_index_, this->address_str_, param->ble_security.auth_cmpl.addr_type, param->ble_security.auth_cmpl.auth_mode); } break; @@ -598,7 +595,7 @@ float BLEClientBase::parse_char_value(uint8_t *value, uint16_t length) { } } ESP_LOGW(TAG, "[%d] [%s] Cannot parse characteristic value of type 0x%x length %d", this->connection_index_, - this->address_str_.c_str(), value[0], length); + this->address_str_, value[0], length); return NAN; } diff --git a/esphome/components/esp32_ble_client/ble_client_base.h b/esphome/components/esp32_ble_client/ble_client_base.h index 7f0ae3b83e..7786495915 100644 --- a/esphome/components/esp32_ble_client/ble_client_base.h +++ b/esphome/components/esp32_ble_client/ble_client_base.h @@ -10,7 +10,6 @@ #endif #include -#include #include #include @@ -23,6 +22,7 @@ namespace esphome::esp32_ble_client { namespace espbt = esphome::esp32_ble_tracker; static const int UNSET_CONN_ID = 0xFFFF; +static constexpr size_t MAC_ADDR_STR_LEN = 18; // "AA:BB:CC:DD:EE:FF\0" class BLEClientBase : public espbt::ESPBTClient, public Component { public: @@ -58,14 +58,12 @@ class BLEClientBase : public espbt::ESPBTClient, public Component { this->remote_bda_[4] = (address >> 8) & 0xFF; this->remote_bda_[5] = (address >> 0) & 0xFF; if (address == 0) { - this->address_str_ = ""; + this->address_str_[0] = '\0'; } else { - char buf[18]; - format_mac_addr_upper(this->remote_bda_, buf); - this->address_str_ = buf; + format_mac_addr_upper(this->remote_bda_, this->address_str_); } } - const std::string &address_str() const { return this->address_str_; } + const char *address_str() const { return this->address_str_; } #ifdef USE_ESP32_BLE_DEVICE BLEService *get_service(espbt::ESPBTUUID uuid); @@ -104,7 +102,6 @@ class BLEClientBase : public espbt::ESPBTClient, public Component { uint64_t address_{0}; // Group 2: Container types (grouped for memory optimization) - std::string address_str_{}; #ifdef USE_ESP32_BLE_DEVICE std::vector services_; #endif @@ -113,8 +110,9 @@ class BLEClientBase : public espbt::ESPBTClient, public Component { int gattc_if_; esp_gatt_status_t status_{ESP_GATT_OK}; - // Group 4: Arrays (6 bytes) - esp_bd_addr_t remote_bda_; + // Group 4: Arrays + char address_str_[MAC_ADDR_STR_LEN]{}; // 18 bytes: "AA:BB:CC:DD:EE:FF\0" + esp_bd_addr_t remote_bda_; // 6 bytes // Group 5: 2-byte types uint16_t conn_id_{UNSET_CONN_ID}; diff --git a/esphome/components/esp32_ble_client/ble_service.cpp b/esphome/components/esp32_ble_client/ble_service.cpp index accaad15e1..deaaa3de02 100644 --- a/esphome/components/esp32_ble_client/ble_service.cpp +++ b/esphome/components/esp32_ble_client/ble_service.cpp @@ -51,7 +51,7 @@ void BLEService::parse_characteristics() { } if (status != ESP_GATT_OK) { ESP_LOGW(TAG, "[%d] [%s] esp_ble_gattc_get_all_char error, status=%d", this->client->get_connection_index(), - this->client->address_str().c_str(), status); + this->client->address_str(), status); break; } if (count == 0) { @@ -65,7 +65,7 @@ void BLEService::parse_characteristics() { characteristic->service = this; this->characteristics.push_back(characteristic); ESP_LOGV(TAG, "[%d] [%s] characteristic %s, handle 0x%x, properties 0x%x", this->client->get_connection_index(), - this->client->address_str().c_str(), characteristic->uuid.to_string().c_str(), characteristic->handle, + this->client->address_str(), characteristic->uuid.to_string().c_str(), characteristic->handle, characteristic->properties); offset++; } diff --git a/esphome/components/pvvx_mithermometer/display/pvvx_display.cpp b/esphome/components/pvvx_mithermometer/display/pvvx_display.cpp index b6916ad68f..8436633619 100644 --- a/esphome/components/pvvx_mithermometer/display/pvvx_display.cpp +++ b/esphome/components/pvvx_mithermometer/display/pvvx_display.cpp @@ -14,7 +14,7 @@ void PVVXDisplay::dump_config() { " Service UUID : %s\n" " Characteristic UUID : %s\n" " Auto clear : %s", - this->parent_->address_str().c_str(), this->service_uuid_.to_string().c_str(), + this->parent_->address_str(), this->service_uuid_.to_string().c_str(), this->char_uuid_.to_string().c_str(), YESNO(this->auto_clear_enabled_)); #ifdef USE_TIME ESP_LOGCONFIG(TAG, " Set time on connection: %s", YESNO(this->time_ != nullptr)); @@ -28,12 +28,12 @@ void PVVXDisplay::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t switch (event) { case ESP_GATTC_OPEN_EVT: if (param->open.status == ESP_GATT_OK) { - ESP_LOGV(TAG, "[%s] Connected successfully!", this->parent_->address_str().c_str()); + ESP_LOGV(TAG, "[%s] Connected successfully!", this->parent_->address_str()); this->delayed_disconnect_(); } break; case ESP_GATTC_DISCONNECT_EVT: - ESP_LOGV(TAG, "[%s] Disconnected", this->parent_->address_str().c_str()); + ESP_LOGV(TAG, "[%s] Disconnected", this->parent_->address_str()); this->connection_established_ = false; this->cancel_timeout("disconnect"); this->char_handle_ = 0; @@ -41,7 +41,7 @@ void PVVXDisplay::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t case ESP_GATTC_SEARCH_CMPL_EVT: { auto *chr = this->parent_->get_characteristic(this->service_uuid_, this->char_uuid_); if (chr == nullptr) { - ESP_LOGW(TAG, "[%s] Characteristic not found.", this->parent_->address_str().c_str()); + ESP_LOGW(TAG, "[%s] Characteristic not found.", this->parent_->address_str()); break; } this->connection_established_ = true; @@ -66,11 +66,11 @@ void PVVXDisplay::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb return; if (param->ble_security.auth_cmpl.success) { - ESP_LOGD(TAG, "[%s] Authentication successful, performing writes.", this->parent_->address_str().c_str()); + ESP_LOGD(TAG, "[%s] Authentication successful, performing writes.", this->parent_->address_str()); // Now that pairing is complete, perform the pending writes this->sync_time_and_display_(); } else { - ESP_LOGW(TAG, "[%s] Authentication failed.", this->parent_->address_str().c_str()); + ESP_LOGW(TAG, "[%s] Authentication failed.", this->parent_->address_str()); } break; } @@ -89,22 +89,20 @@ void PVVXDisplay::update() { void PVVXDisplay::display() { if (!this->parent_->enabled) { - ESP_LOGD(TAG, "[%s] BLE client not enabled. Init connection.", this->parent_->address_str().c_str()); + ESP_LOGD(TAG, "[%s] BLE client not enabled. Init connection.", this->parent_->address_str()); this->parent_->set_enabled(true); return; } if (!this->connection_established_) { - ESP_LOGW(TAG, "[%s] Not connected to BLE client. State update can not be written.", - this->parent_->address_str().c_str()); + ESP_LOGW(TAG, "[%s] Not connected to BLE client. State update can not be written.", this->parent_->address_str()); return; } if (!this->char_handle_) { - ESP_LOGW(TAG, "[%s] No ble handle to BLE client. State update can not be written.", - this->parent_->address_str().c_str()); + ESP_LOGW(TAG, "[%s] No ble handle to BLE client. State update can not be written.", this->parent_->address_str()); return; } ESP_LOGD(TAG, "[%s] Send to display: bignum %d, smallnum: %d, cfg: 0x%02x, validity period: %u.", - this->parent_->address_str().c_str(), this->bignum_, this->smallnum_, this->cfg_, this->validity_period_); + this->parent_->address_str(), this->bignum_, this->smallnum_, this->cfg_, this->validity_period_); uint8_t blk[8] = {}; blk[0] = 0x22; blk[1] = this->bignum_ & 0xff; @@ -128,16 +126,16 @@ void PVVXDisplay::setcfgbit_(uint8_t bit, bool value) { void PVVXDisplay::send_to_setup_char_(uint8_t *blk, size_t size) { if (!this->connection_established_) { - ESP_LOGW(TAG, "[%s] Not connected to BLE client.", this->parent_->address_str().c_str()); + ESP_LOGW(TAG, "[%s] Not connected to BLE client.", this->parent_->address_str()); return; } auto status = esp_ble_gattc_write_char(this->parent_->get_gattc_if(), this->parent_->get_conn_id(), this->char_handle_, size, blk, ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE); if (status) { - ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str().c_str(), status); + ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str(), status); } else { - ESP_LOGV(TAG, "[%s] send %u bytes", this->parent_->address_str().c_str(), size); + ESP_LOGV(TAG, "[%s] send %u bytes", this->parent_->address_str(), size); this->delayed_disconnect_(); } } @@ -161,21 +159,21 @@ void PVVXDisplay::sync_time_() { if (this->time_ == nullptr) return; if (!this->connection_established_) { - ESP_LOGW(TAG, "[%s] Not connected to BLE client. Time can not be synced.", this->parent_->address_str().c_str()); + ESP_LOGW(TAG, "[%s] Not connected to BLE client. Time can not be synced.", this->parent_->address_str()); return; } if (!this->char_handle_) { - ESP_LOGW(TAG, "[%s] No ble handle to BLE client. Time can not be synced.", this->parent_->address_str().c_str()); + ESP_LOGW(TAG, "[%s] No ble handle to BLE client. Time can not be synced.", this->parent_->address_str()); return; } auto time = this->time_->now(); if (!time.is_valid()) { - ESP_LOGW(TAG, "[%s] Time is not yet valid. Time can not be synced.", this->parent_->address_str().c_str()); + ESP_LOGW(TAG, "[%s] Time is not yet valid. Time can not be synced.", this->parent_->address_str()); return; } time.recalc_timestamp_utc(true); // calculate timestamp of local time uint8_t blk[5] = {}; - ESP_LOGD(TAG, "[%s] Sync time with timestamp %" PRIu64 ".", this->parent_->address_str().c_str(), time.timestamp); + ESP_LOGD(TAG, "[%s] Sync time with timestamp %" PRIu64 ".", this->parent_->address_str(), time.timestamp); blk[0] = 0x23; blk[1] = time.timestamp & 0xff; blk[2] = (time.timestamp >> 8) & 0xff; From d443dbbf344626357cb2321c84cf189f0fc75bef Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 25 Nov 2025 19:42:09 -0600 Subject: [PATCH 303/415] [lvgl] Fix lambda return types for coord and font validators (#12113) --- esphome/components/lvgl/lv_validation.py | 6 ++++-- esphome/components/lvgl/widgets/line.py | 6 ++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/esphome/components/lvgl/lv_validation.py b/esphome/components/lvgl/lv_validation.py index 23c322c31f..9c1dd22085 100644 --- a/esphome/components/lvgl/lv_validation.py +++ b/esphome/components/lvgl/lv_validation.py @@ -40,7 +40,7 @@ from .helpers import ( lv_fonts_used, requires_component, ) -from .types import lv_font_t, lv_gradient_t +from .types import lv_gradient_t opacity_consts = LvConstant("LV_OPA_", "TRANSP", "COVER") @@ -498,7 +498,9 @@ class LvFont(LValidator): esphome_fonts_used.add(fontval) return requires_component("font")(fontval) - super().__init__(validator, lv_font_t) + # Use font::Font* as return type for lambdas returning ESPHome fonts + # The inline overloads in lvgl_esphome.h handle conversion to lv_font_t* + super().__init__(validator, Font.operator("ptr")) async def process(self, value, args=()): if is_lv_font(value): diff --git a/esphome/components/lvgl/widgets/line.py b/esphome/components/lvgl/widgets/line.py index bd90edbefc..57cb965737 100644 --- a/esphome/components/lvgl/widgets/line.py +++ b/esphome/components/lvgl/widgets/line.py @@ -6,7 +6,7 @@ from esphome.core import Lambda from ..defines import CONF_MAIN, call_lambda from ..lvcode import lv_add from ..schemas import point_schema -from ..types import LvCompound, LvType +from ..types import LvCompound, LvType, lv_coord_t from . import Widget, WidgetType CONF_LINE = "line" @@ -23,9 +23,7 @@ LINE_SCHEMA = { async def process_coord(coord): if isinstance(coord, Lambda): - coord = call_lambda( - await cg.process_lambda(coord, [], return_type="lv_coord_t") - ) + coord = call_lambda(await cg.process_lambda(coord, [], return_type=lv_coord_t)) if not coord.endswith("()"): coord = f"static_cast({coord})" return cg.RawExpression(coord) From f071b6232a2f9cd043b4870384a9070019221b2d Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Wed, 26 Nov 2025 11:47:27 +1000 Subject: [PATCH 304/415] [lvgl] Fix position of errors in widget config (#12111) Co-authored-by: J. Nick Koston --- esphome/components/lvgl/schemas.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/esphome/components/lvgl/schemas.py b/esphome/components/lvgl/schemas.py index 6b77f66abb..b2d463c5fd 100644 --- a/esphome/components/lvgl/schemas.py +++ b/esphome/components/lvgl/schemas.py @@ -1,6 +1,7 @@ from esphome import config_validation as cv from esphome.automation import Trigger, validate_automation from esphome.components.time import RealTimeClock +from esphome.config_validation import prepend_path from esphome.const import ( CONF_ARGS, CONF_FORMAT, @@ -422,7 +423,10 @@ def any_widget_schema(extras=None): def validator(value): if isinstance(value, dict): # Convert to list + is_dict = True value = [{k: v} for k, v in value.items()] + else: + is_dict = False if not isinstance(value, list): raise cv.Invalid("Expected a list of widgets") result = [] @@ -443,7 +447,9 @@ def any_widget_schema(extras=None): ) # Apply custom validation value = widget_type.validate(value or {}) - result.append({key: container_validator(value)}) + path = [key] if is_dict else [index, key] + with prepend_path(path): + result.append({key: container_validator(value)}) return result return validator From e071380532a3082ac3ba7de0c52aae5250720226 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Wed, 26 Nov 2025 11:49:47 +1000 Subject: [PATCH 305/415] [lvgl] Add missing obj scroll properties (#11901) Co-authored-by: J. Nick Koston --- esphome/components/lvgl/defines.py | 5 +++++ esphome/components/lvgl/schemas.py | 19 ++++++++++++++++++- esphome/components/lvgl/widgets/__init__.py | 6 +++--- tests/components/lvgl/lvgl-package.yaml | 5 +++++ 4 files changed, 31 insertions(+), 4 deletions(-) diff --git a/esphome/components/lvgl/defines.py b/esphome/components/lvgl/defines.py index f2bcb6cc06..6b3b9c97ef 100644 --- a/esphome/components/lvgl/defines.py +++ b/esphome/components/lvgl/defines.py @@ -279,6 +279,8 @@ KEYBOARD_MODES = LvConstant( ) ROLLER_MODES = LvConstant("LV_ROLLER_MODE_", "NORMAL", "INFINITE") TILE_DIRECTIONS = DIRECTIONS.extend("HOR", "VER", "ALL") +SCROLL_DIRECTIONS = TILE_DIRECTIONS.extend("NONE") +SNAP_DIRECTIONS = LvConstant("LV_SCROLL_SNAP_", "NONE", "START", "END", "CENTER") CHILD_ALIGNMENTS = LvConstant( "LV_ALIGN_", "TOP_LEFT", @@ -511,6 +513,9 @@ CONF_ROLLOVER = "rollover" CONF_ROOT_BACK_BTN = "root_back_btn" CONF_SCALE_LINES = "scale_lines" CONF_SCROLLBAR_MODE = "scrollbar_mode" +CONF_SCROLL_DIR = "scroll_dir" +CONF_SCROLL_SNAP_X = "scroll_snap_x" +CONF_SCROLL_SNAP_Y = "scroll_snap_y" CONF_SELECTED_INDEX = "selected_index" CONF_SELECTED_TEXT = "selected_text" CONF_SHOW_SNOW = "show_snow" diff --git a/esphome/components/lvgl/schemas.py b/esphome/components/lvgl/schemas.py index b2d463c5fd..f2704f99de 100644 --- a/esphome/components/lvgl/schemas.py +++ b/esphome/components/lvgl/schemas.py @@ -20,7 +20,14 @@ from esphome.core import TimePeriod from esphome.core.config import StartupTrigger from . import defines as df, lv_validation as lvalid -from .defines import CONF_TIME_FORMAT, LV_GRAD_DIR +from .defines import ( + CONF_SCROLL_DIR, + CONF_SCROLL_SNAP_X, + CONF_SCROLL_SNAP_Y, + CONF_SCROLLBAR_MODE, + CONF_TIME_FORMAT, + LV_GRAD_DIR, +) from .helpers import CONF_IF_NAN, requires_component, validate_printf from .layout import ( FLEX_OBJ_SCHEMA, @@ -234,9 +241,19 @@ STYLE_SCHEMA = cv.Schema({cv.Optional(k): v for k, v in STYLE_PROPS.items()}).ex cv.Optional(df.CONF_SCROLLBAR_MODE): df.LvConstant( "LV_SCROLLBAR_MODE_", "OFF", "ON", "ACTIVE", "AUTO" ).one_of, + cv.Optional(CONF_SCROLL_DIR): df.SCROLL_DIRECTIONS.one_of, + cv.Optional(CONF_SCROLL_SNAP_X): df.SNAP_DIRECTIONS.one_of, + cv.Optional(CONF_SCROLL_SNAP_Y): df.SNAP_DIRECTIONS.one_of, } ) +OBJ_PROPERTIES = { + CONF_SCROLL_SNAP_X, + CONF_SCROLL_SNAP_Y, + CONF_SCROLL_DIR, + CONF_SCROLLBAR_MODE, +} + # Also allow widget specific properties for use in style definitions FULL_STYLE_SCHEMA = STYLE_SCHEMA.extend( { diff --git a/esphome/components/lvgl/widgets/__init__.py b/esphome/components/lvgl/widgets/__init__.py index 187b5828c2..2e7948522e 100644 --- a/esphome/components/lvgl/widgets/__init__.py +++ b/esphome/components/lvgl/widgets/__init__.py @@ -21,7 +21,6 @@ from ..defines import ( CONF_MAIN, CONF_PAD_COLUMN, CONF_PAD_ROW, - CONF_SCROLLBAR_MODE, CONF_STYLES, CONF_WIDGETS, OBJ_FLAGS, @@ -45,7 +44,7 @@ from ..lvcode import ( lv_obj, lv_Pvariable, ) -from ..schemas import ALL_STYLES, STYLE_REMAP, WIDGET_TYPES +from ..schemas import ALL_STYLES, OBJ_PROPERTIES, STYLE_REMAP, WIDGET_TYPES from ..types import LV_STATE, LvType, WidgetType, lv_coord_t, lv_obj_t, lv_obj_t_ptr EVENT_LAMB = "event_lamb__" @@ -414,7 +413,8 @@ async def set_obj_properties(w: Widget, config): w.add_state(state) cond.else_() w.clear_state(state) - await w.set_property(CONF_SCROLLBAR_MODE, config, lv_name="obj") + for property in OBJ_PROPERTIES: + await w.set_property(property, config, lv_name="obj") async def add_widgets(parent: Widget, config: dict): diff --git a/tests/components/lvgl/lvgl-package.yaml b/tests/components/lvgl/lvgl-package.yaml index 708dfa2cb1..eddcbe9fd5 100644 --- a/tests/components/lvgl/lvgl-package.yaml +++ b/tests/components/lvgl/lvgl-package.yaml @@ -537,6 +537,9 @@ lvgl: - tileview: id: tileview_id scrollbar_mode: active + scroll_dir: all + scroll_elastic: true + scroll_momentum: true on_value: then: - if: @@ -546,6 +549,8 @@ lvgl: - logger.log: "tile 1 is now showing" tiles: - id: tile_1 + scroll_snap_y: center + scroll_snap_x: start layout: vertical row: 0 column: 0 From 1207b9e99532733cb77597384c47876c3358a1f4 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Wed, 26 Nov 2025 11:53:51 +1000 Subject: [PATCH 306/415] [lvgl] Automatically pad rows and columns (#11879) Co-authored-by: J. Nick Koston --- esphome/components/lvgl/layout.py | 6 +++++- tests/components/lvgl/lvgl-package.yaml | 2 ++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/esphome/components/lvgl/layout.py b/esphome/components/lvgl/layout.py index caa503ef0d..b27a0b54a2 100644 --- a/esphome/components/lvgl/layout.py +++ b/esphome/components/lvgl/layout.py @@ -172,10 +172,14 @@ class DirectionalLayout(FlexLayout): def validate(self, config): assert config[CONF_LAYOUT].lower() == self.direction - config[CONF_LAYOUT] = { + layout = { **FLEX_HV_STYLE, CONF_FLEX_FLOW: "LV_FLEX_FLOW_" + self.flow.upper(), } + if pad_all := config.get("pad_all"): + layout[CONF_PAD_ROW] = pad_all + layout[CONF_PAD_COLUMN] = pad_all + config[CONF_LAYOUT] = layout return config diff --git a/tests/components/lvgl/lvgl-package.yaml b/tests/components/lvgl/lvgl-package.yaml index eddcbe9fd5..30866a603c 100644 --- a/tests/components/lvgl/lvgl-package.yaml +++ b/tests/components/lvgl/lvgl-package.yaml @@ -552,6 +552,7 @@ lvgl: scroll_snap_y: center scroll_snap_x: start layout: vertical + pad_all: 6px row: 0 column: 0 dir: ALL @@ -1049,6 +1050,7 @@ lvgl: opa: 0% - id: page3 layout: Horizontal + pad_all: 6px widgets: - keyboard: id: lv_keyboard From b328758634676b84839f13ad2f70469351b976dd Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 26 Nov 2025 10:53:44 -0600 Subject: [PATCH 307/415] Revert "[core] Deduplicate identical stateless lambdas to reduce flash usage" (#12117) --- esphome/cpp_generator.py | 177 +-------------- tests/component_tests/text/test_text.py | 19 +- tests/unit_tests/test_lambda_dedup.py | 286 ------------------------ 3 files changed, 11 insertions(+), 471 deletions(-) delete mode 100644 tests/unit_tests/test_lambda_dedup.py diff --git a/esphome/cpp_generator.py b/esphome/cpp_generator.py index 4f91696ca1..6f1af01a5b 100644 --- a/esphome/cpp_generator.py +++ b/esphome/cpp_generator.py @@ -19,21 +19,11 @@ from esphome.core import ( TimePeriodNanoseconds, TimePeriodSeconds, ) -from esphome.coroutine import CoroPriority, coroutine_with_priority from esphome.helpers import cpp_string_escape, indent_all_but_first_and_last from esphome.types import Expression, SafeExpType, TemplateArgsType from esphome.util import OrderedDict from esphome.yaml_util import ESPHomeDataBase -# Keys for lambda deduplication storage in CORE.data -_KEY_LAMBDA_DEDUP = "lambda_dedup" -_KEY_LAMBDA_DEDUP_DECLARATIONS = "lambda_dedup_declarations" - -# Regex patterns for static variable detection (compiled once) -_RE_CPP_SINGLE_LINE_COMMENT = re.compile(r"//.*?$", re.MULTILINE) -_RE_CPP_MULTI_LINE_COMMENT = re.compile(r"/\*.*?\*/", re.DOTALL) -_RE_STATIC_VARIABLE = re.compile(r"\bstatic\s+(?!cast|assert|pointer_cast)\w+\s+\w+") - class RawExpression(Expression): __slots__ = ("text",) @@ -198,7 +188,7 @@ class LambdaExpression(Expression): def __init__( self, parts, parameters, capture: str = "=", return_type=None, source=None - ) -> None: + ): self.parts = parts if not isinstance(parameters, ParameterListExpression): parameters = ParameterListExpression(*parameters) @@ -207,21 +197,16 @@ class LambdaExpression(Expression): self.capture = capture self.return_type = safe_exp(return_type) if return_type is not None else None - def format_body(self) -> str: - """Format the lambda body with source directive and content.""" - body = "" - if self.source is not None: - body += f"{self.source.as_line_directive}\n" - body += self.content - return body - - def __str__(self) -> str: + def __str__(self): # Stateless lambdas (empty capture) implicitly convert to function pointers # when assigned to function pointer types - no unary + needed cpp = f"[{self.capture}]({self.parameters})" if self.return_type is not None: cpp += f" -> {self.return_type}" - cpp += f" {{\n{self.format_body()}\n}}" + cpp += " {\n" + if self.source is not None: + cpp += f"{self.source.as_line_directive}\n" + cpp += f"{self.content}\n}}" return indent_all_but_first_and_last(cpp) @property @@ -229,37 +214,6 @@ class LambdaExpression(Expression): return "".join(str(part) for part in self.parts) -class SharedFunctionLambdaExpression(LambdaExpression): - """A lambda expression that references a shared deduplicated function. - - This class wraps a function pointer but maintains the LambdaExpression - interface so calling code works unchanged. - """ - - __slots__ = ("_func_name",) - - def __init__( - self, - func_name: str, - parameters: TemplateArgsType, - return_type: SafeExpType | None = None, - ) -> None: - # Initialize parent with empty parts since we're just a function reference - super().__init__( - [], parameters, capture="", return_type=return_type, source=None - ) - self._func_name = func_name - - def __str__(self) -> str: - # Just return the function name - it's already a function pointer - return self._func_name - - @property - def content(self) -> str: - # No content, just a function reference - return "" - - # pylint: disable=abstract-method class Literal(Expression, metaclass=abc.ABCMeta): __slots__ = () @@ -629,25 +583,6 @@ def add_global(expression: SafeExpType | Statement, prepend: bool = False): CORE.add_global(expression, prepend) -@coroutine_with_priority(CoroPriority.FINAL) -async def flush_lambda_dedup_declarations() -> None: - """Flush all deferred lambda deduplication declarations to global scope. - - This is a coroutine that runs with FINAL priority (after all components) - to ensure all referenced variables are declared before the shared - lambda functions that use them. - """ - if _KEY_LAMBDA_DEDUP_DECLARATIONS not in CORE.data: - return - - declarations = CORE.data[_KEY_LAMBDA_DEDUP_DECLARATIONS] - for func_declaration in declarations: - add_global(RawStatement(func_declaration)) - - # Clear the list so we don't add them again - CORE.data[_KEY_LAMBDA_DEDUP_DECLARATIONS] = [] - - def add_library(name: str, version: str | None, repository: str | None = None): """Add a library to the codegen library storage. @@ -721,93 +656,6 @@ async def get_variable_with_full_id(id_: ID) -> tuple[ID, "MockObj"]: return await CORE.get_variable_with_full_id(id_) -def _has_static_variables(code: str) -> bool: - """Check if code contains static variable definitions. - - Static variables in lambdas should not be deduplicated because each lambda - instance should have its own static variable state. - - Args: - code: The lambda body code to check - - Returns: - True if code contains static variable definitions - """ - # Remove C++ comments to avoid false positives - # Remove single-line comments (// ...) - code_no_comments = _RE_CPP_SINGLE_LINE_COMMENT.sub("", code) - # Remove multi-line comments (/* ... */) - code_no_comments = _RE_CPP_MULTI_LINE_COMMENT.sub("", code_no_comments) - - # Match: static - # But not: static_cast, static_assert, static_pointer_cast - return bool(_RE_STATIC_VARIABLE.search(code_no_comments)) - - -def _get_shared_lambda_name(lambda_expr: LambdaExpression) -> str | None: - """Get the shared function name for a lambda expression. - - If an identical lambda was already generated, returns the existing shared - function name. Otherwise, creates a new shared function and returns its name. - - Lambdas with static variables are not deduplicated to preserve their - independent state. - - Args: - lambda_expr: The lambda expression to deduplicate - - Returns: - The name of the shared function for this lambda (either existing or newly created), - or None if the lambda should not be deduplicated (e.g., contains static variables) - """ - # Create a unique key from the lambda content, parameters, and return type - content = lambda_expr.content - - # Don't deduplicate lambdas with static variables - each instance needs its own state - if _has_static_variables(content): - return None - param_str = str(lambda_expr.parameters) - return_str = ( - str(lambda_expr.return_type) if lambda_expr.return_type is not None else "void" - ) - - # Use tuple of (content, params, return_type) as key - lambda_key = (content, param_str, return_str) - - # Initialize deduplication storage in CORE.data if not exists - if _KEY_LAMBDA_DEDUP not in CORE.data: - CORE.data[_KEY_LAMBDA_DEDUP] = {} - # Register the flush job to run after all components (FINAL priority) - # This ensures all variables are declared before shared lambda functions - CORE.add_job(flush_lambda_dedup_declarations) - - lambda_cache = CORE.data[_KEY_LAMBDA_DEDUP] - - # Check if we've seen this lambda before - if lambda_key in lambda_cache: - # Return name of existing shared function - return lambda_cache[lambda_key] - - # First occurrence - create a shared function - # Use the cache size as the function number - func_name = f"shared_lambda_{len(lambda_cache)}" - - # Build the function declaration using lambda's body formatting - func_declaration = ( - f"{return_str} {func_name}({param_str}) {{\n{lambda_expr.format_body()}\n}}" - ) - - # Store the declaration to be added later (after all variable declarations) - # We can't add it immediately because it might reference variables not yet declared - CORE.data.setdefault(_KEY_LAMBDA_DEDUP_DECLARATIONS, []).append(func_declaration) - - # Store in cache - lambda_cache[lambda_key] = func_name - - # Return the function name (this is the first occurrence, but we still generate shared function) - return func_name - - async def process_lambda( value: Lambda, parameters: TemplateArgsType, @@ -865,19 +713,6 @@ async def process_lambda( location.line += value.content_offset else: location = None - - # Lambda deduplication: Only deduplicate stateless lambdas (empty capture). - # Stateful lambdas cannot be shared as they capture different contexts. - # Lambdas with static variables are also not deduplicated to preserve independent state. - if capture == "": - lambda_expr = LambdaExpression( - parts, parameters, capture, return_type, location - ) - func_name = _get_shared_lambda_name(lambda_expr) - if func_name is not None: - # Return a shared function reference instead of inline lambda - return SharedFunctionLambdaExpression(func_name, parameters, return_type) - return LambdaExpression(parts, parameters, capture, return_type, location) diff --git a/tests/component_tests/text/test_text.py b/tests/component_tests/text/test_text.py index 56dee205b4..bfc3131f6d 100644 --- a/tests/component_tests/text/test_text.py +++ b/tests/component_tests/text/test_text.py @@ -1,6 +1,4 @@ -"""Tests for the text component.""" - -from esphome.core import CORE +"""Tests for the binary sensor component.""" def test_text_is_setup(generate_main): @@ -58,22 +56,15 @@ def test_text_config_value_mode_set(generate_main): assert "it_3->traits.set_mode(text::TEXT_MODE_PASSWORD);" in main_cpp -def test_text_config_lambda_is_set(generate_main) -> None: +def test_text_config_lamda_is_set(generate_main): """ - Test if lambda is set for lambda mode (optimized with stateless lambda and deduplication) + Test if lambda is set for lambda mode (optimized with stateless lambda) """ # Given # When main_cpp = generate_main("tests/component_tests/text/test_text.yaml") - # Get both global and main sections to find the shared lambda definition - full_cpp = CORE.cpp_global_section + main_cpp - # Then - # Lambda is deduplicated into a shared function (reference in main section) - assert "it_4->set_template(shared_lambda_" in main_cpp - # Lambda body should be in the code somewhere - assert 'return std::string{"Hello"};' in full_cpp - # Verify the shared lambda function is defined (in global section) - assert "esphome::optional shared_lambda_" in full_cpp + assert "it_4->set_template([]() -> esphome::optional {" in main_cpp + assert 'return std::string{"Hello"};' in main_cpp diff --git a/tests/unit_tests/test_lambda_dedup.py b/tests/unit_tests/test_lambda_dedup.py deleted file mode 100644 index bbf5f02e6d..0000000000 --- a/tests/unit_tests/test_lambda_dedup.py +++ /dev/null @@ -1,286 +0,0 @@ -"""Tests for lambda deduplication in cpp_generator.""" - -from esphome import cpp_generator as cg -from esphome.core import CORE - - -def test_deduplicate_identical_lambdas() -> None: - """Test that identical stateless lambdas are deduplicated.""" - # Create two identical lambda expressions - lambda1 = cg.LambdaExpression( - parts=["return 42;"], - parameters=[], - capture="", - return_type=cg.RawExpression("int"), - ) - - lambda2 = cg.LambdaExpression( - parts=["return 42;"], - parameters=[], - capture="", - return_type=cg.RawExpression("int"), - ) - - # Try to deduplicate them - func_name1 = cg._get_shared_lambda_name(lambda1) - func_name2 = cg._get_shared_lambda_name(lambda2) - - # Both should get the same function name (deduplication happened) - assert func_name1 == func_name2 - assert func_name1 == "shared_lambda_0" - - -def test_different_lambdas_not_deduplicated() -> None: - """Test that different lambdas get different function names.""" - lambda1 = cg.LambdaExpression( - parts=["return 42;"], - parameters=[], - capture="", - return_type=cg.RawExpression("int"), - ) - - lambda2 = cg.LambdaExpression( - parts=["return 24;"], # Different content - parameters=[], - capture="", - return_type=cg.RawExpression("int"), - ) - - func_name1 = cg._get_shared_lambda_name(lambda1) - func_name2 = cg._get_shared_lambda_name(lambda2) - - # Different lambdas should get different function names - assert func_name1 != func_name2 - assert func_name1 == "shared_lambda_0" - assert func_name2 == "shared_lambda_1" - - -def test_different_return_types_not_deduplicated() -> None: - """Test that lambdas with different return types are not deduplicated.""" - lambda1 = cg.LambdaExpression( - parts=["return 42;"], - parameters=[], - capture="", - return_type=cg.RawExpression("int"), - ) - - lambda2 = cg.LambdaExpression( - parts=["return 42;"], # Same content - parameters=[], - capture="", - return_type=cg.RawExpression("float"), # Different return type - ) - - func_name1 = cg._get_shared_lambda_name(lambda1) - func_name2 = cg._get_shared_lambda_name(lambda2) - - # Different return types = different functions - assert func_name1 != func_name2 - - -def test_different_parameters_not_deduplicated() -> None: - """Test that lambdas with different parameters are not deduplicated.""" - lambda1 = cg.LambdaExpression( - parts=["return x;"], - parameters=[("int", "x")], - capture="", - return_type=cg.RawExpression("int"), - ) - - lambda2 = cg.LambdaExpression( - parts=["return x;"], # Same content - parameters=[("float", "x")], # Different parameter type - capture="", - return_type=cg.RawExpression("int"), - ) - - func_name1 = cg._get_shared_lambda_name(lambda1) - func_name2 = cg._get_shared_lambda_name(lambda2) - - # Different parameters = different functions - assert func_name1 != func_name2 - - -def test_flush_lambda_dedup_declarations() -> None: - """Test that deferred declarations are properly stored for later flushing.""" - # Create a lambda which will create a deferred declaration - lambda1 = cg.LambdaExpression( - parts=["return 42;"], - parameters=[], - capture="", - return_type=cg.RawExpression("int"), - ) - - cg._get_shared_lambda_name(lambda1) - - # Check that declaration was stored - assert cg._KEY_LAMBDA_DEDUP_DECLARATIONS in CORE.data - assert len(CORE.data[cg._KEY_LAMBDA_DEDUP_DECLARATIONS]) == 1 - - # Verify the declaration content is correct - declaration = CORE.data[cg._KEY_LAMBDA_DEDUP_DECLARATIONS][0] - assert "shared_lambda_0" in declaration - assert "return 42;" in declaration - - # Note: The actual flushing happens via CORE.add_job with FINAL priority - # during real code generation, so we don't test that here - - -def test_shared_function_lambda_expression() -> None: - """Test SharedFunctionLambdaExpression behaves correctly.""" - shared_lambda = cg.SharedFunctionLambdaExpression( - func_name="shared_lambda_0", - parameters=[], - return_type=cg.RawExpression("int"), - ) - - # Should output just the function name - assert str(shared_lambda) == "shared_lambda_0" - - # Should have empty capture (stateless) - assert shared_lambda.capture == "" - - # Should have empty content (just a reference) - assert shared_lambda.content == "" - - -def test_lambda_deduplication_counter() -> None: - """Test that lambda counter increments correctly.""" - # Create 3 different lambdas - for i in range(3): - lambda_expr = cg.LambdaExpression( - parts=[f"return {i};"], - parameters=[], - capture="", - return_type=cg.RawExpression("int"), - ) - func_name = cg._get_shared_lambda_name(lambda_expr) - assert func_name == f"shared_lambda_{i}" - - -def test_lambda_format_body() -> None: - """Test that format_body correctly formats lambda body with source.""" - # Without source - lambda1 = cg.LambdaExpression( - parts=["return 42;"], - parameters=[], - capture="", - return_type=None, - source=None, - ) - assert lambda1.format_body() == "return 42;" - - # With source would need a proper source object, skip for now - - -def test_stateful_lambdas_not_deduplicated() -> None: - """Test that stateful lambdas (non-empty capture) are not deduplicated.""" - # _get_shared_lambda_name is only called for stateless lambdas (capture == "") - # Stateful lambdas bypass deduplication entirely in process_lambda - - # Verify that a stateful lambda would NOT get deduplicated - # by checking it's not in the stateless dedup cache - stateful_lambda = cg.LambdaExpression( - parts=["return x + y;"], - parameters=[], - capture="=", # Non-empty capture means stateful - return_type=cg.RawExpression("int"), - ) - - # Stateful lambdas should NOT be passed to _get_shared_lambda_name - # This is enforced by the `if capture == ""` check in process_lambda - # We verify the lambda has a non-empty capture - assert stateful_lambda.capture != "" - assert stateful_lambda.capture == "=" - - -def test_static_variable_detection() -> None: - """Test detection of static variables in lambda code.""" - # Should detect static variables - assert cg._has_static_variables("static int counter = 0;") - assert cg._has_static_variables("static bool flag = false; return flag;") - assert cg._has_static_variables(" static float value = 1.0; ") - - # Should NOT detect static_cast, static_assert, etc. (with underscores) - assert not cg._has_static_variables("return static_cast(value);") - assert not cg._has_static_variables("static_assert(sizeof(int) == 4);") - assert not cg._has_static_variables("auto ptr = static_pointer_cast(bar);") - - # Edge case: 'cast', 'assert', 'pointer_cast' are NOT C++ keywords - # Someone could use them as type names, but we should NOT flag them - # because they're not actually static variables with state - # NOTE: These are valid C++ but extremely unlikely in ESPHome lambdas - assert not cg._has_static_variables("static cast obj;") # 'cast' as type name - assert not cg._has_static_variables("static assert value;") # 'assert' as type name - assert not cg._has_static_variables( - "static pointer_cast ptr;" - ) # 'pointer_cast' as type - - # Should NOT detect in comments - assert not cg._has_static_variables("// static int x = 0;\nreturn 42;") - assert not cg._has_static_variables("/* static int y = 0; */ return 42;") - - # Should detect even with comments elsewhere - assert cg._has_static_variables("// comment\nstatic int x = 0;\nreturn x;") - - # Should NOT detect non-static code - assert not cg._has_static_variables("int counter = 0; return counter++;") - assert not cg._has_static_variables("return 42;") - - # Should handle newlines between static and type/variable - assert cg._has_static_variables("static int\nfoo = 0;") - assert cg._has_static_variables("static\nint\nbar = 0;") - assert cg._has_static_variables( - "static int \n foo = 0;" - ) # Mixed spaces/newlines - - -def test_lambdas_with_static_not_deduplicated() -> None: - """Test that lambdas with static variables are not deduplicated.""" - # Two identical lambdas with static variables - lambda1 = cg.LambdaExpression( - parts=["static int counter = 0; return counter++;"], - parameters=[], - capture="", - return_type=cg.RawExpression("int"), - ) - - lambda2 = cg.LambdaExpression( - parts=["static int counter = 0; return counter++;"], - parameters=[], - capture="", - return_type=cg.RawExpression("int"), - ) - - # Should return None (not deduplicated) - func_name1 = cg._get_shared_lambda_name(lambda1) - func_name2 = cg._get_shared_lambda_name(lambda2) - - assert func_name1 is None - assert func_name2 is None - - -def test_lambdas_without_static_still_deduplicated() -> None: - """Test that lambdas without static variables are still deduplicated.""" - # Two identical lambdas WITHOUT static variables - lambda1 = cg.LambdaExpression( - parts=["int counter = 0; return counter++;"], # No static - parameters=[], - capture="", - return_type=cg.RawExpression("int"), - ) - - lambda2 = cg.LambdaExpression( - parts=["int counter = 0; return counter++;"], # No static - parameters=[], - capture="", - return_type=cg.RawExpression("int"), - ) - - # Should be deduplicated (same function name) - func_name1 = cg._get_shared_lambda_name(lambda1) - func_name2 = cg._get_shared_lambda_name(lambda2) - - assert func_name1 is not None - assert func_name2 is not None - assert func_name1 == func_name2 From 12a51ff047641d7ecdc3ec1a5b532ba8bdf5e49a Mon Sep 17 00:00:00 2001 From: Javier Peletier Date: Wed, 26 Nov 2025 18:00:44 +0100 Subject: [PATCH 308/415] [packages] Fix package schema validation (#12116) Co-authored-by: J. Nick Koston --- esphome/components/packages/__init__.py | 70 ++++++++++++++----- .../component_tests/packages/test_packages.py | 39 +++++++++-- 2 files changed, 87 insertions(+), 22 deletions(-) diff --git a/esphome/components/packages/__init__.py b/esphome/components/packages/__init__.py index 04057c07f2..41cde0391b 100644 --- a/esphome/components/packages/__init__.py +++ b/esphome/components/packages/__init__.py @@ -1,3 +1,4 @@ +import logging from pathlib import Path from esphome import git, yaml_util @@ -20,18 +21,41 @@ from esphome.const import ( ) from esphome.core import EsphomeError +_LOGGER = logging.getLogger(__name__) + DOMAIN = CONF_PACKAGES -def validate_git_package(config: dict): - if CONF_URL not in config: - return config - config = BASE_SCHEMA(config) - new_config = config +def valid_package_contents(package_config: dict): + """Validates that a package_config that will be merged looks as much as possible to a valid config + to fail early on obvious mistakes.""" + if isinstance(package_config, dict): + if CONF_URL in package_config: + # If a URL key is found, then make sure the config conforms to a remote package schema: + return REMOTE_PACKAGE_SCHEMA(package_config) + + # Validate manually since Voluptuous would regenerate dicts and lose metadata + # such as ESPHomeDataBase + for k, v in package_config.items(): + if not isinstance(k, str): + raise cv.Invalid("Package content keys must be strings") + if isinstance(v, (dict, list)): + continue # e.g. script: [] or logger: {level: debug} + if v is None: + continue # e.g. web_server: + raise cv.Invalid("Invalid component content in package definition") + return package_config + + raise cv.Invalid("Package contents must be a dict") + + +def expand_file_to_files(config: dict): if CONF_FILE in config: + new_config = config new_config[CONF_FILES] = [config[CONF_FILE]] del new_config[CONF_FILE] - return new_config + return new_config + return config def validate_yaml_filename(value): @@ -45,7 +69,7 @@ def validate_yaml_filename(value): def validate_source_shorthand(value): if not isinstance(value, str): - raise cv.Invalid("Shorthand only for strings") + raise cv.Invalid("Git URL shorthand only for strings") git_file = git.GitFile.from_shorthand(value) @@ -56,10 +80,17 @@ def validate_source_shorthand(value): if git_file.ref: conf[CONF_REF] = git_file.ref - return BASE_SCHEMA(conf) + return REMOTE_PACKAGE_SCHEMA(conf) -BASE_SCHEMA = cv.All( +def deprecate_single_package(config): + _LOGGER.warning( + "Including a single package under `packages:` is deprecated. Use a list instead." + ) + return config + + +REMOTE_PACKAGE_SCHEMA = cv.All( cv.Schema( { cv.Required(CONF_URL): cv.url, @@ -90,23 +121,30 @@ BASE_SCHEMA = cv.All( } ), cv.has_at_least_one_key(CONF_FILE, CONF_FILES), + expand_file_to_files, ) -PACKAGE_SCHEMA = cv.All( - cv.Any(validate_source_shorthand, BASE_SCHEMA, dict), validate_git_package +PACKAGE_SCHEMA = cv.Any( # A package definition is either: + validate_source_shorthand, # A git URL shorthand string that expands to a remote package schema, or + REMOTE_PACKAGE_SCHEMA, # a valid remote package schema, or + valid_package_contents, # Something that at least looks like an actual package, e.g. {wifi:{ssid: xxx}} + # which will have to be fully validated later as per each component's schema. ) -CONFIG_SCHEMA = cv.Any( +CONFIG_SCHEMA = cv.Any( # under `packages:` we can have either: cv.Schema( { - str: PACKAGE_SCHEMA, + str: PACKAGE_SCHEMA, # a named dict of package definitions, or } ), - [PACKAGE_SCHEMA], + [PACKAGE_SCHEMA], # a list of package definitions, or + cv.All( # a single package definition (deprecated) + cv.ensure_list(PACKAGE_SCHEMA), deprecate_single_package + ), ) -def _process_base_package(config: dict, skip_update: bool = False) -> dict: +def _process_remote_package(config: dict, skip_update: bool = False) -> dict: # When skip_update is True, use NEVER_REFRESH to prevent updates actual_refresh = git.NEVER_REFRESH if skip_update else config[CONF_REFRESH] repo_dir, revert = git.clone_or_update( @@ -185,7 +223,7 @@ def _process_base_package(config: dict, skip_update: bool = False) -> dict: def _process_package(package_config, config, skip_update: bool = False): recursive_package = package_config if CONF_URL in package_config: - package_config = _process_base_package(package_config, skip_update) + package_config = _process_remote_package(package_config, skip_update) if isinstance(package_config, dict): recursive_package = do_packages_pass(package_config, skip_update) return merge_config(recursive_package, config) diff --git a/tests/component_tests/packages/test_packages.py b/tests/component_tests/packages/test_packages.py index 1c4c91aa52..ac4e211fe6 100644 --- a/tests/component_tests/packages/test_packages.py +++ b/tests/component_tests/packages/test_packages.py @@ -95,7 +95,7 @@ def test_package_invalid_dict(basic_esphome, basic_wifi): @pytest.mark.parametrize( - "package", + "packages", [ {"package1": "github://esphome/non-existant-repo/file1.yml@main"}, {"package2": "github://esphome/non-existant-repo/file1.yml"}, @@ -107,12 +107,12 @@ def test_package_invalid_dict(basic_esphome, basic_wifi): ], ], ) -def test_package_shorthand(package): - CONFIG_SCHEMA(package) +def test_package_shorthand(packages): + CONFIG_SCHEMA(packages) @pytest.mark.parametrize( - "package", + "packages", [ # not github {"package1": "someplace://esphome/non-existant-repo/file1.yml@main"}, @@ -133,9 +133,9 @@ def test_package_shorthand(package): [3], ], ) -def test_package_invalid(package): +def test_package_invalid(packages): with pytest.raises(cv.Invalid): - CONFIG_SCHEMA(package) + CONFIG_SCHEMA(packages) def test_package_include(basic_wifi, basic_esphome): @@ -155,6 +155,33 @@ def test_package_include(basic_wifi, basic_esphome): assert actual == expected +def test_single_package( + basic_esphome, + basic_wifi, + caplog: pytest.LogCaptureFixture, +): + """ + Tests the simple case where a single package is added to the top-level config as is. + In this test, the CONF_WIFI config is expected to be simply added to the top-level config. + This tests the case where the user just put packages: !include package.yaml, not + part of a list or mapping of packages. + This behavior is deprecated, the test also checks if a warning is issued. + """ + config = {CONF_ESPHOME: basic_esphome, CONF_PACKAGES: {CONF_WIFI: basic_wifi}} + + expected = {CONF_ESPHOME: basic_esphome, CONF_WIFI: basic_wifi} + + with caplog.at_level("WARNING"): + actual = packages_pass(config) + + assert actual == expected + + assert ( + "Including a single package under `packages:` is deprecated. Use a list instead." + in caplog.text + ) + + def test_package_append(basic_wifi, basic_esphome): """ Tests the case where a key is present in both a package and top-level config. From 083886c4b05a95aa117ba4dfafac17b45908b681 Mon Sep 17 00:00:00 2001 From: Pawelo <81100874+pgolawsk@users.noreply.github.com> Date: Wed, 26 Nov 2025 19:06:51 +0100 Subject: [PATCH 309/415] [prometheus] Avoid generating unused light color metrics to reduce memory usage on ESP8266 (#9530) Co-authored-by: J. Nick Koston Co-authored-by: J. Nick Koston Co-authored-by: J. Nick Koston --- .../prometheus/prometheus_handler.cpp | 135 ++++++++---------- .../prometheus/prometheus_handler.h | 8 ++ tests/components/prometheus/common.yaml | 27 ++++ 3 files changed, 92 insertions(+), 78 deletions(-) diff --git a/esphome/components/prometheus/prometheus_handler.cpp b/esphome/components/prometheus/prometheus_handler.cpp index 812b547860..252b477400 100644 --- a/esphome/components/prometheus/prometheus_handler.cpp +++ b/esphome/components/prometheus/prometheus_handler.cpp @@ -141,6 +141,24 @@ void PrometheusHandler::add_friendly_name_label_(AsyncResponseStream *stream, st } } +#ifdef USE_ESP8266 +void PrometheusHandler::print_metric_labels_(AsyncResponseStream *stream, const __FlashStringHelper *metric_name, + EntityBase *obj, std::string &area, std::string &node, + std::string &friendly_name) { +#else +void PrometheusHandler::print_metric_labels_(AsyncResponseStream *stream, const char *metric_name, EntityBase *obj, + std::string &area, std::string &node, std::string &friendly_name) { +#endif + stream->print(metric_name); + stream->print(ESPHOME_F("{id=\"")); + stream->print(relabel_id_(obj).c_str()); + add_area_label_(stream, area); + add_node_label_(stream, node); + add_friendly_name_label_(stream, friendly_name); + stream->print(ESPHOME_F("\",name=\"")); + stream->print(relabel_name_(obj).c_str()); +} + // Type-specific implementation #ifdef USE_SENSOR void PrometheusHandler::sensor_type_(AsyncResponseStream *stream) { @@ -303,13 +321,7 @@ void PrometheusHandler::light_row_(AsyncResponseStream *stream, light::LightStat if (obj->is_internal() && !this->include_internal_) return; // State - stream->print(ESPHOME_F("esphome_light_state{id=\"")); - stream->print(relabel_id_(obj).c_str()); - add_area_label_(stream, area); - add_node_label_(stream, node); - add_friendly_name_label_(stream, friendly_name); - stream->print(ESPHOME_F("\",name=\"")); - stream->print(relabel_name_(obj).c_str()); + print_metric_labels_(stream, ESPHOME_F("esphome_light_state"), obj, area, node, friendly_name); stream->print(ESPHOME_F("\"} ")); stream->print(obj->remote_values.is_on()); stream->print(ESPHOME_F("\n")); @@ -318,78 +330,45 @@ void PrometheusHandler::light_row_(AsyncResponseStream *stream, light::LightStat float brightness, r, g, b, w; color.as_brightness(&brightness); color.as_rgbw(&r, &g, &b, &w); - stream->print(ESPHOME_F("esphome_light_color{id=\"")); - stream->print(relabel_id_(obj).c_str()); - add_area_label_(stream, area); - add_node_label_(stream, node); - add_friendly_name_label_(stream, friendly_name); - stream->print(ESPHOME_F("\",name=\"")); - stream->print(relabel_name_(obj).c_str()); - stream->print(ESPHOME_F("\",channel=\"brightness\"} ")); - stream->print(brightness); - stream->print(ESPHOME_F("\n")); - stream->print(ESPHOME_F("esphome_light_color{id=\"")); - stream->print(relabel_id_(obj).c_str()); - add_area_label_(stream, area); - add_node_label_(stream, node); - add_friendly_name_label_(stream, friendly_name); - stream->print(ESPHOME_F("\",name=\"")); - stream->print(relabel_name_(obj).c_str()); - stream->print(ESPHOME_F("\",channel=\"r\"} ")); - stream->print(r); - stream->print(ESPHOME_F("\n")); - stream->print(ESPHOME_F("esphome_light_color{id=\"")); - stream->print(relabel_id_(obj).c_str()); - add_area_label_(stream, area); - add_node_label_(stream, node); - add_friendly_name_label_(stream, friendly_name); - stream->print(ESPHOME_F("\",name=\"")); - stream->print(relabel_name_(obj).c_str()); - stream->print(ESPHOME_F("\",channel=\"g\"} ")); - stream->print(g); - stream->print(ESPHOME_F("\n")); - stream->print(ESPHOME_F("esphome_light_color{id=\"")); - stream->print(relabel_id_(obj).c_str()); - add_area_label_(stream, area); - add_node_label_(stream, node); - add_friendly_name_label_(stream, friendly_name); - stream->print(ESPHOME_F("\",name=\"")); - stream->print(relabel_name_(obj).c_str()); - stream->print(ESPHOME_F("\",channel=\"b\"} ")); - stream->print(b); - stream->print(ESPHOME_F("\n")); - stream->print(ESPHOME_F("esphome_light_color{id=\"")); - stream->print(relabel_id_(obj).c_str()); - add_area_label_(stream, area); - add_node_label_(stream, node); - add_friendly_name_label_(stream, friendly_name); - stream->print(ESPHOME_F("\",name=\"")); - stream->print(relabel_name_(obj).c_str()); - stream->print(ESPHOME_F("\",channel=\"w\"} ")); - stream->print(w); - stream->print(ESPHOME_F("\n")); - // Effect - std::string effect = obj->get_effect_name(); - if (effect == "None") { - stream->print(ESPHOME_F("esphome_light_effect_active{id=\"")); - stream->print(relabel_id_(obj).c_str()); - add_area_label_(stream, area); - add_node_label_(stream, node); - add_friendly_name_label_(stream, friendly_name); - stream->print(ESPHOME_F("\",name=\"")); - stream->print(relabel_name_(obj).c_str()); - stream->print(ESPHOME_F("\",effect=\"None\"} 0\n")); - } else { - stream->print(ESPHOME_F("esphome_light_effect_active{id=\"")); - stream->print(relabel_id_(obj).c_str()); - add_area_label_(stream, area); - add_node_label_(stream, node); - add_friendly_name_label_(stream, friendly_name); - stream->print(ESPHOME_F("\",name=\"")); - stream->print(relabel_name_(obj).c_str()); + if (obj->get_traits().supports_color_capability(light::ColorCapability::BRIGHTNESS)) { + print_metric_labels_(stream, ESPHOME_F("esphome_light_color"), obj, area, node, friendly_name); + stream->print(ESPHOME_F("\",channel=\"brightness\"} ")); + stream->print(brightness); + stream->print(ESPHOME_F("\n")); + } + if (obj->get_traits().supports_color_capability(light::ColorCapability::RGB)) { + print_metric_labels_(stream, ESPHOME_F("esphome_light_color"), obj, area, node, friendly_name); + stream->print(ESPHOME_F("\",channel=\"r\"} ")); + stream->print(r); + stream->print(ESPHOME_F("\n")); + print_metric_labels_(stream, ESPHOME_F("esphome_light_color"), obj, area, node, friendly_name); + stream->print(ESPHOME_F("\",channel=\"g\"} ")); + stream->print(g); + stream->print(ESPHOME_F("\n")); + print_metric_labels_(stream, ESPHOME_F("esphome_light_color"), obj, area, node, friendly_name); + stream->print(ESPHOME_F("\",channel=\"b\"} ")); + stream->print(b); + stream->print(ESPHOME_F("\n")); + } + if (obj->get_traits().supports_color_capability(light::ColorCapability::WHITE)) { + print_metric_labels_(stream, ESPHOME_F("esphome_light_color"), obj, area, node, friendly_name); + stream->print(ESPHOME_F("\",channel=\"w\"} ")); + stream->print(w); + stream->print(ESPHOME_F("\n")); + } + // Skip effect metrics if light has no effects + if (!obj->get_effects().empty()) { + // Effect + std::string effect = obj->get_effect_name(); + print_metric_labels_(stream, ESPHOME_F("esphome_light_effect_active"), obj, area, node, friendly_name); stream->print(ESPHOME_F("\",effect=\"")); - stream->print(effect.c_str()); - stream->print(ESPHOME_F("\"} 1\n")); + // Only vary based on effect + if (effect == "None") { + stream->print(ESPHOME_F("None\"} 0\n")); + } else { + stream->print(effect.c_str()); + stream->print(ESPHOME_F("\"} 1\n")); + } } } #endif diff --git a/esphome/components/prometheus/prometheus_handler.h b/esphome/components/prometheus/prometheus_handler.h index 45cc81b899..24243c8c98 100644 --- a/esphome/components/prometheus/prometheus_handler.h +++ b/esphome/components/prometheus/prometheus_handler.h @@ -66,6 +66,14 @@ class PrometheusHandler : public AsyncWebHandler, public Component { void add_area_label_(AsyncResponseStream *stream, std::string &area); void add_node_label_(AsyncResponseStream *stream, std::string &node); void add_friendly_name_label_(AsyncResponseStream *stream, std::string &friendly_name); + /// Print metric name and common labels (id, area, node, friendly_name, name) +#ifdef USE_ESP8266 + void print_metric_labels_(AsyncResponseStream *stream, const __FlashStringHelper *metric_name, EntityBase *obj, + std::string &area, std::string &node, std::string &friendly_name); +#else + void print_metric_labels_(AsyncResponseStream *stream, const char *metric_name, EntityBase *obj, std::string &area, + std::string &node, std::string &friendly_name); +#endif #ifdef USE_SENSOR /// Return the type for prometheus diff --git a/tests/components/prometheus/common.yaml b/tests/components/prometheus/common.yaml index 0b90d614dd..7ff416dccb 100644 --- a/tests/components/prometheus/common.yaml +++ b/tests/components/prometheus/common.yaml @@ -112,6 +112,25 @@ cover: } return COVER_CLOSED; +light: + - platform: binary + name: "Binary Light" + output: test_output + - platform: monochromatic + name: "Brightness Light" + output: test_output + - platform: rgb + name: "RGB Light" + red: test_output + green: test_output + blue: test_output + - platform: rgbw + name: "RGBW Light" + red: test_output + green: test_output + blue: test_output + white: test_output + lock: - platform: template id: template_lock1 @@ -122,6 +141,14 @@ lock: return LOCK_STATE_UNLOCKED; optimistic: true +output: + - platform: template + id: test_output + type: float + write_action: + - lambda: |- + // no-op for CI/build tests + (void)state; select: - platform: template id: template_select1 From 1d59c7a838578c0055499cdeb3f2006a8908eba1 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 26 Nov 2025 12:08:49 -0600 Subject: [PATCH 310/415] [scheduler] Fix use-after-move crash in heap operations --- esphome/core/scheduler.cpp | 24 ++++++++++++------------ esphome/core/scheduler.h | 4 +++- 2 files changed, 15 insertions(+), 13 deletions(-) diff --git a/esphome/core/scheduler.cpp b/esphome/core/scheduler.cpp index 09d50ee7c8..ae486ea3b0 100644 --- a/esphome/core/scheduler.cpp +++ b/esphome/core/scheduler.cpp @@ -359,8 +359,7 @@ void HOT Scheduler::call(uint32_t now) { std::unique_ptr item; { LockGuard guard{this->lock_}; - item = std::move(this->items_[0]); - this->pop_raw_(); + item = this->pop_raw_(); } const char *name = item->get_name(); @@ -401,7 +400,7 @@ void HOT Scheduler::call(uint32_t now) { // Don't run on failed components if (item->component != nullptr && item->component->is_failed()) { LockGuard guard{this->lock_}; - this->pop_raw_(); + this->recycle_item_(this->pop_raw_()); continue; } @@ -414,7 +413,7 @@ void HOT Scheduler::call(uint32_t now) { { LockGuard guard{this->lock_}; if (is_item_removed_(item.get())) { - this->pop_raw_(); + this->recycle_item_(this->pop_raw_()); this->to_remove_--; continue; } @@ -423,7 +422,7 @@ void HOT Scheduler::call(uint32_t now) { // Single-threaded or multi-threaded with atomics: can check without lock if (is_item_removed_(item.get())) { LockGuard guard{this->lock_}; - this->pop_raw_(); + this->recycle_item_(this->pop_raw_()); this->to_remove_--; continue; } @@ -443,14 +442,14 @@ void HOT Scheduler::call(uint32_t now) { LockGuard guard{this->lock_}; - auto executed_item = std::move(this->items_[0]); // Only pop after function call, this ensures we were reachable // during the function call and know if we were cancelled. - this->pop_raw_(); + auto executed_item = this->pop_raw_(); if (executed_item->remove) { - // We were removed/cancelled in the function call, stop + // We were removed/cancelled in the function call, recycle and continue this->to_remove_--; + this->recycle_item_(std::move(executed_item)); continue; } @@ -510,17 +509,18 @@ size_t HOT Scheduler::cleanup_() { if (!item->remove) break; this->to_remove_--; - this->pop_raw_(); + this->recycle_item_(this->pop_raw_()); } return this->items_.size(); } -void HOT Scheduler::pop_raw_() { +std::unique_ptr HOT Scheduler::pop_raw_() { std::pop_heap(this->items_.begin(), this->items_.end(), SchedulerItem::cmp); - // Instead of destroying, recycle the item - this->recycle_item_(std::move(this->items_.back())); + // Move the item out before popping - this is the item that was at the front of the heap + auto item = std::move(this->items_.back()); this->items_.pop_back(); + return item; } // Helper to execute a scheduler item diff --git a/esphome/core/scheduler.h b/esphome/core/scheduler.h index bea1503df0..476681a787 100644 --- a/esphome/core/scheduler.h +++ b/esphome/core/scheduler.h @@ -219,7 +219,9 @@ class Scheduler { // Returns the number of items remaining after cleanup // IMPORTANT: This method should only be called from the main thread (loop task). size_t cleanup_(); - void pop_raw_(); + // Remove and return the front item from the heap + // IMPORTANT: Caller must hold the scheduler lock before calling this function. + std::unique_ptr pop_raw_(); private: // Helper to cancel items by name - must be called with lock held From 877d2b914c11d4a0ef7b163be1c67e724d6d4bfb Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 26 Nov 2025 16:38:39 -0600 Subject: [PATCH 311/415] tweaks --- esphome/core/scheduler.cpp | 16 ++++++++-------- esphome/core/scheduler.h | 2 +- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/esphome/core/scheduler.cpp b/esphome/core/scheduler.cpp index ae486ea3b0..352587bf10 100644 --- a/esphome/core/scheduler.cpp +++ b/esphome/core/scheduler.cpp @@ -359,7 +359,7 @@ void HOT Scheduler::call(uint32_t now) { std::unique_ptr item; { LockGuard guard{this->lock_}; - item = this->pop_raw_(); + item = this->pop_raw_locked_(); } const char *name = item->get_name(); @@ -400,7 +400,7 @@ void HOT Scheduler::call(uint32_t now) { // Don't run on failed components if (item->component != nullptr && item->component->is_failed()) { LockGuard guard{this->lock_}; - this->recycle_item_(this->pop_raw_()); + this->recycle_item_(this->pop_raw_locked_()); continue; } @@ -413,7 +413,7 @@ void HOT Scheduler::call(uint32_t now) { { LockGuard guard{this->lock_}; if (is_item_removed_(item.get())) { - this->recycle_item_(this->pop_raw_()); + this->recycle_item_(this->pop_raw_locked_()); this->to_remove_--; continue; } @@ -422,7 +422,7 @@ void HOT Scheduler::call(uint32_t now) { // Single-threaded or multi-threaded with atomics: can check without lock if (is_item_removed_(item.get())) { LockGuard guard{this->lock_}; - this->recycle_item_(this->pop_raw_()); + this->recycle_item_(this->pop_raw_locked_()); this->to_remove_--; continue; } @@ -444,7 +444,7 @@ void HOT Scheduler::call(uint32_t now) { // Only pop after function call, this ensures we were reachable // during the function call and know if we were cancelled. - auto executed_item = this->pop_raw_(); + auto executed_item = this->pop_raw_locked_(); if (executed_item->remove) { // We were removed/cancelled in the function call, recycle and continue @@ -496,7 +496,7 @@ size_t HOT Scheduler::cleanup_() { return this->items_.size(); // We must hold the lock for the entire cleanup operation because: - // 1. We're modifying items_ (via pop_raw_) which requires exclusive access + // 1. We're modifying items_ (via pop_raw_locked_) which requires exclusive access // 2. We're decrementing to_remove_ which is also modified by other threads // (though all modifications are already under lock) // 3. Other threads read items_ when searching for items to cancel in cancel_item_locked_() @@ -509,11 +509,11 @@ size_t HOT Scheduler::cleanup_() { if (!item->remove) break; this->to_remove_--; - this->recycle_item_(this->pop_raw_()); + this->recycle_item_(this->pop_raw_locked_()); } return this->items_.size(); } -std::unique_ptr HOT Scheduler::pop_raw_() { +std::unique_ptr HOT Scheduler::pop_raw_locked_() { std::pop_heap(this->items_.begin(), this->items_.end(), SchedulerItem::cmp); // Move the item out before popping - this is the item that was at the front of the heap diff --git a/esphome/core/scheduler.h b/esphome/core/scheduler.h index 476681a787..08e003c9fb 100644 --- a/esphome/core/scheduler.h +++ b/esphome/core/scheduler.h @@ -221,7 +221,7 @@ class Scheduler { size_t cleanup_(); // Remove and return the front item from the heap // IMPORTANT: Caller must hold the scheduler lock before calling this function. - std::unique_ptr pop_raw_(); + std::unique_ptr pop_raw_locked_(); private: // Helper to cancel items by name - must be called with lock held From eb970cf44ec07ea45596f837c573e4fec7ff09d7 Mon Sep 17 00:00:00 2001 From: Jon Oberheide <506986+jonoberheide@users.noreply.github.com> Date: Wed, 26 Nov 2025 17:56:22 -0500 Subject: [PATCH 312/415] make thermostat humidification_action public (#12132) --- esphome/components/thermostat/thermostat_climate.cpp | 8 ++++---- esphome/components/thermostat/thermostat_climate.h | 6 +++--- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/esphome/components/thermostat/thermostat_climate.cpp b/esphome/components/thermostat/thermostat_climate.cpp index 2b51f58f4f..e79eed4055 100644 --- a/esphome/components/thermostat/thermostat_climate.cpp +++ b/esphome/components/thermostat/thermostat_climate.cpp @@ -654,7 +654,7 @@ void ThermostatClimate::trigger_supplemental_action_() { void ThermostatClimate::switch_to_humidity_control_action_(HumidificationAction action) { // setup_complete_ helps us ensure an action is called immediately after boot - if ((action == this->humidification_action_) && this->setup_complete_) { + if ((action == this->humidification_action) && this->setup_complete_) { // already in target mode return; } @@ -683,7 +683,7 @@ void ThermostatClimate::switch_to_humidity_control_action_(HumidificationAction this->prev_humidity_control_trigger_->stop_action(); this->prev_humidity_control_trigger_ = nullptr; } - this->humidification_action_ = action; + this->humidification_action = action; this->prev_humidity_control_trigger_ = trig; if (trig != nullptr) { trig->trigger(); @@ -1114,7 +1114,7 @@ bool ThermostatClimate::dehumidification_required_() { } // if we get here, the current humidity is between target + hysteresis and target - hysteresis, // so the action should not change - return this->humidification_action_ == THERMOSTAT_HUMIDITY_CONTROL_ACTION_DEHUMIDIFY; + return this->humidification_action == THERMOSTAT_HUMIDITY_CONTROL_ACTION_DEHUMIDIFY; } bool ThermostatClimate::humidification_required_() { @@ -1127,7 +1127,7 @@ bool ThermostatClimate::humidification_required_() { } // if we get here, the current humidity is between target - hysteresis and target + hysteresis, // so the action should not change - return this->humidification_action_ == THERMOSTAT_HUMIDITY_CONTROL_ACTION_HUMIDIFY; + return this->humidification_action == THERMOSTAT_HUMIDITY_CONTROL_ACTION_HUMIDIFY; } void ThermostatClimate::dump_preset_config_(const char *preset_name, const ThermostatClimateTargetTempConfig &config) { diff --git a/esphome/components/thermostat/thermostat_climate.h b/esphome/components/thermostat/thermostat_climate.h index 76391f800c..69d2307b1c 100644 --- a/esphome/components/thermostat/thermostat_climate.h +++ b/esphome/components/thermostat/thermostat_climate.h @@ -207,6 +207,9 @@ class ThermostatClimate : public climate::Climate, public Component { void validate_target_temperature_high(); void validate_target_humidity(); + /// The current humidification action + HumidificationAction humidification_action{THERMOSTAT_HUMIDITY_CONTROL_ACTION_NONE}; + protected: /// Override control to change settings of the climate device. void control(const climate::ClimateCall &call) override; @@ -301,9 +304,6 @@ class ThermostatClimate : public climate::Climate, public Component { /// The current supplemental action climate::ClimateAction supplemental_action_{climate::CLIMATE_ACTION_OFF}; - /// The current humidification action - HumidificationAction humidification_action_{THERMOSTAT_HUMIDITY_CONTROL_ACTION_NONE}; - /// Default standard preset to use on start up climate::ClimatePreset default_preset_{}; From caaa08d678f43ebcad29e837dfc25137f21d2776 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Thu, 27 Nov 2025 09:05:45 +1000 Subject: [PATCH 313/415] [core] Fix for missing arguments to shared_lambda (#12115) --- esphome/components/lvgl/widgets/__init__.py | 4 ++-- esphome/cpp_generator.py | 8 +------- tests/components/lvgl/lvgl-package.yaml | 12 ++++++++++++ 3 files changed, 15 insertions(+), 9 deletions(-) diff --git a/esphome/components/lvgl/widgets/__init__.py b/esphome/components/lvgl/widgets/__init__.py index 2e7948522e..b1d157325b 100644 --- a/esphome/components/lvgl/widgets/__init__.py +++ b/esphome/components/lvgl/widgets/__init__.py @@ -382,7 +382,7 @@ async def set_obj_properties(w: Widget, config): clrs = join_enums(flag_clr, "LV_OBJ_FLAG_") w.clear_flag(clrs) for key, value in lambs.items(): - lamb = await cg.process_lambda(value, [], return_type=cg.bool_) + lamb = await cg.process_lambda(value, [], capture="=", return_type=cg.bool_) flag = f"LV_OBJ_FLAG_{key.upper()}" with LvConditional(call_lambda(lamb)) as cond: w.add_flag(flag) @@ -407,7 +407,7 @@ async def set_obj_properties(w: Widget, config): clears = join_enums(clears, "LV_STATE_") w.clear_state(clears) for key, value in lambs.items(): - lamb = await cg.process_lambda(value, [], return_type=cg.bool_) + lamb = await cg.process_lambda(value, [], capture="=", return_type=cg.bool_) state = f"LV_STATE_{key.upper()}" with LvConditional(call_lambda(lamb)) as cond: w.add_state(state) diff --git a/esphome/cpp_generator.py b/esphome/cpp_generator.py index 6f1af01a5b..1a47b346b7 100644 --- a/esphome/cpp_generator.py +++ b/esphome/cpp_generator.py @@ -659,7 +659,7 @@ async def get_variable_with_full_id(id_: ID) -> tuple[ID, "MockObj"]: async def process_lambda( value: Lambda, parameters: TemplateArgsType, - capture: str = "=", + capture: str = "", return_type: SafeExpType = None, ) -> LambdaExpression | None: """Process the given lambda value into a LambdaExpression. @@ -702,12 +702,6 @@ async def process_lambda( parts[i * 3 + 1] = var parts[i * 3 + 2] = "" - # All id() references are global variables in generated C++ code. - # Global variables should not be captured - they're accessible everywhere. - # Use empty capture instead of capture-by-value. - if capture == "=": - capture = "" - if isinstance(value, ESPHomeDataBase) and value.esp_range is not None: location = value.esp_range.start_mark location.line += value.content_offset diff --git a/tests/components/lvgl/lvgl-package.yaml b/tests/components/lvgl/lvgl-package.yaml index 30866a603c..a5714d5639 100644 --- a/tests/components/lvgl/lvgl-package.yaml +++ b/tests/components/lvgl/lvgl-package.yaml @@ -16,6 +16,18 @@ binary_sensor: platform: template - id: left_sensor platform: template + - platform: lvgl + id: button_checker + name: LVGL button + widget: button_button + on_state: + then: + - lvgl.checkbox.update: + id: checkbox_id + state: + checked: !lambda |- + auto y = x; // block inlining of one line return + return y; lvgl: log_level: debug From a2d9941c622388f827570077dc6ab45d1d80112f Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Thu, 27 Nov 2025 09:06:32 +1000 Subject: [PATCH 314/415] [lvgl] Add option to sync updates with display (#11896) Co-authored-by: J. Nick Koston --- esphome/components/lvgl/__init__.py | 4 ++ esphome/components/lvgl/defines.py | 1 + esphome/components/lvgl/lvgl_esphome.cpp | 48 +++++++++++++++++++---- esphome/components/lvgl/lvgl_esphome.h | 13 +++--- tests/components/lvgl/test.esp32-idf.yaml | 1 + 5 files changed, 55 insertions(+), 12 deletions(-) diff --git a/esphome/components/lvgl/__init__.py b/esphome/components/lvgl/__init__.py index eaa37b54dd..eeabf755a6 100644 --- a/esphome/components/lvgl/__init__.py +++ b/esphome/components/lvgl/__init__.py @@ -276,6 +276,7 @@ async def to_code(configs): config[df.CONF_FULL_REFRESH], config[CONF_DRAW_ROUNDING], config[df.CONF_RESUME_ON_INPUT], + config[df.CONF_UPDATE_WHEN_DISPLAY_IDLE], ) await cg.register_component(lv_component, config) Widget.create(config[CONF_ID], lv_component, LvScrActType(), config) @@ -373,6 +374,9 @@ LVGL_SCHEMA = cv.All( df.CONF_DEFAULT_FONT, default="montserrat_14" ): lvalid.lv_font, cv.Optional(df.CONF_FULL_REFRESH, default=False): cv.boolean, + cv.Optional( + df.CONF_UPDATE_WHEN_DISPLAY_IDLE, default=False + ): cv.boolean, cv.Optional(CONF_DRAW_ROUNDING, default=2): cv.positive_int, cv.Optional(CONF_BUFFER_SIZE, default=0): cv.percentage, cv.Optional(CONF_LOG_LEVEL, default="WARN"): cv.one_of( diff --git a/esphome/components/lvgl/defines.py b/esphome/components/lvgl/defines.py index 6b3b9c97ef..1fce6fa458 100644 --- a/esphome/components/lvgl/defines.py +++ b/esphome/components/lvgl/defines.py @@ -542,6 +542,7 @@ CONF_TOUCHSCREENS = "touchscreens" CONF_TRANSPARENCY_KEY = "transparency_key" CONF_THEME = "theme" CONF_UPDATE_ON_RELEASE = "update_on_release" +CONF_UPDATE_WHEN_DISPLAY_IDLE = "update_when_display_idle" CONF_VISIBLE_ROW_COUNT = "visible_row_count" CONF_WIDGET = "widget" CONF_WIDGETS = "widgets" diff --git a/esphome/components/lvgl/lvgl_esphome.cpp b/esphome/components/lvgl/lvgl_esphome.cpp index fbcd68378c..18226a9f57 100644 --- a/esphome/components/lvgl/lvgl_esphome.cpp +++ b/esphome/components/lvgl/lvgl_esphome.cpp @@ -106,6 +106,7 @@ void LvglComponent::dump_config() { this->disp_drv_.hor_res, this->disp_drv_.ver_res, 100 / this->buffer_frac_, this->rotation, (int) this->draw_rounding); } + void LvglComponent::set_paused(bool paused, bool show_snow) { this->paused_ = paused; this->show_snow_ = show_snow; @@ -124,32 +125,38 @@ void LvglComponent::esphome_lvgl_init() { lv_update_event = static_cast(lv_event_register_id()); lv_api_event = static_cast(lv_event_register_id()); } + void LvglComponent::add_event_cb(lv_obj_t *obj, event_callback_t callback, lv_event_code_t event) { lv_obj_add_event_cb(obj, callback, event, nullptr); } + void LvglComponent::add_event_cb(lv_obj_t *obj, event_callback_t callback, lv_event_code_t event1, lv_event_code_t event2) { add_event_cb(obj, callback, event1); add_event_cb(obj, callback, event2); } + void LvglComponent::add_event_cb(lv_obj_t *obj, event_callback_t callback, lv_event_code_t event1, lv_event_code_t event2, lv_event_code_t event3) { add_event_cb(obj, callback, event1); add_event_cb(obj, callback, event2); add_event_cb(obj, callback, event3); } + void LvglComponent::add_page(LvPageType *page) { this->pages_.push_back(page); page->set_parent(this); lv_disp_set_default(this->disp_); page->setup(this->pages_.size() - 1); } + void LvglComponent::show_page(size_t index, lv_scr_load_anim_t anim, uint32_t time) { if (index >= this->pages_.size()) return; this->current_page_ = index; lv_scr_load_anim(this->pages_[this->current_page_]->obj, anim, time, 0, false); } + void LvglComponent::show_next_page(lv_scr_load_anim_t anim, uint32_t time) { if (this->pages_.empty() || (this->current_page_ == this->pages_.size() - 1 && !this->page_wrap_)) return; @@ -158,6 +165,7 @@ void LvglComponent::show_next_page(lv_scr_load_anim_t anim, uint32_t time) { } while (this->pages_[this->current_page_]->skip); // skip empty pages() this->show_page(this->current_page_, anim, time); } + void LvglComponent::show_prev_page(lv_scr_load_anim_t anim, uint32_t time) { if (this->pages_.empty() || (this->current_page_ == 0 && !this->page_wrap_)) return; @@ -166,8 +174,10 @@ void LvglComponent::show_prev_page(lv_scr_load_anim_t anim, uint32_t time) { } while (this->pages_[this->current_page_]->skip); // skip empty pages() this->show_page(this->current_page_, anim, time); } + size_t LvglComponent::get_current_page() const { return this->current_page_; } bool LvPageType::is_showing() const { return this->parent_->get_current_page() == this->index; } + void LvglComponent::draw_buffer_(const lv_area_t *area, lv_color_t *ptr) { auto width = lv_area_get_width(area); auto height = lv_area_get_height(area); @@ -222,7 +232,7 @@ void LvglComponent::draw_buffer_(const lv_area_t *area, lv_color_t *ptr) { } void LvglComponent::flush_cb_(lv_disp_drv_t *disp_drv, const lv_area_t *area, lv_color_t *color_p) { - if (!this->paused_) { + if (!this->is_paused()) { auto now = millis(); this->draw_buffer_(area, color_p); ESP_LOGVV(TAG, "flush_cb, area=%d/%d, %d/%d took %dms", area->x1, area->y1, lv_area_get_width(area), @@ -230,6 +240,7 @@ void LvglComponent::flush_cb_(lv_disp_drv_t *disp_drv, const lv_area_t *area, lv } lv_disp_flush_ready(disp_drv); } + IdleTrigger::IdleTrigger(LvglComponent *parent, TemplatableValue timeout) : timeout_(std::move(timeout)) { parent->add_on_idle_callback([this](uint32_t idle_time) { if (!this->is_idle_ && idle_time > this->timeout_.value()) { @@ -377,6 +388,27 @@ void LvKeyboardType::set_obj(lv_obj_t *lv_obj) { } #endif // USE_LVGL_KEYBOARD +void LvglComponent::draw_end_() { + if (this->draw_end_callback_ != nullptr) + this->draw_end_callback_->trigger(); + if (this->update_when_display_idle_) { + for (auto *disp : this->displays_) + disp->update(); + } +} + +bool LvglComponent::is_paused() const { + if (this->paused_) + return true; + if (this->update_when_display_idle_) { + for (auto *disp : this->displays_) { + if (!disp->is_idle()) + return true; + } + } + return false; +} + void LvglComponent::write_random_() { int iterations = 6 - lv_disp_get_inactive_time(this->disp_) / 60000; if (iterations <= 0) @@ -426,12 +458,13 @@ void LvglComponent::write_random_() { * presses a key or clicks on the screen. */ LvglComponent::LvglComponent(std::vector displays, float buffer_frac, bool full_refresh, - int draw_rounding, bool resume_on_input) + int draw_rounding, bool resume_on_input, bool update_when_display_idle) : draw_rounding(draw_rounding), displays_(std::move(displays)), buffer_frac_(buffer_frac), full_refresh_(full_refresh), - resume_on_input_(resume_on_input) { + resume_on_input_(resume_on_input), + update_when_display_idle_(update_when_display_idle) { lv_disp_draw_buf_init(&this->draw_buf_, nullptr, nullptr, 0); lv_disp_drv_init(&this->disp_drv_); this->disp_drv_.draw_buf = &this->draw_buf_; @@ -487,7 +520,7 @@ void LvglComponent::setup() { if (this->draw_start_callback_ != nullptr) { this->disp_drv_.render_start_cb = render_start_cb; } - if (this->draw_end_callback_ != nullptr) { + if (this->draw_end_callback_ != nullptr || this->update_when_display_idle_) { this->disp_drv_.monitor_cb = monitor_cb; } #if LV_USE_LOG @@ -509,14 +542,15 @@ void LvglComponent::setup() { void LvglComponent::update() { // update indicators - if (this->paused_) { + if (this->is_paused()) { return; } this->idle_callbacks_.call(lv_disp_get_inactive_time(this->disp_)); } + void LvglComponent::loop() { - if (this->paused_) { - if (this->show_snow_) + if (this->is_paused()) { + if (this->paused_ && this->show_snow_) this->write_random_(); } else { lv_timer_handler_run_in_period(5); diff --git a/esphome/components/lvgl/lvgl_esphome.h b/esphome/components/lvgl/lvgl_esphome.h index bd6f1fdb61..9c82f3646b 100644 --- a/esphome/components/lvgl/lvgl_esphome.h +++ b/esphome/components/lvgl/lvgl_esphome.h @@ -151,7 +151,7 @@ class LvglComponent : public PollingComponent { public: LvglComponent(std::vector displays, float buffer_frac, bool full_refresh, int draw_rounding, - bool resume_on_input); + bool resume_on_input, bool update_when_display_idle); static void static_flush_cb(lv_disp_drv_t *disp_drv, const lv_area_t *area, lv_color_t *color_p); float get_setup_priority() const override { return setup_priority::PROCESSOR; } @@ -171,7 +171,9 @@ class LvglComponent : public PollingComponent { // @param paused If true, pause the display. If false, resume the display. // @param show_snow If true, show the snow effect when paused. void set_paused(bool paused, bool show_snow); - bool is_paused() const { return this->paused_; } + + // Returns true if the display is explicitly paused, or a blocking display update is in progress. + bool is_paused() const; // If the display is paused and we have resume_on_input_ set to true, resume the display. void maybe_wakeup() { if (this->paused_ && this->resume_on_input_) { @@ -210,10 +212,10 @@ class LvglComponent : public PollingComponent { void set_draw_end_trigger(Trigger<> *trigger) { this->draw_end_callback_ = trigger; } protected: - // these functions are never called unless the callbacks are non-null since the - // LVGL callbacks that call them are not set unless the start/end callbacks are non-null + void draw_end_(); + // Not checking for non-null callback since the + // LVGL callback that calls it is not set in that case void draw_start_() const { this->draw_start_callback_->trigger(); } - void draw_end_() const { this->draw_end_callback_->trigger(); } void write_random_(); void draw_buffer_(const lv_area_t *area, lv_color_t *ptr); @@ -222,6 +224,7 @@ class LvglComponent : public PollingComponent { size_t buffer_frac_{1}; bool full_refresh_{}; bool resume_on_input_{}; + bool update_when_display_idle_{}; lv_disp_draw_buf_t draw_buf_{}; lv_disp_drv_t disp_drv_{}; diff --git a/tests/components/lvgl/test.esp32-idf.yaml b/tests/components/lvgl/test.esp32-idf.yaml index 2450d28eb8..e6025e17fc 100644 --- a/tests/components/lvgl/test.esp32-idf.yaml +++ b/tests/components/lvgl/test.esp32-idf.yaml @@ -60,6 +60,7 @@ display: update_interval: never lvgl: + update_when_display_idle: true displays: - tft_display - second_display From 927d3715c1ab16d5963ee404790b5ae142b1b613 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Thu, 27 Nov 2025 09:06:40 +1000 Subject: [PATCH 315/415] [lvgl] Allow setting text directly on a button (#11964) Co-authored-by: J. Nick Koston --- esphome/components/lvgl/__init__.py | 18 +++++++--- esphome/components/lvgl/defines.py | 22 ++++++++++-- esphome/components/lvgl/schemas.py | 35 ++++++++++++++----- esphome/components/lvgl/types.py | 16 ++++++--- esphome/components/lvgl/widgets/button.py | 42 ++++++++++++++++++++--- tests/components/lvgl/lvgl-package.yaml | 8 +++++ 6 files changed, 115 insertions(+), 26 deletions(-) diff --git a/esphome/components/lvgl/__init__.py b/esphome/components/lvgl/__init__.py index eeabf755a6..040661495c 100644 --- a/esphome/components/lvgl/__init__.py +++ b/esphome/components/lvgl/__init__.py @@ -108,7 +108,7 @@ LV_CONF_H_FORMAT = """\ def generate_lv_conf_h(): - definitions = [as_macro(m, v) for m, v in df.lv_defines.items()] + definitions = [as_macro(m, v) for m, v in df.get_data(df.KEY_LV_DEFINES).items()] definitions.sort() return LV_CONF_H_FORMAT.format("\n".join(definitions)) @@ -140,11 +140,11 @@ def multi_conf_validate(configs: list[dict]): ) -def final_validation(configs): - if len(configs) != 1: - multi_conf_validate(configs) +def final_validation(config_list): + if len(config_list) != 1: + multi_conf_validate(config_list) global_config = full_config.get() - for config in configs: + for config in config_list: if (pages := config.get(CONF_PAGES)) and all(p[df.CONF_SKIP] for p in pages): raise cv.Invalid("At least one page must not be skipped") for display_id in config[df.CONF_DISPLAYS]: @@ -190,6 +190,14 @@ def final_validation(configs): raise cv.Invalid( f"Widget '{w}' does not have any dynamic properties to refresh", ) + # Do per-widget type final validation for update actions + for widget_type, update_configs in df.get_data(df.KEY_UPDATED_WIDGETS).items(): + for conf in update_configs: + for id_conf in conf.get(CONF_ID, ()): + name = id_conf[CONF_ID] + path = global_config.get_path_for_id(name) + widget_conf = global_config.get_config_for_path(path[:-1]) + widget_type.final_validate(name, conf, widget_conf, path[1:]) async def to_code(configs): diff --git a/esphome/components/lvgl/defines.py b/esphome/components/lvgl/defines.py index 1fce6fa458..1d528b2f73 100644 --- a/esphome/components/lvgl/defines.py +++ b/esphome/components/lvgl/defines.py @@ -9,7 +9,7 @@ from typing import TYPE_CHECKING, Any from esphome import codegen as cg, config_validation as cv from esphome.const import CONF_ITEMS -from esphome.core import ID, Lambda +from esphome.core import CORE, ID, Lambda from esphome.cpp_generator import LambdaExpression, MockObj from esphome.cpp_types import uint32 from esphome.schema_extractors import SCHEMA_EXTRACT, schema_extractor @@ -20,11 +20,27 @@ from .helpers import requires_component LOGGER = logging.getLogger(__name__) lvgl_ns = cg.esphome_ns.namespace("lvgl") -lv_defines = {} # Dict of #defines to provide as build flags +DOMAIN = "lvgl" +KEY_LV_DEFINES = "lv_defines" +KEY_UPDATED_WIDGETS = "updated_widgets" + + +def get_data(key, default=None): + """ + Get a data structure from the global data store by key + :param key: A key for the data + :param default: The default data - the default is an empty dict + :return: + """ + return CORE.data.setdefault(DOMAIN, {}).setdefault( + key, default if default is not None else {} + ) def add_define(macro, value="1"): - if macro in lv_defines and lv_defines[macro] != value: + lv_defines = get_data(KEY_LV_DEFINES) + value = str(value) + if lv_defines.setdefault(macro, value) != value: LOGGER.error( "Redefinition of %s - was %s now %s", macro, lv_defines[macro], value ) diff --git a/esphome/components/lvgl/schemas.py b/esphome/components/lvgl/schemas.py index f2704f99de..45d933c00e 100644 --- a/esphome/components/lvgl/schemas.py +++ b/esphome/components/lvgl/schemas.py @@ -1,3 +1,5 @@ +from collections.abc import Callable + from esphome import config_validation as cv from esphome.automation import Trigger, validate_automation from esphome.components.time import RealTimeClock @@ -311,19 +313,36 @@ def automation_schema(typ: LvType): } -def base_update_schema(widget_type, parts): +def _update_widget(widget_type: WidgetType) -> Callable[[dict], dict]: """ - Create a schema for updating a widgets style properties, states and flags + During validation of update actions, create a map of action types to affected widgets + for use in final validation. + :param widget_type: + :return: + """ + + def validator(value: dict) -> dict: + df.get_data(df.KEY_UPDATED_WIDGETS).setdefault(widget_type, []).append(value) + return value + + return validator + + +def base_update_schema(widget_type: WidgetType | LvType, parts): + """ + Create a schema for updating a widget's style properties, states and flags. :param widget_type: The type of the ID :param parts: The allowable parts to specify :return: """ - return part_schema(parts).extend( + + w_type = widget_type.w_type if isinstance(widget_type, WidgetType) else widget_type + schema = part_schema(parts).extend( { cv.Required(CONF_ID): cv.ensure_list( cv.maybe_simple_value( { - cv.Required(CONF_ID): cv.use_id(widget_type), + cv.Required(CONF_ID): cv.use_id(w_type), }, key=CONF_ID, ) @@ -332,11 +351,9 @@ def base_update_schema(widget_type, parts): } ) - -def create_modify_schema(widget_type): - return base_update_schema(widget_type.w_type, widget_type.parts).extend( - widget_type.modify_schema - ) + if isinstance(widget_type, WidgetType): + schema.add_extra(_update_widget(widget_type)) + return schema def obj_schema(widget_type: WidgetType): diff --git a/esphome/components/lvgl/types.py b/esphome/components/lvgl/types.py index b99c0ad5a3..9c92ca7e98 100644 --- a/esphome/components/lvgl/types.py +++ b/esphome/components/lvgl/types.py @@ -152,18 +152,18 @@ class WidgetType: # Local import to avoid circular import from .automation import update_to_code - from .schemas import WIDGET_TYPES, create_modify_schema + from .schemas import WIDGET_TYPES, base_update_schema if not is_mock: if self.name in WIDGET_TYPES: raise EsphomeError(f"Duplicate definition of widget type '{self.name}'") WIDGET_TYPES[self.name] = self - # Register the update action automatically + # Register the update action automatically, adding widget-specific properties register_action( f"lvgl.{self.name}.update", ObjUpdateAction, - create_modify_schema(self), + base_update_schema(self, self.parts).extend(self.modify_schema), )(update_to_code) @property @@ -182,7 +182,6 @@ class WidgetType: Generate code for a given widget :param w: The widget :param config: Its configuration - :return: Generated code as a list of text lines """ async def obj_creator(self, parent: MockObjClass, config: dict): @@ -228,6 +227,15 @@ class WidgetType: """ return value + def final_validate(self, widget, update_config, widget_config, path): + """ + Allow final validation for a given widget type update action + :param widget: A widget + :param update_config: The configuration for the update action + :param widget_config: The configuration for the widget itself + :param path: The path to the widget, for error reporting + """ + class NumberType(WidgetType): def get_max(self, config: dict): diff --git a/esphome/components/lvgl/widgets/button.py b/esphome/components/lvgl/widgets/button.py index b59884ee67..5f2910174f 100644 --- a/esphome/components/lvgl/widgets/button.py +++ b/esphome/components/lvgl/widgets/button.py @@ -1,20 +1,52 @@ -from esphome.const import CONF_BUTTON +from esphome import config_validation as cv +from esphome.const import CONF_BUTTON, CONF_TEXT +from esphome.cpp_generator import MockObj -from ..defines import CONF_MAIN +from ..defines import CONF_MAIN, CONF_WIDGETS +from ..helpers import add_lv_use +from ..lv_validation import lv_text +from ..lvcode import lv, lv_expr +from ..schemas import TEXT_SCHEMA from ..types import LvBoolean, WidgetType +from . import Widget +from .label import label_spec lv_button_t = LvBoolean("lv_btn_t") class ButtonType(WidgetType): def __init__(self): - super().__init__(CONF_BUTTON, lv_button_t, (CONF_MAIN,), lv_name="btn") + super().__init__( + CONF_BUTTON, lv_button_t, (CONF_MAIN,), schema=TEXT_SCHEMA, lv_name="btn" + ) + + def validate(self, value): + if CONF_TEXT in value: + if CONF_WIDGETS in value: + raise cv.Invalid("Cannot use both text and widgets in a button") + add_lv_use("label") + return value def get_uses(self): return ("btn",) - async def to_code(self, w, config): - return [] + def on_create(self, var: MockObj, config: dict): + if CONF_TEXT in config: + lv.label_create(var) + return var + + async def to_code(self, w: Widget, config): + if text := config.get(CONF_TEXT): + label_widget = Widget.create( + None, lv_expr.obj_get_child(w.obj, 0), label_spec + ) + await label_widget.set_property(CONF_TEXT, await lv_text.process(text)) + + def final_validate(self, widget, update_config, widget_config, path): + if CONF_TEXT in update_config and CONF_TEXT not in widget_config: + raise cv.Invalid( + "Button must have 'text:' configured to allow updating text", path + ) button_spec = ButtonType() diff --git a/tests/components/lvgl/lvgl-package.yaml b/tests/components/lvgl/lvgl-package.yaml index a5714d5639..65d629bcdf 100644 --- a/tests/components/lvgl/lvgl-package.yaml +++ b/tests/components/lvgl/lvgl-package.yaml @@ -426,6 +426,14 @@ lvgl: logger.log: Long pressed repeated - buttons: - id: button_e + - button: + id: button_with_text + text: Button + on_click: + lvgl.button.update: + id: button_with_text + text: Clicked + - button: layout: 2x1 id: button_button From b3955cd151d0f1b8ac488d75ab47dc468d9adde7 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Thu, 27 Nov 2025 09:07:51 +1000 Subject: [PATCH 316/415] [epaper_spi] Add SSD1677 and Waveshare 4.26 (#11887) Co-authored-by: J. Nick Koston --- esphome/components/display/display.cpp | 128 +++++++++++++----- esphome/components/epaper_spi/display.py | 90 ++++++++++-- esphome/components/epaper_spi/epaper_spi.cpp | 107 ++++++++++++--- esphome/components/epaper_spi/epaper_spi.h | 80 ++++++++--- .../epaper_spi/epaper_spi_spectra_e6.cpp | 23 ++-- .../epaper_spi/epaper_spi_spectra_e6.h | 4 +- .../epaper_spi/epaper_spi_ssd1677.cpp | 86 ++++++++++++ .../epaper_spi/epaper_spi_ssd1677.h | 25 ++++ .../components/epaper_spi/models/ssd1677.py | 42 ++++++ .../epaper_spi/test.esp32-s3-idf.yaml | 5 + 10 files changed, 488 insertions(+), 102 deletions(-) create mode 100644 esphome/components/epaper_spi/epaper_spi_ssd1677.cpp create mode 100644 esphome/components/epaper_spi/epaper_spi_ssd1677.h create mode 100644 esphome/components/epaper_spi/models/ssd1677.py diff --git a/esphome/components/display/display.cpp b/esphome/components/display/display.cpp index 1451d14e2e..ebc3c0a9f6 100644 --- a/esphome/components/display/display.cpp +++ b/esphome/components/display/display.cpp @@ -7,7 +7,6 @@ namespace esphome { namespace display { - static const char *const TAG = "display"; const Color COLOR_OFF(0, 0, 0, 0); @@ -16,6 +15,7 @@ const Color COLOR_ON(255, 255, 255, 255); void Display::fill(Color color) { this->filled_rectangle(0, 0, this->get_width(), this->get_height(), color); } void Display::clear() { this->fill(COLOR_OFF); } void Display::set_rotation(DisplayRotation rotation) { this->rotation_ = rotation; } + void HOT Display::line(int x1, int y1, int x2, int y2, Color color) { const int32_t dx = abs(x2 - x1), sx = x1 < x2 ? 1 : -1; const int32_t dy = -abs(y2 - y1), sy = y1 < y2 ? 1 : -1; @@ -91,23 +91,27 @@ void HOT Display::horizontal_line(int x, int y, int width, Color color) { for (int i = x; i < x + width; i++) this->draw_pixel_at(i, y, color); } + void HOT Display::vertical_line(int x, int y, int height, Color color) { // Future: Could be made more efficient by manipulating buffer directly in certain rotations. for (int i = y; i < y + height; i++) this->draw_pixel_at(x, i, color); } + void Display::rectangle(int x1, int y1, int width, int height, Color color) { this->horizontal_line(x1, y1, width, color); this->horizontal_line(x1, y1 + height - 1, width, color); this->vertical_line(x1, y1, height, color); this->vertical_line(x1 + width - 1, y1, height, color); } + void Display::filled_rectangle(int x1, int y1, int width, int height, Color color) { // Future: Use vertical_line and horizontal_line methods depending on rotation to reduce memory accesses. for (int i = y1; i < y1 + height; i++) { this->horizontal_line(x1, i, width, color); } } + void HOT Display::circle(int center_x, int center_xy, int radius, Color color) { int dx = -radius; int dy = 0; @@ -131,6 +135,7 @@ void HOT Display::circle(int center_x, int center_xy, int radius, Color color) { } } while (dx <= 0); } + void Display::filled_circle(int center_x, int center_y, int radius, Color color) { int dx = -int32_t(radius); int dy = 0; @@ -157,6 +162,7 @@ void Display::filled_circle(int center_x, int center_y, int radius, Color color) } } while (dx <= 0); } + void Display::filled_ring(int center_x, int center_y, int radius1, int radius2, Color color) { int rmax = radius1 > radius2 ? radius1 : radius2; int rmin = radius1 < radius2 ? radius1 : radius2; @@ -213,6 +219,7 @@ void Display::filled_ring(int center_x, int center_y, int radius1, int radius2, } } while (dxmax <= 0); } + void Display::filled_gauge(int center_x, int center_y, int radius1, int radius2, int progress, Color color) { int rmax = radius1 > radius2 ? radius1 : radius2; int rmin = radius1 < radius2 ? radius1 : radius2; @@ -228,7 +235,8 @@ void Display::filled_gauge(int center_x, int center_y, int radius1, int radius2, // outer dots this->draw_pixel_at(center_x + dxmax, center_y - dymax, color); this->draw_pixel_at(center_x - dxmax, center_y - dymax, color); - if (dymin < rmin) { // side parts + if (dymin < rmin) { + // side parts int lhline_width = -(dxmax - dxmin) + 1; if (progress >= 50) { if (float(dymax) < float(-dxmax) * tan_a) { @@ -239,7 +247,8 @@ void Display::filled_gauge(int center_x, int center_y, int radius1, int radius2, this->horizontal_line(center_x + dxmax, center_y - dymax, lhline_width, color); // left if (!dymax) this->horizontal_line(center_x - dxmin, center_y, lhline_width, color); // right horizontal border - if (upd_dxmax > -dxmin) { // right + if (upd_dxmax > -dxmin) { + // right int rhline_width = (upd_dxmax + dxmin) + 1; this->horizontal_line(center_x - dxmin, center_y - dymax, rhline_width > lhline_width ? lhline_width : rhline_width, color); @@ -256,7 +265,8 @@ void Display::filled_gauge(int center_x, int center_y, int radius1, int radius2, if (lhline_width > 0) this->horizontal_line(center_x + dxmax, center_y - dymax, lhline_width, color); } - } else { // top part + } else { + // top part int hline_width = 2 * (-dxmax) + 1; if (progress >= 50) { if (dymax < float(-dxmax) * tan_a) { @@ -300,11 +310,13 @@ void Display::filled_gauge(int center_x, int center_y, int radius1, int radius2, } } while (dxmax <= 0); } + void HOT Display::triangle(int x1, int y1, int x2, int y2, int x3, int y3, Color color) { this->line(x1, y1, x2, y2, color); this->line(x1, y1, x3, y3, color); this->line(x2, y2, x3, y3, color); } + void Display::sort_triangle_points_by_y_(int *x1, int *y1, int *x2, int *y2, int *x3, int *y3) { if (*y1 > *y2) { int x_temp = *x1, y_temp = *y1; @@ -322,6 +334,7 @@ void Display::sort_triangle_points_by_y_(int *x1, int *y1, int *x2, int *y2, int *x3 = x_temp, *y3 = y_temp; } } + void Display::filled_flat_side_triangle_(int x1, int y1, int x2, int y2, int x3, int y3, Color color) { // y2 must be equal to y3 (same horizontal line) @@ -333,7 +346,8 @@ void Display::filled_flat_side_triangle_(int x1, int y1, int x2, int y2, int x3, int s1_dy = abs(y2 - y1); int s1_sign_x = ((x2 - x1) >= 0) ? 1 : -1; int s1_sign_y = ((y2 - y1) >= 0) ? 1 : -1; - if (s1_dy > s1_dx) { // swap values + if (s1_dy > s1_dx) { + // swap values int tmp = s1_dx; s1_dx = s1_dy; s1_dy = tmp; @@ -349,7 +363,8 @@ void Display::filled_flat_side_triangle_(int x1, int y1, int x2, int y2, int x3, int s2_dy = abs(y3 - y1); int s2_sign_x = ((x3 - x1) >= 0) ? 1 : -1; int s2_sign_y = ((y3 - y1) >= 0) ? 1 : -1; - if (s2_dy > s2_dx) { // swap values + if (s2_dy > s2_dx) { + // swap values int tmp = s2_dx; s2_dx = s2_dy; s2_dy = tmp; @@ -402,20 +417,25 @@ void Display::filled_flat_side_triangle_(int x1, int y1, int x2, int y2, int x3, } } } + void Display::filled_triangle(int x1, int y1, int x2, int y2, int x3, int y3, Color color) { // Sort the three points by y-coordinate ascending, so [x1,y1] is the topmost point this->sort_triangle_points_by_y_(&x1, &y1, &x2, &y2, &x3, &y3); - if (y2 == y3) { // Check for special case of a bottom-flat triangle + if (y2 == y3) { + // Check for special case of a bottom-flat triangle this->filled_flat_side_triangle_(x1, y1, x2, y2, x3, y3, color); - } else if (y1 == y2) { // Check for special case of a top-flat triangle + } else if (y1 == y2) { + // Check for special case of a top-flat triangle this->filled_flat_side_triangle_(x3, y3, x1, y1, x2, y2, color); - } else { // General case: split the no-flat-side triangle in a top-flat triangle and bottom-flat triangle + } else { + // General case: split the no-flat-side triangle in a top-flat triangle and bottom-flat triangle int x_temp = (int) (x1 + ((float) (y2 - y1) / (float) (y3 - y1)) * (x3 - x1)), y_temp = y2; this->filled_flat_side_triangle_(x1, y1, x2, y2, x_temp, y_temp, color); this->filled_flat_side_triangle_(x3, y3, x2, y2, x_temp, y_temp, color); } } + void HOT Display::get_regular_polygon_vertex(int vertex_id, int *vertex_x, int *vertex_y, int center_x, int center_y, int radius, int edges, RegularPolygonVariation variation, float rotation_degrees) { @@ -447,7 +467,8 @@ void HOT Display::regular_polygon(int x, int y, int radius, int edges, RegularPo int current_vertex_x, current_vertex_y; get_regular_polygon_vertex(current_vertex_id, ¤t_vertex_x, ¤t_vertex_y, x, y, radius, edges, variation, rotation_degrees); - if (current_vertex_id > 0) { // Start drawing after the 2nd vertex coordinates has been calculated + if (current_vertex_id > 0) { + // Start drawing after the 2nd vertex coordinates has been calculated if (drawing == DRAWING_FILLED) { this->filled_triangle(x, y, previous_vertex_x, previous_vertex_y, current_vertex_x, current_vertex_y, color); } else if (drawing == DRAWING_OUTLINE) { @@ -459,21 +480,26 @@ void HOT Display::regular_polygon(int x, int y, int radius, int edges, RegularPo } } } + void HOT Display::regular_polygon(int x, int y, int radius, int edges, RegularPolygonVariation variation, Color color, RegularPolygonDrawing drawing) { regular_polygon(x, y, radius, edges, variation, ROTATION_0_DEGREES, color, drawing); } + void HOT Display::regular_polygon(int x, int y, int radius, int edges, Color color, RegularPolygonDrawing drawing) { regular_polygon(x, y, radius, edges, VARIATION_POINTY_TOP, ROTATION_0_DEGREES, color, drawing); } + void Display::filled_regular_polygon(int x, int y, int radius, int edges, RegularPolygonVariation variation, float rotation_degrees, Color color) { regular_polygon(x, y, radius, edges, variation, rotation_degrees, color, DRAWING_FILLED); } + void Display::filled_regular_polygon(int x, int y, int radius, int edges, RegularPolygonVariation variation, Color color) { regular_polygon(x, y, radius, edges, variation, ROTATION_0_DEGREES, color, DRAWING_FILLED); } + void Display::filled_regular_polygon(int x, int y, int radius, int edges, Color color) { regular_polygon(x, y, radius, edges, VARIATION_POINTY_TOP, ROTATION_0_DEGREES, color, DRAWING_FILLED); } @@ -584,15 +610,19 @@ void Display::get_text_bounds(int x, int y, const char *text, BaseFont *font, Te break; } } + void Display::print(int x, int y, BaseFont *font, Color color, const char *text, Color background) { this->print(x, y, font, color, TextAlign::TOP_LEFT, text, background); } + void Display::print(int x, int y, BaseFont *font, TextAlign align, const char *text) { this->print(x, y, font, COLOR_ON, align, text); } + void Display::print(int x, int y, BaseFont *font, const char *text) { this->print(x, y, font, COLOR_ON, TextAlign::TOP_LEFT, text); } + void Display::printf(int x, int y, BaseFont *font, Color color, Color background, TextAlign align, const char *format, ...) { va_list arg; @@ -600,31 +630,37 @@ void Display::printf(int x, int y, BaseFont *font, Color color, Color background this->vprintf_(x, y, font, color, background, align, format, arg); va_end(arg); } + void Display::printf(int x, int y, BaseFont *font, Color color, TextAlign align, const char *format, ...) { va_list arg; va_start(arg, format); this->vprintf_(x, y, font, color, COLOR_OFF, align, format, arg); va_end(arg); } + void Display::printf(int x, int y, BaseFont *font, Color color, const char *format, ...) { va_list arg; va_start(arg, format); this->vprintf_(x, y, font, color, COLOR_OFF, TextAlign::TOP_LEFT, format, arg); va_end(arg); } + void Display::printf(int x, int y, BaseFont *font, TextAlign align, const char *format, ...) { va_list arg; va_start(arg, format); this->vprintf_(x, y, font, COLOR_ON, COLOR_OFF, align, format, arg); va_end(arg); } + void Display::printf(int x, int y, BaseFont *font, const char *format, ...) { va_list arg; va_start(arg, format); this->vprintf_(x, y, font, COLOR_ON, COLOR_OFF, TextAlign::TOP_LEFT, format, arg); va_end(arg); } + void Display::set_writer(display_writer_t &&writer) { this->writer_ = writer; } + void Display::set_pages(std::vector pages) { for (auto *page : pages) page->set_parent(this); @@ -637,6 +673,7 @@ void Display::set_pages(std::vector pages) { pages[pages.size() - 1]->set_next(pages[0]); this->show_page(pages[0]); } + void Display::show_page(DisplayPage *page) { this->previous_page_ = this->page_; this->page_ = page; @@ -645,8 +682,10 @@ void Display::show_page(DisplayPage *page) { t->process(this->previous_page_, this->page_); } } + void Display::show_next_page() { this->page_->show_next(); } void Display::show_prev_page() { this->page_->show_prev(); } + void Display::do_update_() { if (this->auto_clear_enabled_) { this->clear(); @@ -660,10 +699,12 @@ void Display::do_update_() { } this->clear_clipping_(); } + void DisplayOnPageChangeTrigger::process(DisplayPage *from, DisplayPage *to) { if ((this->from_ == nullptr || this->from_ == from) && (this->to_ == nullptr || this->to_ == to)) this->trigger(from, to); } + void Display::strftime(int x, int y, BaseFont *font, Color color, Color background, TextAlign align, const char *format, ESPTime time) { char buffer[64]; @@ -671,15 +712,19 @@ void Display::strftime(int x, int y, BaseFont *font, Color color, Color backgrou if (ret > 0) this->print(x, y, font, color, align, buffer, background); } + void Display::strftime(int x, int y, BaseFont *font, Color color, TextAlign align, const char *format, ESPTime time) { this->strftime(x, y, font, color, COLOR_OFF, align, format, time); } + void Display::strftime(int x, int y, BaseFont *font, Color color, const char *format, ESPTime time) { this->strftime(x, y, font, color, COLOR_OFF, TextAlign::TOP_LEFT, format, time); } + void Display::strftime(int x, int y, BaseFont *font, TextAlign align, const char *format, ESPTime time) { this->strftime(x, y, font, COLOR_ON, COLOR_OFF, align, format, time); } + void Display::strftime(int x, int y, BaseFont *font, const char *format, ESPTime time) { this->strftime(x, y, font, COLOR_ON, COLOR_OFF, TextAlign::TOP_LEFT, format, time); } @@ -691,6 +736,7 @@ void Display::start_clipping(Rect rect) { } this->clipping_rectangle_.push_back(rect); } + void Display::end_clipping() { if (this->clipping_rectangle_.empty()) { ESP_LOGE(TAG, "clear: Clipping is not set."); @@ -698,6 +744,7 @@ void Display::end_clipping() { this->clipping_rectangle_.pop_back(); } } + void Display::extend_clipping(Rect add_rect) { if (this->clipping_rectangle_.empty()) { ESP_LOGE(TAG, "add: Clipping is not set."); @@ -705,6 +752,7 @@ void Display::extend_clipping(Rect add_rect) { this->clipping_rectangle_.back().extend(add_rect); } } + void Display::shrink_clipping(Rect add_rect) { if (this->clipping_rectangle_.empty()) { ESP_LOGE(TAG, "add: Clipping is not set."); @@ -712,6 +760,7 @@ void Display::shrink_clipping(Rect add_rect) { this->clipping_rectangle_.back().shrink(add_rect); } } + Rect Display::get_clipping() const { if (this->clipping_rectangle_.empty()) { return Rect(); @@ -719,7 +768,9 @@ Rect Display::get_clipping() const { return this->clipping_rectangle_.back(); } } + void Display::clear_clipping_() { this->clipping_rectangle_.clear(); } + bool Display::clip(int x, int y) { if (x < 0 || x >= this->get_width() || y < 0 || y >= this->get_height()) return false; @@ -727,6 +778,7 @@ bool Display::clip(int x, int y) { return false; return true; } + bool Display::clamp_x_(int x, int w, int &min_x, int &max_x) { min_x = std::max(x, 0); max_x = std::min(x + w, this->get_width()); @@ -742,6 +794,7 @@ bool Display::clamp_x_(int x, int w, int &min_x, int &max_x) { return min_x < max_x; } + bool Display::clamp_y_(int y, int h, int &min_y, int &max_y) { min_y = std::max(y, 0); max_y = std::min(y + h, this->get_height()); @@ -766,15 +819,15 @@ void Display::test_card() { int w = get_width(), h = get_height(), image_w, image_h; this->clear(); this->show_test_card_ = false; + image_w = std::min(w - 20, 310); + image_h = std::min(h - 20, 255); + int shift_x = (w - image_w) / 2; + int shift_y = (h - image_h) / 2; + int line_w = (image_w - 6) / 6; + int image_c = image_w / 2; if (this->get_display_type() == DISPLAY_TYPE_COLOR) { Color r(255, 0, 0), g(0, 255, 0), b(0, 0, 255); - image_w = std::min(w - 20, 310); - image_h = std::min(h - 20, 255); - int shift_x = (w - image_w) / 2; - int shift_y = (h - image_h) / 2; - int line_w = (image_w - 6) / 6; - int image_c = image_w / 2; for (auto i = 0; i != image_h; i++) { int c = esp_scale(i, image_h); this->horizontal_line(shift_x + 0, shift_y + i, line_w, r.fade_to_white(c)); @@ -786,26 +839,26 @@ void Display::test_card() { this->horizontal_line(shift_x + image_w - (line_w * 2), shift_y + i, line_w, b.fade_to_white(c)); this->horizontal_line(shift_x + image_w - line_w, shift_y + i, line_w, b.fade_to_black(c)); } - this->rectangle(shift_x, shift_y, image_w, image_h, Color(127, 127, 0)); + } + this->rectangle(shift_x, shift_y, image_w, image_h, Color(127, 127, 0)); - uint16_t shift_r = shift_x + line_w - (8 * 3); - uint16_t shift_g = shift_x + image_c - (8 * 3); - uint16_t shift_b = shift_x + image_w - line_w - (8 * 3); - shift_y = h / 2 - (8 * 3); - for (auto i = 0; i < 8; i++) { - uint8_t ftr = progmem_read_byte(&TESTCARD_FONT[0][i]); - uint8_t ftg = progmem_read_byte(&TESTCARD_FONT[1][i]); - uint8_t ftb = progmem_read_byte(&TESTCARD_FONT[2][i]); - for (auto k = 0; k < 8; k++) { - if ((ftr & (1 << k)) != 0) { - this->filled_rectangle(shift_r + (i * 6), shift_y + (k * 6), 6, 6, COLOR_OFF); - } - if ((ftg & (1 << k)) != 0) { - this->filled_rectangle(shift_g + (i * 6), shift_y + (k * 6), 6, 6, COLOR_OFF); - } - if ((ftb & (1 << k)) != 0) { - this->filled_rectangle(shift_b + (i * 6), shift_y + (k * 6), 6, 6, COLOR_OFF); - } + uint16_t shift_r = shift_x + line_w - (8 * 3); + uint16_t shift_g = shift_x + image_c - (8 * 3); + uint16_t shift_b = shift_x + image_w - line_w - (8 * 3); + shift_y = h / 2 - (8 * 3); + for (auto i = 0; i < 8; i++) { + uint8_t ftr = progmem_read_byte(&TESTCARD_FONT[0][i]); + uint8_t ftg = progmem_read_byte(&TESTCARD_FONT[1][i]); + uint8_t ftb = progmem_read_byte(&TESTCARD_FONT[2][i]); + for (auto k = 0; k < 8; k++) { + if ((ftr & (1 << k)) != 0) { + this->filled_rectangle(shift_r + (i * 6), shift_y + (k * 6), 6, 6, COLOR_OFF); + } + if ((ftg & (1 << k)) != 0) { + this->filled_rectangle(shift_g + (i * 6), shift_y + (k * 6), 6, 6, COLOR_OFF); + } + if ((ftb & (1 << k)) != 0) { + this->filled_rectangle(shift_b + (i * 6), shift_y + (k * 6), 6, 6, COLOR_OFF); } } } @@ -818,7 +871,9 @@ void Display::test_card() { } DisplayPage::DisplayPage(display_writer_t writer) : writer_(std::move(writer)) {} + void DisplayPage::show() { this->parent_->show_page(this); } + void DisplayPage::show_next() { if (this->next_ == nullptr) { ESP_LOGE(TAG, "no next page"); @@ -826,6 +881,7 @@ void DisplayPage::show_next() { } this->next_->show(); } + void DisplayPage::show_prev() { if (this->prev_ == nullptr) { ESP_LOGE(TAG, "no previous page"); @@ -833,6 +889,7 @@ void DisplayPage::show_prev() { } this->prev_->show(); } + void DisplayPage::set_parent(Display *parent) { this->parent_ = parent; } void DisplayPage::set_prev(DisplayPage *prev) { this->prev_ = prev; } void DisplayPage::set_next(DisplayPage *next) { this->next_ = next; } @@ -868,6 +925,5 @@ const LogString *text_align_to_string(TextAlign textalign) { return LOG_STR("UNKNOWN"); } } - } // namespace display } // namespace esphome diff --git a/esphome/components/epaper_spi/display.py b/esphome/components/epaper_spi/display.py index 182c37ba40..ff5693c206 100644 --- a/esphome/components/epaper_spi/display.py +++ b/esphome/components/epaper_spi/display.py @@ -4,8 +4,10 @@ import pkgutil from esphome import core, pins import esphome.codegen as cg from esphome.components import display, spi +from esphome.components.display import CONF_SHOW_TEST_CARD, validate_rotation from esphome.components.mipi import flatten_sequence, map_sequence import esphome.config_validation as cv +from esphome.config_validation import update_interval from esphome.const import ( CONF_BUSY_PIN, CONF_CS_PIN, @@ -13,15 +15,25 @@ from esphome.const import ( CONF_DC_PIN, CONF_DIMENSIONS, CONF_ENABLE_PIN, + CONF_FULL_UPDATE_EVERY, CONF_HEIGHT, CONF_ID, CONF_INIT_SEQUENCE, CONF_LAMBDA, + CONF_MIRROR_X, + CONF_MIRROR_Y, CONF_MODEL, + CONF_PAGES, CONF_RESET_DURATION, CONF_RESET_PIN, + CONF_ROTATION, + CONF_SWAP_XY, + CONF_TRANSFORM, + CONF_UPDATE_INTERVAL, CONF_WIDTH, ) +from esphome.cpp_generator import RawExpression +from esphome.final_validate import full_config from . import models @@ -32,8 +44,9 @@ CONF_INIT_SEQUENCE_ID = "init_sequence_id" epaper_spi_ns = cg.esphome_ns.namespace("epaper_spi") EPaperBase = epaper_spi_ns.class_( - "EPaperBase", cg.PollingComponent, spi.SPIDevice, display.DisplayBuffer + "EPaperBase", cg.PollingComponent, spi.SPIDevice, display.Display ) +Transform = epaper_spi_ns.enum("Transform") EPaperSpectraE6 = epaper_spi_ns.class_("EPaperSpectraE6", EPaperBase) EPaper7p3InSpectraE6 = epaper_spi_ns.class_("EPaper7p3InSpectraE6", EPaperSpectraE6) @@ -52,6 +65,8 @@ DIMENSION_SCHEMA = cv.Schema( } ) +TRANSFORM_OPTIONS = {CONF_MIRROR_X, CONF_MIRROR_Y, CONF_SWAP_XY} + def model_schema(config): model = MODELS[config[CONF_MODEL]] @@ -73,7 +88,18 @@ def model_schema(config): ) .extend( { + cv.Optional(CONF_ROTATION, default=0): validate_rotation, cv.Required(CONF_MODEL): cv.one_of(model.name, upper=True), + cv.Optional( + CONF_UPDATE_INTERVAL, default=cv.UNDEFINED + ): update_interval, + cv.Optional(CONF_TRANSFORM): cv.Schema( + { + cv.Required(CONF_MIRROR_X): cv.boolean, + cv.Required(CONF_MIRROR_Y): cv.boolean, + } + ), + cv.Optional(CONF_FULL_UPDATE_EVERY, default=1): cv.int_range(1, 255), model.option(CONF_DC_PIN, fallback=None): pins.gpio_output_pin_schema, cv.GenerateID(): cv.declare_id(class_name), cv.GenerateID(CONF_INIT_SEQUENCE_ID): cv.declare_id(cg.uint8), @@ -111,9 +137,29 @@ def customise_schema(config): CONFIG_SCHEMA = customise_schema -FINAL_VALIDATE_SCHEMA = spi.final_validate_device_schema( - "epaper_spi", require_miso=False, require_mosi=True -) + +def _final_validate(config): + spi.final_validate_device_schema( + "epaper_spi", require_miso=False, require_mosi=True + )(config) + + global_config = full_config.get() + from esphome.components.lvgl import DOMAIN as LVGL_DOMAIN + + if CONF_LAMBDA not in config and CONF_PAGES not in config: + if LVGL_DOMAIN in global_config: + if CONF_UPDATE_INTERVAL not in config: + config[CONF_UPDATE_INTERVAL] = update_interval("never") + else: + # If no drawing methods are configured, and LVGL is not enabled, show a test card + config[CONF_SHOW_TEST_CARD] = True + config[CONF_UPDATE_INTERVAL] = core.TimePeriod( + seconds=60 + ).total_milliseconds + return config + + +FINAL_VALIDATE_SCHEMA = _final_validate async def to_code(config): @@ -137,7 +183,9 @@ async def to_code(config): init_sequence_length, ) - await display.register_display(var, config) + # Rotation is handled by setting the transform + display_config = {k: v for k, v in config.items() if k != CONF_ROTATION} + await display.register_display(var, display_config) await spi.register_spi_device(var, config) dc = await cg.gpio_pin_expression(config[CONF_DC_PIN]) @@ -148,11 +196,35 @@ async def to_code(config): config[CONF_LAMBDA], [(display.DisplayRef, "it")], return_type=cg.void ) cg.add(var.set_writer(lambda_)) - if CONF_RESET_PIN in config: - reset = await cg.gpio_pin_expression(config[CONF_RESET_PIN]) + if reset_pin := config.get(CONF_RESET_PIN): + reset = await cg.gpio_pin_expression(reset_pin) cg.add(var.set_reset_pin(reset)) - if CONF_BUSY_PIN in config: - busy = await cg.gpio_pin_expression(config[CONF_BUSY_PIN]) + if busy_pin := config.get(CONF_BUSY_PIN): + busy = await cg.gpio_pin_expression(busy_pin) cg.add(var.set_busy_pin(busy)) + cg.add(var.set_full_update_every(config[CONF_FULL_UPDATE_EVERY])) if CONF_RESET_DURATION in config: cg.add(var.set_reset_duration(config[CONF_RESET_DURATION])) + if transform := config.get(CONF_TRANSFORM): + transform[CONF_SWAP_XY] = False + else: + transform = {x: model.get_default(x, False) for x in TRANSFORM_OPTIONS} + rotation = config[CONF_ROTATION] + if rotation == 180: + transform[CONF_MIRROR_X] = not transform[CONF_MIRROR_X] + transform[CONF_MIRROR_Y] = not transform[CONF_MIRROR_Y] + elif rotation == 90: + transform[CONF_SWAP_XY] = not transform[CONF_SWAP_XY] + transform[CONF_MIRROR_X] = not transform[CONF_MIRROR_X] + elif rotation == 270: + transform[CONF_SWAP_XY] = not transform[CONF_SWAP_XY] + transform[CONF_MIRROR_Y] = not transform[CONF_MIRROR_Y] + transform_str = "|".join( + { + str(getattr(Transform, x.upper())) + for x in TRANSFORM_OPTIONS + if transform.get(x) + } + ) + if transform_str: + cg.add(var.set_transform(RawExpression(transform_str))) diff --git a/esphome/components/epaper_spi/epaper_spi.cpp b/esphome/components/epaper_spi/epaper_spi.cpp index 39959cd743..f6313d33ef 100644 --- a/esphome/components/epaper_spi/epaper_spi.cpp +++ b/esphome/components/epaper_spi/epaper_spi.cpp @@ -9,9 +9,8 @@ namespace esphome::epaper_spi { static const char *const TAG = "epaper_spi"; static constexpr const char *const EPAPER_STATE_STRINGS[] = { - "IDLE", "UPDATE", "RESET", "RESET_END", - - "SHOULD_WAIT", "INITIALISE", "TRANSFER_DATA", "POWER_ON", "REFRESH_SCREEN", "POWER_OFF", "DEEP_SLEEP", + "IDLE", "UPDATE", "RESET", "RESET_END", "SHOULD_WAIT", "INITIALISE", + "TRANSFER_DATA", "POWER_ON", "REFRESH_SCREEN", "POWER_OFF", "DEEP_SLEEP", }; const char *EPaperBase::epaper_state_to_string_() { @@ -69,8 +68,8 @@ void EPaperBase::data(uint8_t value) { // The command is the first byte, length is the length of data only in the second byte, followed by the data. // [COMMAND, LENGTH, DATA...] void EPaperBase::cmd_data(uint8_t command, const uint8_t *ptr, size_t length) { - ESP_LOGVV(TAG, "Command: 0x%02X, Length: %d, Data: %s", command, length, - format_hex_pretty(ptr, length, '.', false).c_str()); + ESP_LOGV(TAG, "Command: 0x%02X, Length: %d, Data: %s", command, length, + format_hex_pretty(ptr, length, '.', false).c_str()); this->dc_pin_->digital_write(false); this->enable(); @@ -89,7 +88,7 @@ bool EPaperBase::is_idle_() const { return !this->busy_pin_->digital_read(); } -bool EPaperBase::reset_() const { +bool EPaperBase::reset() { if (this->reset_pin_ != nullptr) { if (this->state_ == EPaperState::RESET) { this->reset_pin_->digital_write(false); @@ -105,16 +104,16 @@ void EPaperBase::update() { ESP_LOGE(TAG, "Display already in state %s", epaper_state_to_string_()); return; } - this->set_state_(EPaperState::RESET); + this->set_state_(EPaperState::UPDATE); this->enable_loop(); +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_DEBUG + this->update_start_time_ = millis(); +#endif } void EPaperBase::wait_for_idle_(bool should_wait) { #if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE - if (should_wait) { - this->waiting_for_idle_start_ = millis(); - this->waiting_for_idle_last_print_ = this->waiting_for_idle_start_; - } + this->waiting_for_idle_start_ = millis(); #endif this->waiting_for_idle_ = should_wait; } @@ -138,7 +137,9 @@ void EPaperBase::loop() { if (this->waiting_for_idle_) { if (this->is_idle_()) { this->waiting_for_idle_ = false; - ESP_LOGV(TAG, "Screen now idle after %u ms", (unsigned) (millis() - this->waiting_for_idle_start_)); +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE + ESP_LOGV(TAG, "Screen was busy for %u ms", (unsigned) (millis() - this->waiting_for_idle_start_)); +#endif } else { #if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE if (now - this->waiting_for_idle_last_print_ >= 1000) { @@ -164,23 +165,27 @@ void EPaperBase::process_state_() { ESP_LOGV(TAG, "Process state entered in state %s", epaper_state_to_string_()); switch (this->state_) { default: - ESP_LOGD(TAG, "Display is in unhandled state %s", epaper_state_to_string_()); - this->disable_loop(); + ESP_LOGE(TAG, "Display is in unhandled state %s", epaper_state_to_string_()); + this->set_state_(EPaperState::IDLE); break; case EPaperState::IDLE: this->disable_loop(); break; case EPaperState::RESET: case EPaperState::RESET_END: - if (this->reset_()) { - this->set_state_(EPaperState::UPDATE); + if (this->reset()) { + this->set_state_(EPaperState::INITIALISE); } else { - this->set_state_(EPaperState::RESET_END); + this->set_state_(EPaperState::RESET_END, this->reset_duration_); } break; case EPaperState::UPDATE: this->do_update_(); // Calls ESPHome (current page) lambda - this->set_state_(EPaperState::INITIALISE); + if (this->x_high_ < this->x_low_ || this->y_high_ < this->y_low_) { + this->set_state_(EPaperState::IDLE); + return; + } + this->set_state_(EPaperState::RESET); break; case EPaperState::INITIALISE: this->initialise_(); @@ -190,6 +195,10 @@ void EPaperBase::process_state_() { if (!this->transfer_data()) { return; // Not done yet, come back next loop } + this->x_low_ = this->width_; + this->x_high_ = 0; + this->y_low_ = this->height_; + this->y_high_ = 0; this->set_state_(EPaperState::POWER_ON); break; case EPaperState::POWER_ON: @@ -197,7 +206,8 @@ void EPaperBase::process_state_() { this->set_state_(EPaperState::REFRESH_SCREEN); break; case EPaperState::REFRESH_SCREEN: - this->refresh_screen(); + this->refresh_screen(this->update_count_ != 0); + this->update_count_ = (this->update_count_ + 1) % this->full_update_every_; this->set_state_(EPaperState::POWER_OFF); break; case EPaperState::POWER_OFF: @@ -207,6 +217,7 @@ void EPaperBase::process_state_() { case EPaperState::DEEP_SLEEP: this->deep_sleep(); this->set_state_(EPaperState::IDLE); + ESP_LOGD(TAG, "Display update took %" PRIu32 " ms", millis() - this->update_start_time_); break; } } @@ -222,6 +233,9 @@ void EPaperBase::set_state_(EPaperState state, uint16_t delay) { } ESP_LOGV(TAG, "Enter state %s, delay %u, wait_for_idle=%s", this->epaper_state_to_string_(), delay, TRUEFALSE(this->waiting_for_idle_)); + if (state == EPaperState::IDLE) { + this->disable_loop(); + } } void EPaperBase::start_command_() { @@ -260,20 +274,73 @@ void EPaperBase::initialise_() { this->mark_failed(); return; } - ESP_LOGV(TAG, "Command %02X, length %d", cmd, num_args); this->cmd_data(cmd, sequence + index, num_args); index += num_args; } } } +/** + * Check and rotate coordinates based on the transform flags. + * @param x + * @param y + * @return false if the coordinates are out of bounds + */ +bool EPaperBase::rotate_coordinates_(int &x, int &y) const { + if (!this->get_clipping().inside(x, y)) + return false; + if (this->transform_ & SWAP_XY) + std::swap(x, y); + if (this->transform_ & MIRROR_X) + x = this->width_ - x - 1; + if (this->transform_ & MIRROR_Y) + y = this->height_ - y - 1; + if (x >= this->width_ || y >= this->height_ || x < 0 || y < 0) + return false; + return true; +} + +/** + * Default implementation for monochrome displays where 8 pixels are packed to a byte. + * @param x + * @param y + * @param color + */ +void HOT EPaperBase::draw_pixel_at(int x, int y, Color color) { + if (!rotate_coordinates_(x, y)) + return; + const size_t pixel_position = y * this->width_ + x; + const size_t byte_position = pixel_position / 8; + const uint8_t bit_position = pixel_position % 8; + const uint8_t pixel_bit = 0x80 >> bit_position; + const auto original = this->buffer_[byte_position]; + if ((color_to_bit(color) == 0)) { + this->buffer_[byte_position] = original & ~pixel_bit; + } else { + this->buffer_[byte_position] = original | pixel_bit; + } + this->x_low_ = clamp_at_most(this->x_low_, x); + this->x_high_ = clamp_at_least(this->x_high_, x + 1); + this->y_low_ = clamp_at_most(this->y_low_, y); + this->y_high_ = clamp_at_least(this->y_high_, y + 1); +} + void EPaperBase::dump_config() { LOG_DISPLAY("", "E-Paper SPI", this); ESP_LOGCONFIG(TAG, " Model: %s", this->name_); LOG_PIN(" Reset Pin: ", this->reset_pin_); LOG_PIN(" DC Pin: ", this->dc_pin_); LOG_PIN(" Busy Pin: ", this->busy_pin_); + LOG_PIN(" CS Pin: ", this->cs_); LOG_UPDATE_INTERVAL(this); + ESP_LOGCONFIG(TAG, + " SPI Data Rate: %uMHz\n" + " Full update every: %d\n" + " Swap X/Y: %s\n" + " Mirror X: %s\n" + " Mirror Y: %s", + (unsigned) (this->data_rate_ / 1000000), this->full_update_every_, YESNO(this->transform_ & SWAP_XY), + YESNO(this->transform_ & MIRROR_X), YESNO(this->transform_ & MIRROR_Y)); } } // namespace esphome::epaper_spi diff --git a/esphome/components/epaper_spi/epaper_spi.h b/esphome/components/epaper_spi/epaper_spi.h index 4745ec7339..544ea3e9ba 100644 --- a/esphome/components/epaper_spi/epaper_spi.h +++ b/esphome/components/epaper_spi/epaper_spi.h @@ -5,8 +5,6 @@ #include "esphome/components/split_buffer/split_buffer.h" #include "esphome/core/component.h" -#include - namespace esphome::epaper_spi { using namespace display; @@ -25,10 +23,16 @@ enum class EPaperState : uint8_t { DEEP_SLEEP, // deep sleep the display }; -static constexpr uint8_t MAX_TRANSFER_TIME = 10; // Transfer in 10ms blocks to allow the loop to run +static constexpr uint8_t NONE = 0; +static constexpr uint8_t MIRROR_X = 1; +static constexpr uint8_t MIRROR_Y = 2; +static constexpr uint8_t SWAP_XY = 4; + +static constexpr uint32_t MAX_TRANSFER_TIME = 10; // Transfer in 10ms blocks to allow the loop to run +static constexpr size_t MAX_TRANSFER_SIZE = 128; static constexpr uint8_t DELAY_FLAG = 0xFF; -class EPaperBase : public DisplayBuffer, +class EPaperBase : public Display, public spi::SPIDevice { public: @@ -45,6 +49,8 @@ class EPaperBase : public DisplayBuffer, void set_reset_pin(GPIOPin *reset) { this->reset_pin_ = reset; } void set_busy_pin(GPIOPin *busy) { this->busy_pin_ = busy; } void set_reset_duration(uint32_t reset_duration) { this->reset_duration_ = reset_duration; } + void set_transform(uint8_t transform) { this->transform_ = transform; } + void set_full_update_every(uint8_t full_update_every) { this->full_update_every_ = full_update_every; } void dump_config() override; void command(uint8_t value); @@ -60,20 +66,47 @@ class EPaperBase : public DisplayBuffer, DisplayType get_display_type() override { return this->display_type_; }; + // Default implementations for monochrome displays + static uint8_t color_to_bit(Color color) { + // It's always a shade of gray. Map to BLACK or WHITE. + // We split the luminance at a suitable point + if ((static_cast(color.r) + color.g + color.b) > 512) { + return 1; + } + return 0; + } + void fill(Color color) override { + auto pixel_color = color_to_bit(color) ? 0xFF : 0x00; + + // We store 8 pixels per byte + this->buffer_.fill(pixel_color); + this->x_high_ = this->width_; + this->y_high_ = this->height_; + this->x_low_ = 0; + this->y_low_ = 0; + } + + void clear() override { + // clear buffer to white, just like real paper. + this->fill(COLOR_ON); + } + protected: int get_height_internal() override { return this->height_; }; int get_width_internal() override { return this->width_; }; + int get_width() override { return this->transform_ & SWAP_XY ? this->height_ : this->width_; } + int get_height() override { return this->transform_ & SWAP_XY ? this->width_ : this->height_; } + void draw_pixel_at(int x, int y, Color color) override; void process_state_(); const char *epaper_state_to_string_(); bool is_idle_() const; void setup_pins_() const; - bool reset_() const; + virtual bool reset(); void initialise_(); void wait_for_idle_(bool should_wait); bool init_buffer_(size_t buffer_length); - - virtual int get_width_controller() { return this->get_width_internal(); }; + bool rotate_coordinates_(int &x, int &y) const; /** * Methods that must be implemented by concrete classes to control the display @@ -86,7 +119,7 @@ class EPaperBase : public DisplayBuffer, /** * Refresh the screen after data transfer */ - virtual void refresh_screen() = 0; + virtual void refresh_screen(bool partial) = 0; /** * Power the display on @@ -118,24 +151,31 @@ class EPaperBase : public DisplayBuffer, DisplayType display_type_; size_t buffer_length_{}; - size_t current_data_index_{0}; // used by data transfer to track progress - uint32_t reset_duration_{200}; -#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE - uint32_t transfer_start_time_{}; - uint32_t waiting_for_idle_last_print_{0}; - uint32_t waiting_for_idle_start_{0}; -#endif - + size_t current_data_index_{}; // used by data transfer to track progress + split_buffer::SplitBuffer buffer_{}; GPIOPin *dc_pin_{}; GPIOPin *busy_pin_{}; GPIOPin *reset_pin_{}; + bool waiting_for_idle_{}; + uint32_t delay_until_{}; + uint8_t transform_{}; + uint8_t update_count_{}; + // these values represent the bounds of the updated buffer. Note that x_high and y_high + // point to the pixel past the last one updated, i.e. may range up to width/height. + uint16_t x_low_{}, y_low_{}, x_high_{}, y_high_{}; - bool waiting_for_idle_{false}; - uint32_t delay_until_{0}; - - split_buffer::SplitBuffer buffer_; +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE + uint32_t waiting_for_idle_last_print_{}; + uint32_t waiting_for_idle_start_{}; +#endif +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_DEBUG + uint32_t update_start_time_{}; +#endif + // properties with specific initialisers go last EPaperState state_{EPaperState::IDLE}; + uint32_t reset_duration_{10}; + uint8_t full_update_every_{1}; }; } // namespace esphome::epaper_spi diff --git a/esphome/components/epaper_spi/epaper_spi_spectra_e6.cpp b/esphome/components/epaper_spi/epaper_spi_spectra_e6.cpp index 8e4cbdde2a..d0e68595d0 100644 --- a/esphome/components/epaper_spi/epaper_spi_spectra_e6.cpp +++ b/esphome/components/epaper_spi/epaper_spi_spectra_e6.cpp @@ -6,7 +6,6 @@ namespace esphome::epaper_spi { static constexpr const char *const TAG = "epaper_spi.6c"; -static constexpr size_t MAX_TRANSFER_SIZE = 128; static constexpr unsigned char GRAY_THRESHOLD = 50; enum E6Color { @@ -75,24 +74,24 @@ static uint8_t color_to_hex(Color color) { } void EPaperSpectraE6::power_on() { - ESP_LOGD(TAG, "Power on"); + ESP_LOGV(TAG, "Power on"); this->command(0x04); } void EPaperSpectraE6::power_off() { - ESP_LOGD(TAG, "Power off"); + ESP_LOGV(TAG, "Power off"); this->command(0x02); this->data(0x00); } -void EPaperSpectraE6::refresh_screen() { - ESP_LOGD(TAG, "Refresh"); +void EPaperSpectraE6::refresh_screen(bool partial) { + ESP_LOGV(TAG, "Refresh"); this->command(0x12); this->data(0x00); } void EPaperSpectraE6::deep_sleep() { - ESP_LOGD(TAG, "Deep sleep"); + ESP_LOGV(TAG, "Deep sleep"); this->command(0x07); this->data(0xA5); } @@ -109,12 +108,11 @@ void EPaperSpectraE6::clear() { this->fill(COLOR_ON); } -void HOT EPaperSpectraE6::draw_absolute_pixel_internal(int x, int y, Color color) { - if (x >= this->width_ || y >= this->height_ || x < 0 || y < 0) +void HOT EPaperSpectraE6::draw_pixel_at(int x, int y, Color color) { + if (!this->rotate_coordinates_(x, y)) return; - auto pixel_bits = color_to_hex(color); - uint32_t pixel_position = x + y * this->get_width_controller(); + uint32_t pixel_position = x + y * this->get_width_internal(); uint32_t byte_position = pixel_position / 2; auto original = this->buffer_[byte_position]; if ((pixel_position & 1) != 0) { @@ -128,10 +126,6 @@ bool HOT EPaperSpectraE6::transfer_data() { const uint32_t start_time = App.get_loop_component_start_time(); const size_t buffer_length = this->buffer_length_; if (this->current_data_index_ == 0) { -#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE - this->transfer_start_time_ = millis(); -#endif - ESP_LOGV(TAG, "Start sending data at %ums", (unsigned) millis()); this->command(0x10); } @@ -160,7 +154,6 @@ bool HOT EPaperSpectraE6::transfer_data() { this->end_data_(); } this->current_data_index_ = 0; - ESP_LOGV(TAG, "Sent data in %" PRIu32 " ms", millis() - this->transfer_start_time_); return true; } } // namespace esphome::epaper_spi diff --git a/esphome/components/epaper_spi/epaper_spi_spectra_e6.h b/esphome/components/epaper_spi/epaper_spi_spectra_e6.h index 48356ad74b..b8dbf0b0c5 100644 --- a/esphome/components/epaper_spi/epaper_spi_spectra_e6.h +++ b/esphome/components/epaper_spi/epaper_spi_spectra_e6.h @@ -16,11 +16,11 @@ class EPaperSpectraE6 : public EPaperBase { void clear() override; protected: - void refresh_screen() override; + void refresh_screen(bool partial) override; void power_on() override; void power_off() override; void deep_sleep() override; - void draw_absolute_pixel_internal(int x, int y, Color color) override; + void draw_pixel_at(int x, int y, Color color) override; bool transfer_data() override; }; diff --git a/esphome/components/epaper_spi/epaper_spi_ssd1677.cpp b/esphome/components/epaper_spi/epaper_spi_ssd1677.cpp new file mode 100644 index 0000000000..e4f04657ad --- /dev/null +++ b/esphome/components/epaper_spi/epaper_spi_ssd1677.cpp @@ -0,0 +1,86 @@ +#include "epaper_spi_ssd1677.h" + +#include + +#include "esphome/core/log.h" + +namespace esphome::epaper_spi { +static constexpr const char *const TAG = "epaper_spi.ssd1677"; + +void EPaperSSD1677::refresh_screen(bool partial) { + ESP_LOGV(TAG, "Refresh screen"); + this->command(0x22); + this->data(partial ? 0xFF : 0xF7); + this->command(0x20); +} + +void EPaperSSD1677::deep_sleep() { + ESP_LOGV(TAG, "Deep sleep"); + this->command(0x10); +} + +bool EPaperSSD1677::reset() { + if (EPaperBase::reset()) { + this->command(0x12); + return true; + } + return false; +} + +bool HOT EPaperSSD1677::transfer_data() { + auto start_time = millis(); + if (this->current_data_index_ == 0) { + uint8_t data[4]{}; + // round to byte boundaries + this->x_low_ &= ~7; + this->y_low_ &= ~7; + this->x_high_ += 7; + this->x_high_ &= ~7; + this->y_high_ += 7; + this->y_high_ &= ~7; + data[0] = this->x_low_; + data[1] = this->x_low_ / 256; + data[2] = this->x_high_ - 1; + data[3] = (this->x_high_ - 1) / 256; + cmd_data(0x4E, data, 2); + cmd_data(0x44, data, sizeof(data)); + data[0] = this->y_low_; + data[1] = this->y_low_ / 256; + data[2] = this->y_high_ - 1; + data[3] = (this->y_high_ - 1) / 256; + cmd_data(0x4F, data, 2); + this->cmd_data(0x45, data, sizeof(data)); + // for monochrome, we still need to clear the red data buffer at least once to prevent it + // causing dirty pixels after partial refresh. + this->command(this->send_red_ ? 0x26 : 0x24); + this->current_data_index_ = this->y_low_; // actually current line + } + size_t row_length = (this->x_high_ - this->x_low_) / 8; + FixedVector bytes_to_send{}; + bytes_to_send.init(row_length); + ESP_LOGV(TAG, "Writing bytes at line %zu at %ums", this->current_data_index_, (unsigned) millis()); + this->start_data_(); + while (this->current_data_index_ != this->y_high_) { + size_t data_idx = (this->current_data_index_ * this->width_ + this->x_low_) / 8; + for (size_t i = 0; i != row_length; i++) { + bytes_to_send[i] = this->send_red_ ? 0 : this->buffer_[data_idx++]; + } + ++this->current_data_index_; + this->write_array(&bytes_to_send.front(), row_length); // NOLINT + if (millis() - start_time > MAX_TRANSFER_TIME) { + // Let the main loop run and come back next loop + this->end_data_(); + return false; + } + } + + this->end_data_(); + this->current_data_index_ = 0; + if (this->send_red_) { + this->send_red_ = false; + return false; + } + return true; +} + +} // namespace esphome::epaper_spi diff --git a/esphome/components/epaper_spi/epaper_spi_ssd1677.h b/esphome/components/epaper_spi/epaper_spi_ssd1677.h new file mode 100644 index 0000000000..47584d24c0 --- /dev/null +++ b/esphome/components/epaper_spi/epaper_spi_ssd1677.h @@ -0,0 +1,25 @@ +#pragma once + +#include "epaper_spi.h" + +namespace esphome::epaper_spi { + +class EPaperSSD1677 : public EPaperBase { + public: + EPaperSSD1677(const char *name, uint16_t width, uint16_t height, const uint8_t *init_sequence, + size_t init_sequence_length) + : EPaperBase(name, width, height, init_sequence, init_sequence_length, DISPLAY_TYPE_BINARY) { + this->buffer_length_ = width * height / 8; // 8 pixels per byte + } + + protected: + void refresh_screen(bool partial) override; + void power_on() override {} + void power_off() override{}; + void deep_sleep() override; + bool reset() override; + bool transfer_data() override; + bool send_red_{true}; +}; + +} // namespace esphome::epaper_spi diff --git a/esphome/components/epaper_spi/models/ssd1677.py b/esphome/components/epaper_spi/models/ssd1677.py new file mode 100644 index 0000000000..3eb53d650e --- /dev/null +++ b/esphome/components/epaper_spi/models/ssd1677.py @@ -0,0 +1,42 @@ +from esphome.const import CONF_DATA_RATE + +from . import EpaperModel + + +class SSD1677(EpaperModel): + def __init__(self, name, class_name="EPaperSSD1677", **kwargs): + if CONF_DATA_RATE not in kwargs: + kwargs[CONF_DATA_RATE] = "20MHz" + super().__init__(name, class_name, **kwargs) + + # fmt: off + def get_init_sequence(self, config: dict): + width, _height = self.get_dimensions(config) + return ( + (0x18, 0x80), # Select internal Temp sensor + (0x0C, 0xAE, 0xC7, 0xC3, 0xC0, 0x80), # inrush current level 2 + (0x01, (width - 1) % 256, (width - 1) // 256, 0x02), # Set column gate limit + (0x3C, 0x01), # Set border waveform + (0x11, 3), # Set transform + ) + + +ssd1677 = SSD1677("ssd1677") + +ssd1677.extend( + "seeed-ee04-mono-4.26", + width=800, + height=480, + mirror_x=True, + cs_pin=44, + dc_pin=10, + reset_pin=38, + busy_pin={ + "number": 4, + "inverted": False, + "mode": { + "input": True, + "pulldown": True, + }, + }, +) diff --git a/tests/components/epaper_spi/test.esp32-s3-idf.yaml b/tests/components/epaper_spi/test.esp32-s3-idf.yaml index cff1f51897..d330b4127d 100644 --- a/tests/components/epaper_spi/test.esp32-s3-idf.yaml +++ b/tests/components/epaper_spi/test.esp32-s3-idf.yaml @@ -19,3 +19,8 @@ display: - platform: epaper_spi model: seeed-reterminal-e1002 + - platform: epaper_spi + model: seeed-ee04-mono-4.26 + # Override pins to avoid conflict with other display configs + busy_pin: 43 + dc_pin: 42 From 23e58c1c7b597b81f0634acfa3b907686de68333 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Thu, 27 Nov 2025 12:08:40 +1300 Subject: [PATCH 317/415] [inkplate] Ignore strapping pin warnings on default pins (#12110) --- esphome/components/inkplate/display.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/esphome/components/inkplate/display.py b/esphome/components/inkplate/display.py index 89518dcfab..47c8c898e5 100644 --- a/esphome/components/inkplate/display.py +++ b/esphome/components/inkplate/display.py @@ -6,10 +6,12 @@ import esphome.config_validation as cv from esphome.const import ( CONF_FULL_UPDATE_EVERY, CONF_ID, + CONF_IGNORE_STRAPPING_WARNING, CONF_LAMBDA, CONF_MIRROR_X, CONF_MIRROR_Y, CONF_MODEL, + CONF_NUMBER, CONF_OE_PIN, CONF_PAGES, CONF_TRANSFORM, @@ -101,14 +103,21 @@ CONFIG_SCHEMA = cv.All( cv.Required(CONF_SPV_PIN): pins.gpio_output_pin_schema, cv.Required(CONF_VCOM_PIN): pins.gpio_output_pin_schema, cv.Required(CONF_WAKEUP_PIN): pins.gpio_output_pin_schema, - cv.Optional(CONF_CL_PIN, default=0): pins.internal_gpio_output_pin_schema, - cv.Optional(CONF_LE_PIN, default=2): pins.internal_gpio_output_pin_schema, + cv.Optional( + CONF_CL_PIN, + default={CONF_NUMBER: 0, CONF_IGNORE_STRAPPING_WARNING: True}, + ): pins.internal_gpio_output_pin_schema, + cv.Optional( + CONF_LE_PIN, + default={CONF_NUMBER: 2, CONF_IGNORE_STRAPPING_WARNING: True}, + ): pins.internal_gpio_output_pin_schema, # Data pins cv.Optional( CONF_DISPLAY_DATA_0_PIN, default=4 ): pins.internal_gpio_output_pin_schema, cv.Optional( - CONF_DISPLAY_DATA_1_PIN, default=5 + CONF_DISPLAY_DATA_1_PIN, + default={CONF_NUMBER: 5, CONF_IGNORE_STRAPPING_WARNING: True}, ): pins.internal_gpio_output_pin_schema, cv.Optional( CONF_DISPLAY_DATA_2_PIN, default=18 From bbf7e8c1f25e85b3cfe0756f5e0b6fd45e304f27 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 26 Nov 2025 18:17:25 -0600 Subject: [PATCH 318/415] [api] Eliminate rx_buf heap churn and release buffers after initial sync --- esphome/components/api/api_connection.cpp | 6 ++++-- esphome/components/api/api_connection.h | 9 ++++++++ esphome/components/api/api_frame_helper.h | 21 ++++++++++++++++--- .../components/api/api_frame_helper_noise.cpp | 3 +-- .../api/api_frame_helper_plaintext.cpp | 3 +-- esphome/components/api/api_pb2_service.cpp | 4 ++-- esphome/components/api/api_pb2_service.h | 4 ++-- esphome/components/api/proto.h | 2 +- script/api_protobuf/api_protobuf.py | 8 +++---- 9 files changed, 42 insertions(+), 18 deletions(-) diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 12cbbb991d..9ad45dc6b7 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -169,8 +169,7 @@ void APIConnection::loop() { } else { this->last_traffic_ = now; // read a packet - this->read_message(buffer.data_len, buffer.type, - buffer.data_len > 0 ? &buffer.container[buffer.data_offset] : nullptr); + this->read_message(buffer.data_len, buffer.type, buffer.data); if (this->flags_.remove) return; } @@ -195,6 +194,9 @@ void APIConnection::loop() { } // Now that everything is sent, enable immediate sending for future state changes this->flags_.should_try_send_immediately = true; + // Release excess memory from buffers that grew during initial sync + this->deferred_batch_.release_buffer(); + this->helper_->release_buffers(); } } diff --git a/esphome/components/api/api_connection.h b/esphome/components/api/api_connection.h index af3a19909f..458fee6c99 100644 --- a/esphome/components/api/api_connection.h +++ b/esphome/components/api/api_connection.h @@ -576,6 +576,15 @@ class APIConnection final : public APIServerConnection { bool empty() const { return items.empty(); } size_t size() const { return items.size(); } const BatchItem &operator[](size_t index) const { return items[index]; } + // Release excess capacity - only releases if items already empty + void release_buffer() { + // Safe to call: batch is processed before release_buffer is called, + // and if any items remain (partial processing), we must not clear them. + // Use swap trick since shrink_to_fit() is non-binding and may be ignored. + if (items.empty()) { + std::vector().swap(items); + } + } }; // DeferredBatch here (16 bytes, 4-byte aligned) diff --git a/esphome/components/api/api_frame_helper.h b/esphome/components/api/api_frame_helper.h index d931a6e3a9..b582bcea9a 100644 --- a/esphome/components/api/api_frame_helper.h +++ b/esphome/components/api/api_frame_helper.h @@ -35,10 +35,9 @@ struct ClientInfo; class ProtoWriteBuffer; struct ReadPacketBuffer { - std::vector container; - uint16_t type; - uint16_t data_offset; + const uint8_t *data; // Points directly into frame helper's rx_buf_ (valid until next read_packet call) uint16_t data_len; + uint16_t type; }; // Packed packet info structure to minimize memory usage @@ -119,6 +118,22 @@ class APIFrameHelper { uint8_t frame_footer_size() const { return frame_footer_size_; } // Check if socket has data ready to read bool is_socket_ready() const { return socket_ != nullptr && socket_->ready(); } + // Release excess memory from internal buffers after initial sync + void release_buffers() { + // rx_buf_: Safe to clear only if no partial read in progress. + // rx_buf_len_ tracks bytes read so far; if non-zero, we're mid-frame + // and clearing would lose partially received data. + if (this->rx_buf_len_ == 0) { + // Use swap trick since shrink_to_fit() is non-binding and may be ignored + std::vector().swap(this->rx_buf_); + } + // reusable_iovs_: Safe to release unconditionally. + // Only used within write_protobuf_packets() calls - cleared at start, + // populated with pointers, used for writev(), then function returns. + // The iovecs contain stale pointers after the call (data was either sent + // or copied to tx_buf_), and are cleared on next write_protobuf_packets(). + std::vector().swap(this->reusable_iovs_); + } protected: // Buffer containing data to be sent diff --git a/esphome/components/api/api_frame_helper_noise.cpp b/esphome/components/api/api_frame_helper_noise.cpp index f1028fa299..ae69f0b673 100644 --- a/esphome/components/api/api_frame_helper_noise.cpp +++ b/esphome/components/api/api_frame_helper_noise.cpp @@ -407,8 +407,7 @@ APIError APINoiseFrameHelper::read_packet(ReadPacketBuffer *buffer) { return APIError::BAD_DATA_PACKET; } - buffer->container = std::move(this->rx_buf_); - buffer->data_offset = 4; + buffer->data = msg_data + 4; // Skip 4-byte header (type + length) buffer->data_len = data_len; buffer->type = type; return APIError::OK; diff --git a/esphome/components/api/api_frame_helper_plaintext.cpp b/esphome/components/api/api_frame_helper_plaintext.cpp index dcbd35aa32..b5d90b2429 100644 --- a/esphome/components/api/api_frame_helper_plaintext.cpp +++ b/esphome/components/api/api_frame_helper_plaintext.cpp @@ -210,8 +210,7 @@ APIError APIPlaintextFrameHelper::read_packet(ReadPacketBuffer *buffer) { return aerr; } - buffer->container = std::move(this->rx_buf_); - buffer->data_offset = 0; + buffer->data = this->rx_buf_.data(); buffer->data_len = this->rx_header_parsed_len_; buffer->type = this->rx_header_parsed_type_; return APIError::OK; diff --git a/esphome/components/api/api_pb2_service.cpp b/esphome/components/api/api_pb2_service.cpp index 3d28a137c8..45f6ecd30e 100644 --- a/esphome/components/api/api_pb2_service.cpp +++ b/esphome/components/api/api_pb2_service.cpp @@ -13,7 +13,7 @@ void APIServerConnectionBase::log_send_message_(const char *name, const std::str } #endif -void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) { +void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, const uint8_t *msg_data) { switch (msg_type) { case HelloRequest::MESSAGE_TYPE: { HelloRequest msg; @@ -827,7 +827,7 @@ void APIServerConnection::on_z_wave_proxy_frame(const ZWaveProxyFrame &msg) { th void APIServerConnection::on_z_wave_proxy_request(const ZWaveProxyRequest &msg) { this->zwave_proxy_request(msg); } #endif -void APIServerConnection::read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) { +void APIServerConnection::read_message(uint32_t msg_size, uint32_t msg_type, const uint8_t *msg_data) { // Check authentication/connection requirements for messages switch (msg_type) { case HelloRequest::MESSAGE_TYPE: // No setup required diff --git a/esphome/components/api/api_pb2_service.h b/esphome/components/api/api_pb2_service.h index 827b89e23c..6d94046a23 100644 --- a/esphome/components/api/api_pb2_service.h +++ b/esphome/components/api/api_pb2_service.h @@ -218,7 +218,7 @@ class APIServerConnectionBase : public ProtoService { virtual void on_z_wave_proxy_request(const ZWaveProxyRequest &value){}; #endif protected: - void read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) override; + void read_message(uint32_t msg_size, uint32_t msg_type, const uint8_t *msg_data) override; }; class APIServerConnection : public APIServerConnectionBase { @@ -480,7 +480,7 @@ class APIServerConnection : public APIServerConnectionBase { #ifdef USE_ZWAVE_PROXY void on_z_wave_proxy_request(const ZWaveProxyRequest &msg) override; #endif - void read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) override; + void read_message(uint32_t msg_size, uint32_t msg_type, const uint8_t *msg_data) override; }; } // namespace esphome::api diff --git a/esphome/components/api/proto.h b/esphome/components/api/proto.h index e7585924a5..83b6922be1 100644 --- a/esphome/components/api/proto.h +++ b/esphome/components/api/proto.h @@ -846,7 +846,7 @@ class ProtoService { */ virtual ProtoWriteBuffer create_buffer(uint32_t reserve_size) = 0; virtual bool send_buffer(ProtoWriteBuffer buffer, uint8_t message_type) = 0; - virtual void read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) = 0; + virtual void read_message(uint32_t msg_size, uint32_t msg_type, const uint8_t *msg_data) = 0; // Optimized method that pre-allocates buffer based on message size bool send_message_(const ProtoMessage &msg, uint8_t message_type) { diff --git a/script/api_protobuf/api_protobuf.py b/script/api_protobuf/api_protobuf.py index b07a249c8d..3412fac5db 100755 --- a/script/api_protobuf/api_protobuf.py +++ b/script/api_protobuf/api_protobuf.py @@ -2769,8 +2769,8 @@ static const char *const TAG = "api.service"; cases = list(RECEIVE_CASES.items()) cases.sort() hpp += " protected:\n" - hpp += " void read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) override;\n" - out = f"void {class_name}::read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) {{\n" + hpp += " void read_message(uint32_t msg_size, uint32_t msg_type, const uint8_t *msg_data) override;\n" + out = f"void {class_name}::read_message(uint32_t msg_size, uint32_t msg_type, const uint8_t *msg_data) {{\n" out += " switch (msg_type) {\n" for i, (case, ifdef, message_name) in cases: if ifdef is not None: @@ -2878,9 +2878,9 @@ static const char *const TAG = "api.service"; result += "#endif\n" return result - hpp_protected += " void read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) override;\n" + hpp_protected += " void read_message(uint32_t msg_size, uint32_t msg_type, const uint8_t *msg_data) override;\n" - cpp += f"\nvoid {class_name}::read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) {{\n" + cpp += f"\nvoid {class_name}::read_message(uint32_t msg_size, uint32_t msg_type, const uint8_t *msg_data) {{\n" cpp += " // Check authentication/connection requirements for messages\n" cpp += " switch (msg_type) {\n" From 406fa220f548a485e68f800de9308790c78a789d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 26 Nov 2025 18:59:11 -0600 Subject: [PATCH 319/415] logs! --- esphome/components/api/api_connection.h | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/esphome/components/api/api_connection.h b/esphome/components/api/api_connection.h index 458fee6c99..05af0ccde7 100644 --- a/esphome/components/api/api_connection.h +++ b/esphome/components/api/api_connection.h @@ -554,10 +554,8 @@ class APIConnection final : public APIServerConnection { std::vector items; uint32_t batch_start_time{0}; - DeferredBatch() { - // Pre-allocate capacity for typical batch sizes to avoid reallocation - items.reserve(8); - } + // No pre-allocation - log connections never use batching, and for + // connections that do, buffers are released after initial sync anyway // Add item to the batch void add_item(EntityBase *entity, MessageCreator creator, uint8_t message_type, uint8_t estimated_size); From 9c85ec9182bbbbdf9ab057ec2ff931dbcffe4d7e Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Wed, 26 Nov 2025 20:01:35 -0500 Subject: [PATCH 320/415] [esp32] Fix hosted update when there is no wifi (#12123) --- .../components/esp32_hosted/update/esp32_hosted_update.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/esphome/components/esp32_hosted/update/esp32_hosted_update.cpp b/esphome/components/esp32_hosted/update/esp32_hosted_update.cpp index f34a0ae10e..de130ca71f 100644 --- a/esphome/components/esp32_hosted/update/esp32_hosted_update.cpp +++ b/esphome/components/esp32_hosted/update/esp32_hosted_update.cpp @@ -22,6 +22,11 @@ constexpr size_t CHUNK_SIZE = 1500; void Esp32HostedUpdate::setup() { this->update_info_.title = "ESP32 Hosted Coprocessor"; + // if wifi is not present, connect to the coprocessor +#ifndef USE_WIFI + esp_hosted_connect_to_slave(); // NOLINT +#endif + // get coprocessor version esp_hosted_coprocessor_fwver_t ver_info; if (esp_hosted_get_coprocessor_fwversion(&ver_info) == ESP_OK) { From a7a5a0b9a23a048a63b8e40621f39c411b9db8c4 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Wed, 26 Nov 2025 22:46:17 -0500 Subject: [PATCH 321/415] [esp32] Improve IDF component support (#12127) --- esphome/components/esp32/__init__.py | 74 ++++++++++++++----- tests/components/esp32/test.esp32-p4-idf.yaml | 4 + 2 files changed, 61 insertions(+), 17 deletions(-) diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index d372af3e6a..35ef76634b 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -37,6 +37,7 @@ from esphome.const import ( __version__, ) from esphome.core import CORE, HexInt, TimePeriod +from esphome.coroutine import CoroPriority, coroutine_with_priority import esphome.final_validate as fv from esphome.helpers import copy_file_if_changed, write_file_if_changed from esphome.types import ConfigType @@ -262,15 +263,32 @@ def add_idf_component( "deprecated and will be removed in ESPHome 2026.1. If you are seeing this, report " "an issue to the external_component author and ask them to update it." ) + components_registry = CORE.data[KEY_ESP32][KEY_COMPONENTS] if components: for comp in components: - CORE.data[KEY_ESP32][KEY_COMPONENTS][comp] = { + existing = components_registry.get(comp) + if existing and existing.get(KEY_REF) != ref: + _LOGGER.warning( + "IDF component %s version conflict %s replaced by %s", + comp, + existing.get(KEY_REF), + ref, + ) + components_registry[comp] = { KEY_REPO: repo, KEY_REF: ref, KEY_PATH: f"{path}/{comp}" if path else comp, } else: - CORE.data[KEY_ESP32][KEY_COMPONENTS][name] = { + existing = components_registry.get(name) + if existing and existing.get(KEY_REF) != ref: + _LOGGER.warning( + "IDF component %s version conflict %s replaced by %s", + name, + existing.get(KEY_REF), + ref, + ) + components_registry[name] = { KEY_REPO: repo, KEY_REF: ref, KEY_PATH: path, @@ -592,6 +610,14 @@ def require_vfs_dir() -> None: CORE.data[KEY_VFS_DIR_REQUIRED] = True +def _parse_idf_component(value: str) -> ConfigType: + """Parse IDF component shorthand syntax like 'owner/component^version'""" + if "^" not in value: + raise cv.Invalid(f"Invalid IDF component shorthand '{value}'") + name, ref = value.split("^", 1) + return {CONF_NAME: name, CONF_REF: ref} + + def _validate_idf_component(config: ConfigType) -> ConfigType: """Validate IDF component config and warn about deprecated options.""" if CONF_REFRESH in config: @@ -659,14 +685,19 @@ FRAMEWORK_SCHEMA = cv.Schema( ), cv.Optional(CONF_COMPONENTS, default=[]): cv.ensure_list( cv.All( - cv.Schema( - { - cv.Required(CONF_NAME): cv.string_strict, - cv.Optional(CONF_SOURCE): cv.git_ref, - cv.Optional(CONF_REF): cv.string, - cv.Optional(CONF_PATH): cv.string, - cv.Optional(CONF_REFRESH): cv.All(cv.string, cv.source_refresh), - } + cv.Any( + cv.All(cv.string_strict, _parse_idf_component), + cv.Schema( + { + cv.Required(CONF_NAME): cv.string_strict, + cv.Optional(CONF_SOURCE): cv.git_ref, + cv.Optional(CONF_REF): cv.string, + cv.Optional(CONF_PATH): cv.string, + cv.Optional(CONF_REFRESH): cv.All( + cv.string, cv.source_refresh + ), + } + ), ), _validate_idf_component, ) @@ -851,6 +882,18 @@ def _configure_lwip_max_sockets(conf: dict) -> None: add_idf_sdkconfig_option("CONFIG_LWIP_MAX_SOCKETS", max_sockets) +@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.""" + for component in components: + add_idf_component( + name=component[CONF_NAME], + repo=component.get(CONF_SOURCE), + ref=component.get(CONF_REF), + path=component.get(CONF_PATH), + ) + + async def to_code(config): cg.add_platformio_option("board", config[CONF_BOARD]) cg.add_platformio_option("board_upload.flash_size", config[CONF_FLASH_SIZE]) @@ -1097,13 +1140,10 @@ async def to_code(config): for name, value in conf[CONF_SDKCONFIG_OPTIONS].items(): add_idf_sdkconfig_option(name, RawSdkconfigValue(value)) - for component in conf[CONF_COMPONENTS]: - add_idf_component( - name=component[CONF_NAME], - repo=component.get(CONF_SOURCE), - ref=component.get(CONF_REF), - path=component.get(CONF_PATH), - ) + # Components from YAML are added in a separate coroutine with FINAL priority + # Schedule it to run after all other components + if conf[CONF_COMPONENTS]: + CORE.add_job(_add_yaml_idf_components, conf[CONF_COMPONENTS]) APP_PARTITION_SIZES = { diff --git a/tests/components/esp32/test.esp32-p4-idf.yaml b/tests/components/esp32/test.esp32-p4-idf.yaml index a4c930f236..1c243ef459 100644 --- a/tests/components/esp32/test.esp32-p4-idf.yaml +++ b/tests/components/esp32/test.esp32-p4-idf.yaml @@ -4,6 +4,10 @@ esp32: cpu_frequency: 400MHz framework: type: esp-idf + components: + - espressif/mdns^1.8.2 + - name: espressif/esp_hosted + ref: 2.6.6 advanced: enable_idf_experimental_features: yes From 8ecd40608ba2dd7a193a42d56bbb5cb9bfcb7ed6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 26 Nov 2025 22:17:36 -0600 Subject: [PATCH 322/415] [wifi] Save 112 bytes BSS on ESP8266 by calling SDK directly for BSSID --- esphome/components/wifi/wifi_component_esp8266.cpp | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/esphome/components/wifi/wifi_component_esp8266.cpp b/esphome/components/wifi/wifi_component_esp8266.cpp index 540ad3a585..100f0651b9 100644 --- a/esphome/components/wifi/wifi_component_esp8266.cpp +++ b/esphome/components/wifi/wifi_component_esp8266.cpp @@ -878,10 +878,9 @@ network::IPAddress WiFiComponent::wifi_soft_ap_ip() { bssid_t WiFiComponent::wifi_bssid() { bssid_t bssid{}; - uint8_t *raw_bssid = WiFi.BSSID(); - if (raw_bssid != nullptr) { - for (size_t i = 0; i < bssid.size(); i++) - bssid[i] = raw_bssid[i]; + struct station_config conf; + if (wifi_station_get_config(&conf)) { + std::copy_n(conf.bssid, bssid.size(), bssid.begin()); } return bssid; } From 91ff949399f29114433b5f6bf592f0b930e35005 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 26 Nov 2025 22:45:51 -0600 Subject: [PATCH 323/415] [web_server] Replace routing table with if-else chain to save 116 bytes RAM --- esphome/components/web_server/web_server.cpp | 104 ++++++++++++------- 1 file changed, 65 insertions(+), 39 deletions(-) diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index 6bf6524fbc..de237d0434 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -1940,83 +1940,109 @@ void WebServer::handleRequest(AsyncWebServerRequest *request) { // Parse URL for component routing UrlMatch match = match_url(url.c_str(), url.length(), false); - // Component routing using minimal code repetition - struct ComponentRoute { - const char *domain; - void (WebServer::*handler)(AsyncWebServerRequest *, const UrlMatch &); - }; - - static const ComponentRoute ROUTES[] = { + // Route to appropriate handler based on domain + if (false) { // Start chain for else-if macro pattern + } #ifdef USE_SENSOR - {"sensor", &WebServer::handle_sensor_request}, + else if (match.domain_equals("sensor")) { + this->handle_sensor_request(request, match); + } #endif #ifdef USE_SWITCH - {"switch", &WebServer::handle_switch_request}, + else if (match.domain_equals("switch")) { + this->handle_switch_request(request, match); + } #endif #ifdef USE_BUTTON - {"button", &WebServer::handle_button_request}, + else if (match.domain_equals("button")) { + this->handle_button_request(request, match); + } #endif #ifdef USE_BINARY_SENSOR - {"binary_sensor", &WebServer::handle_binary_sensor_request}, + else if (match.domain_equals("binary_sensor")) { + this->handle_binary_sensor_request(request, match); + } #endif #ifdef USE_FAN - {"fan", &WebServer::handle_fan_request}, + else if (match.domain_equals("fan")) { + this->handle_fan_request(request, match); + } #endif #ifdef USE_LIGHT - {"light", &WebServer::handle_light_request}, + else if (match.domain_equals("light")) { + this->handle_light_request(request, match); + } #endif #ifdef USE_TEXT_SENSOR - {"text_sensor", &WebServer::handle_text_sensor_request}, + else if (match.domain_equals("text_sensor")) { + this->handle_text_sensor_request(request, match); + } #endif #ifdef USE_COVER - {"cover", &WebServer::handle_cover_request}, + else if (match.domain_equals("cover")) { + this->handle_cover_request(request, match); + } #endif #ifdef USE_NUMBER - {"number", &WebServer::handle_number_request}, + else if (match.domain_equals("number")) { + this->handle_number_request(request, match); + } #endif #ifdef USE_DATETIME_DATE - {"date", &WebServer::handle_date_request}, + else if (match.domain_equals("date")) { + this->handle_date_request(request, match); + } #endif #ifdef USE_DATETIME_TIME - {"time", &WebServer::handle_time_request}, + else if (match.domain_equals("time")) { + this->handle_time_request(request, match); + } #endif #ifdef USE_DATETIME_DATETIME - {"datetime", &WebServer::handle_datetime_request}, + else if (match.domain_equals("datetime")) { + this->handle_datetime_request(request, match); + } #endif #ifdef USE_TEXT - {"text", &WebServer::handle_text_request}, + else if (match.domain_equals("text")) { + this->handle_text_request(request, match); + } #endif #ifdef USE_SELECT - {"select", &WebServer::handle_select_request}, + else if (match.domain_equals("select")) { + this->handle_select_request(request, match); + } #endif #ifdef USE_CLIMATE - {"climate", &WebServer::handle_climate_request}, + else if (match.domain_equals("climate")) { + this->handle_climate_request(request, match); + } #endif #ifdef USE_LOCK - {"lock", &WebServer::handle_lock_request}, + else if (match.domain_equals("lock")) { + this->handle_lock_request(request, match); + } #endif #ifdef USE_VALVE - {"valve", &WebServer::handle_valve_request}, + else if (match.domain_equals("valve")) { + this->handle_valve_request(request, match); + } #endif #ifdef USE_ALARM_CONTROL_PANEL - {"alarm_control_panel", &WebServer::handle_alarm_control_panel_request}, + else if (match.domain_equals("alarm_control_panel")) { + this->handle_alarm_control_panel_request(request, match); + } #endif #ifdef USE_UPDATE - {"update", &WebServer::handle_update_request}, -#endif - }; - - // Check each route - for (const auto &route : ROUTES) { - if (match.domain_equals(route.domain)) { - (this->*route.handler)(request, match); - return; - } + else if (match.domain_equals("update")) { + this->handle_update_request(request, match); + } +#endif + else { + // No matching handler found - send 404 + ESP_LOGV(TAG, "Request for unknown URL: %s", url.c_str()); + request->send(404, "text/plain", "Not Found"); } - - // No matching handler found - send 404 - ESP_LOGV(TAG, "Request for unknown URL: %s", url.c_str()); - request->send(404, "text/plain", "Not Found"); } bool WebServer::isRequestHandlerTrivial() const { return false; } From 22eea92534ac0b6a4c01a4717fab012ce6226337 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 26 Nov 2025 22:54:45 -0600 Subject: [PATCH 324/415] [light] Replace sparse enum switch with linear search to save 156 bytes RAM --- .../components/light/light_json_schema.cpp | 45 +++++++++---------- 1 file changed, 22 insertions(+), 23 deletions(-) diff --git a/esphome/components/light/light_json_schema.cpp b/esphome/components/light/light_json_schema.cpp index 1c9b92f504..41cb855630 100644 --- a/esphome/components/light/light_json_schema.cpp +++ b/esphome/components/light/light_json_schema.cpp @@ -7,30 +7,29 @@ namespace esphome::light { // See https://www.home-assistant.io/integrations/light.mqtt/#json-schema for documentation on the schema -// Lookup table for color mode strings -static constexpr const char *get_color_mode_json_str(ColorMode mode) { - switch (mode) { - case ColorMode::ON_OFF: - return "onoff"; - case ColorMode::BRIGHTNESS: - return "brightness"; - case ColorMode::WHITE: - return "white"; // not supported by HA in MQTT - case ColorMode::COLOR_TEMPERATURE: - return "color_temp"; - case ColorMode::COLD_WARM_WHITE: - return "cwww"; // not supported by HA - case ColorMode::RGB: - return "rgb"; - case ColorMode::RGB_WHITE: - return "rgbw"; - case ColorMode::RGB_COLOR_TEMPERATURE: - return "rgbct"; // not supported by HA - case ColorMode::RGB_COLD_WARM_WHITE: - return "rgbww"; - default: - return nullptr; +// Get JSON string for color mode using linear search (avoids large switch jump table) +static const char *get_color_mode_json_str(ColorMode mode) { + // Parallel arrays: mode values and their corresponding strings + // Uses less RAM than a switch jump table on sparse enum values + static constexpr ColorMode MODES[] = { + ColorMode::ON_OFF, + ColorMode::BRIGHTNESS, + ColorMode::WHITE, + ColorMode::COLOR_TEMPERATURE, + ColorMode::COLD_WARM_WHITE, + ColorMode::RGB, + ColorMode::RGB_WHITE, + ColorMode::RGB_COLOR_TEMPERATURE, + ColorMode::RGB_COLD_WARM_WHITE, + }; + static constexpr const char *STRINGS[] = { + "onoff", "brightness", "white", "color_temp", "cwww", "rgb", "rgbw", "rgbct", "rgbww", + }; + for (size_t i = 0; i < sizeof(MODES) / sizeof(MODES[0]); i++) { + if (MODES[i] == mode) + return STRINGS[i]; } + return nullptr; } void LightJSONSchema::dump_json(LightState &state, JsonObject root) { From 75b4401cd4b245d547c20d3fb6744da0c80315a7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 26 Nov 2025 22:57:17 -0600 Subject: [PATCH 325/415] disable tidy --- esphome/components/web_server/web_server.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index de237d0434..f048a3007f 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -1941,6 +1941,7 @@ void WebServer::handleRequest(AsyncWebServerRequest *request) { UrlMatch match = match_url(url.c_str(), url.length(), false); // Route to appropriate handler based on domain + // NOLINTNEXTLINE(readability-simplify-boolean-expr) if (false) { // Start chain for else-if macro pattern } #ifdef USE_SENSOR From 91df0548efc10423b5785d45d6c830178ed3dc56 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Fri, 28 Nov 2025 02:30:03 +1100 Subject: [PATCH 326/415] [wifi] Restore blocking setup until connected for RP2040 (#12142) --- esphome/components/wifi/wifi_component.cpp | 14 ++++++++++++++ esphome/components/wifi/wifi_component.h | 4 ++++ 2 files changed, 18 insertions(+) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index d53de83bd3..e67493aa4d 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -1612,6 +1612,20 @@ void WiFiComponent::retry_connect() { } } +#ifdef USE_RP2040 +// RP2040's mDNS library (LEAmDNS) relies on LwipIntf::stateUpCB() to restart +// mDNS when the network interface reconnects. However, this callback is disabled +// in the arduino-pico framework. As a workaround, we block component setup until +// WiFi is connected, ensuring mDNS.begin() is called with an active connection. + +bool WiFiComponent::can_proceed() { + if (!this->has_sta() || this->state_ == WIFI_COMPONENT_STATE_DISABLED || this->ap_setup_) { + return true; + } + return this->is_connected(); +} +#endif + void WiFiComponent::set_reboot_timeout(uint32_t reboot_timeout) { this->reboot_timeout_ = reboot_timeout; } bool WiFiComponent::is_connected() { return this->state_ == WIFI_COMPONENT_STATE_STA_CONNECTED && diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index b6b956a12d..a9b03a8b8d 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -284,6 +284,10 @@ class WiFiComponent : public Component { void retry_connect(); +#ifdef USE_RP2040 + bool can_proceed() override; +#endif + void set_reboot_timeout(uint32_t reboot_timeout); bool is_connected(); From 1fadd1227d531ddb611ec58d9c189f0821cfe796 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 27 Nov 2025 10:50:21 -0600 Subject: [PATCH 327/415] [scheduler] Fix use-after-move crash in heap operations (#12124) --- esphome/core/scheduler.cpp | 26 +++++++++++++------------- esphome/core/scheduler.h | 4 +++- 2 files changed, 16 insertions(+), 14 deletions(-) diff --git a/esphome/core/scheduler.cpp b/esphome/core/scheduler.cpp index 09d50ee7c8..352587bf10 100644 --- a/esphome/core/scheduler.cpp +++ b/esphome/core/scheduler.cpp @@ -359,8 +359,7 @@ void HOT Scheduler::call(uint32_t now) { std::unique_ptr item; { LockGuard guard{this->lock_}; - item = std::move(this->items_[0]); - this->pop_raw_(); + item = this->pop_raw_locked_(); } const char *name = item->get_name(); @@ -401,7 +400,7 @@ void HOT Scheduler::call(uint32_t now) { // Don't run on failed components if (item->component != nullptr && item->component->is_failed()) { LockGuard guard{this->lock_}; - this->pop_raw_(); + this->recycle_item_(this->pop_raw_locked_()); continue; } @@ -414,7 +413,7 @@ void HOT Scheduler::call(uint32_t now) { { LockGuard guard{this->lock_}; if (is_item_removed_(item.get())) { - this->pop_raw_(); + this->recycle_item_(this->pop_raw_locked_()); this->to_remove_--; continue; } @@ -423,7 +422,7 @@ void HOT Scheduler::call(uint32_t now) { // Single-threaded or multi-threaded with atomics: can check without lock if (is_item_removed_(item.get())) { LockGuard guard{this->lock_}; - this->pop_raw_(); + this->recycle_item_(this->pop_raw_locked_()); this->to_remove_--; continue; } @@ -443,14 +442,14 @@ void HOT Scheduler::call(uint32_t now) { LockGuard guard{this->lock_}; - auto executed_item = std::move(this->items_[0]); // Only pop after function call, this ensures we were reachable // during the function call and know if we were cancelled. - this->pop_raw_(); + auto executed_item = this->pop_raw_locked_(); if (executed_item->remove) { - // We were removed/cancelled in the function call, stop + // We were removed/cancelled in the function call, recycle and continue this->to_remove_--; + this->recycle_item_(std::move(executed_item)); continue; } @@ -497,7 +496,7 @@ size_t HOT Scheduler::cleanup_() { return this->items_.size(); // We must hold the lock for the entire cleanup operation because: - // 1. We're modifying items_ (via pop_raw_) which requires exclusive access + // 1. We're modifying items_ (via pop_raw_locked_) which requires exclusive access // 2. We're decrementing to_remove_ which is also modified by other threads // (though all modifications are already under lock) // 3. Other threads read items_ when searching for items to cancel in cancel_item_locked_() @@ -510,17 +509,18 @@ size_t HOT Scheduler::cleanup_() { if (!item->remove) break; this->to_remove_--; - this->pop_raw_(); + this->recycle_item_(this->pop_raw_locked_()); } return this->items_.size(); } -void HOT Scheduler::pop_raw_() { +std::unique_ptr HOT Scheduler::pop_raw_locked_() { std::pop_heap(this->items_.begin(), this->items_.end(), SchedulerItem::cmp); - // Instead of destroying, recycle the item - this->recycle_item_(std::move(this->items_.back())); + // Move the item out before popping - this is the item that was at the front of the heap + auto item = std::move(this->items_.back()); this->items_.pop_back(); + return item; } // Helper to execute a scheduler item diff --git a/esphome/core/scheduler.h b/esphome/core/scheduler.h index bea1503df0..08e003c9fb 100644 --- a/esphome/core/scheduler.h +++ b/esphome/core/scheduler.h @@ -219,7 +219,9 @@ class Scheduler { // Returns the number of items remaining after cleanup // IMPORTANT: This method should only be called from the main thread (loop task). size_t cleanup_(); - void pop_raw_(); + // Remove and return the front item from the heap + // IMPORTANT: Caller must hold the scheduler lock before calling this function. + std::unique_ptr pop_raw_locked_(); private: // Helper to cancel items by name - must be called with lock held From 9289fc36f79222af346394b096ad629b683234d8 Mon Sep 17 00:00:00 2001 From: Edward Firmo <94725493+edwardtfn@users.noreply.github.com> Date: Tue, 25 Nov 2025 13:48:32 +0100 Subject: [PATCH 328/415] [nextion] Do not set alternative baud rate when not specified or `<= 0` (#12097) --- esphome/components/nextion/nextion_upload_arduino.cpp | 3 +++ esphome/components/nextion/nextion_upload_idf.cpp | 3 +++ 2 files changed, 6 insertions(+) diff --git a/esphome/components/nextion/nextion_upload_arduino.cpp b/esphome/components/nextion/nextion_upload_arduino.cpp index b4d217d7aa..baea938729 100644 --- a/esphome/components/nextion/nextion_upload_arduino.cpp +++ b/esphome/components/nextion/nextion_upload_arduino.cpp @@ -174,6 +174,9 @@ bool Nextion::upload_tft(uint32_t baud_rate, bool exit_reparse) { // Check if baud rate is supported this->original_baud_rate_ = this->parent_->get_baud_rate(); + if (baud_rate <= 0) { + baud_rate = this->original_baud_rate_; + } ESP_LOGD(TAG, "Baud rate: %" PRIu32, baud_rate); // Define the configuration for the HTTP client diff --git a/esphome/components/nextion/nextion_upload_idf.cpp b/esphome/components/nextion/nextion_upload_idf.cpp index 3b0d65643d..942e3dd6c3 100644 --- a/esphome/components/nextion/nextion_upload_idf.cpp +++ b/esphome/components/nextion/nextion_upload_idf.cpp @@ -177,6 +177,9 @@ bool Nextion::upload_tft(uint32_t baud_rate, bool exit_reparse) { // Check if baud rate is supported this->original_baud_rate_ = this->parent_->get_baud_rate(); + if (baud_rate <= 0) { + baud_rate = this->original_baud_rate_; + } ESP_LOGD(TAG, "Baud rate: %" PRIu32, baud_rate); // Define the configuration for the HTTP client From acdcd56395fdcad51c652c10b6e289196403ef1b Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Tue, 25 Nov 2025 11:14:53 -0500 Subject: [PATCH 329/415] [esp32] Fix platformio flash size print (#12099) Co-authored-by: J. Nick Koston --- esphome/components/esp32/__init__.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index 59c6029334..d372af3e6a 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -854,6 +854,10 @@ def _configure_lwip_max_sockets(conf: dict) -> None: async def to_code(config): cg.add_platformio_option("board", config[CONF_BOARD]) cg.add_platformio_option("board_upload.flash_size", config[CONF_FLASH_SIZE]) + cg.add_platformio_option( + "board_upload.maximum_size", + int(config[CONF_FLASH_SIZE].removesuffix("MB")) * 1024 * 1024, + ) cg.set_cpp_standard("gnu++20") cg.add_build_flag("-DUSE_ESP32") cg.add_define("ESPHOME_BOARD", config[CONF_BOARD]) From 278f12fb99fd83eefaf261e2ccab396c0cb38d67 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 25 Nov 2025 10:30:01 -0600 Subject: [PATCH 330/415] [script] Fix script.wait hanging when triggered from on_boot (#12102) --- esphome/components/script/script.h | 7 +- .../fixtures/script_wait_on_boot.yaml | 54 ++++++++ tests/integration/test_script_wait_on_boot.py | 130 ++++++++++++++++++ 3 files changed, 190 insertions(+), 1 deletion(-) create mode 100644 tests/integration/fixtures/script_wait_on_boot.yaml create mode 100644 tests/integration/test_script_wait_on_boot.py diff --git a/esphome/components/script/script.h b/esphome/components/script/script.h index d60ed657f7..3a0823f3cc 100644 --- a/esphome/components/script/script.h +++ b/esphome/components/script/script.h @@ -278,7 +278,12 @@ template class ScriptWaitAction : public Action, void setup() override { // Start with loop disabled - only enable when there's work to do - this->disable_loop(); + // IMPORTANT: Only disable if num_running_ is 0, otherwise play_complex() was already + // called before our setup() (e.g., from on_boot trigger at same priority level) + // and we must not undo its enable_loop() call + if (this->num_running_ == 0) { + this->disable_loop(); + } } void play_complex(const Ts &...x) override { diff --git a/tests/integration/fixtures/script_wait_on_boot.yaml b/tests/integration/fixtures/script_wait_on_boot.yaml new file mode 100644 index 0000000000..8736b02294 --- /dev/null +++ b/tests/integration/fixtures/script_wait_on_boot.yaml @@ -0,0 +1,54 @@ +esphome: + name: test-script-wait-on-boot + on_boot: + # Use default priority (600.0) which is same as ScriptWaitAction's setup priority + # This tests the race condition where on_boot runs before ScriptWaitAction::setup() + then: + - logger.log: "=== on_boot: Starting boot sequence ===" + - script.execute: show_start_page + - script.wait: show_start_page + - logger.log: "=== on_boot: First script completed, starting second ===" + - script.execute: flip_thru_pages + - script.wait: flip_thru_pages + - logger.log: "=== on_boot: All boot scripts completed successfully ===" + +host: + +api: + actions: + # Manual trigger for additional testing + - action: test_script_wait + then: + - logger.log: "=== Manual test: Starting ===" + - script.execute: show_start_page + - script.wait: show_start_page + - logger.log: "=== Manual test: First script completed ===" + - script.execute: flip_thru_pages + - script.wait: flip_thru_pages + - logger.log: "=== Manual test: All completed ===" + +logger: + level: DEBUG + +script: + # First script - simulates display initialization + - id: show_start_page + mode: single + then: + - logger.log: "show_start_page: Starting" + - delay: 100ms + - logger.log: "show_start_page: After delay 1" + - delay: 100ms + - logger.log: "show_start_page: Completed" + + # Second script - simulates page flip sequence + - id: flip_thru_pages + mode: single + then: + - logger.log: "flip_thru_pages: Starting" + - delay: 50ms + - logger.log: "flip_thru_pages: Page 1" + - delay: 50ms + - logger.log: "flip_thru_pages: Page 2" + - delay: 50ms + - logger.log: "flip_thru_pages: Completed" diff --git a/tests/integration/test_script_wait_on_boot.py b/tests/integration/test_script_wait_on_boot.py new file mode 100644 index 0000000000..478090f782 --- /dev/null +++ b/tests/integration/test_script_wait_on_boot.py @@ -0,0 +1,130 @@ +"""Integration test for script.wait during on_boot (issue #12043). + +This test verifies that script.wait works correctly when triggered from on_boot. +The issue was that ScriptWaitAction::setup() unconditionally disabled the loop, +even if play_complex() had already been called (from an on_boot trigger at the +same priority level) and enabled it. + +The race condition occurs because: +1. on_boot's default priority is 600.0 (setup_priority::DATA) +2. ScriptWaitAction's default setup priority is also DATA (600.0) +3. When they have the same priority, if on_boot runs first and triggers a script, + ScriptWaitAction::play_complex() enables the loop +4. Then ScriptWaitAction::setup() runs and unconditionally disables the loop +5. The wait never completes because the loop is disabled + +The fix adds a conditional check (like WaitUntilAction has) to only disable the +loop in setup() if num_running_ is 0. +""" + +from __future__ import annotations + +import asyncio +import re + +import pytest + +from .types import APIClientConnectedFactory, RunCompiledFunction + + +@pytest.mark.asyncio +async def test_script_wait_on_boot( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """Test that script.wait works correctly when triggered from on_boot. + + This reproduces issue #12043 where script.wait would hang forever when + triggered from on_boot due to a race condition in ScriptWaitAction::setup(). + """ + test_complete = asyncio.Event() + + # Track progress through the boot sequence + boot_started = False + first_script_started = False + first_script_completed = False + first_wait_returned = False + second_script_started = False + second_script_completed = False + all_completed = False + + # Patterns for boot sequence logs + boot_start_pattern = re.compile(r"on_boot: Starting boot sequence") + show_start_pattern = re.compile(r"show_start_page: Starting") + show_complete_pattern = re.compile(r"show_start_page: Completed") + first_wait_pattern = re.compile(r"on_boot: First script completed") + flip_start_pattern = re.compile(r"flip_thru_pages: Starting") + flip_complete_pattern = re.compile(r"flip_thru_pages: Completed") + all_complete_pattern = re.compile(r"on_boot: All boot scripts completed") + + def check_output(line: str) -> None: + """Check log output for boot sequence progress.""" + nonlocal boot_started, first_script_started, first_script_completed + nonlocal first_wait_returned, second_script_started, second_script_completed + nonlocal all_completed + + if boot_start_pattern.search(line): + boot_started = True + elif show_start_pattern.search(line): + first_script_started = True + elif show_complete_pattern.search(line): + first_script_completed = True + elif first_wait_pattern.search(line): + first_wait_returned = True + elif flip_start_pattern.search(line): + second_script_started = True + elif flip_complete_pattern.search(line): + second_script_completed = True + elif all_complete_pattern.search(line): + all_completed = True + test_complete.set() + + 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 == "test-script-wait-on-boot" + + # Wait for on_boot sequence to complete + # The boot sequence should complete automatically + # Timeout is generous to allow for delays in the scripts + try: + await asyncio.wait_for(test_complete.wait(), timeout=5.0) + except TimeoutError: + # Build a detailed error message showing where the boot sequence got stuck + progress = [] + if boot_started: + progress.append("boot started") + if first_script_started: + progress.append("show_start_page started") + if first_script_completed: + progress.append("show_start_page completed") + if first_wait_returned: + progress.append("first script.wait returned") + if second_script_started: + progress.append("flip_thru_pages started") + if second_script_completed: + progress.append("flip_thru_pages completed") + + if not first_wait_returned and first_script_completed: + pytest.fail( + f"Test timed out - script.wait hung after show_start_page completed! " + f"This is the issue #12043 bug. Progress: {', '.join(progress)}" + ) + else: + pytest.fail( + f"Test timed out. Progress: {', '.join(progress) if progress else 'none'}" + ) + + # Verify the complete boot sequence executed in order + assert boot_started, "on_boot did not start" + assert first_script_started, "show_start_page did not start" + assert first_script_completed, "show_start_page did not complete" + assert first_wait_returned, "First script.wait did not return" + assert second_script_started, "flip_thru_pages did not start" + assert second_script_completed, "flip_thru_pages did not complete" + assert all_completed, "Boot sequence did not complete" From 46ae6d35a237fecea76eb68a4398ac2d7765a5d5 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Wed, 26 Nov 2025 05:06:42 +1000 Subject: [PATCH 331/415] [lvgl] Allow multiple widgets per grid cell (#12091) --- esphome/components/lvgl/layout.py | 9 ++++++++- tests/components/lvgl/lvgl-package.yaml | 5 +++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/esphome/components/lvgl/layout.py b/esphome/components/lvgl/layout.py index a6aa816fda..caa503ef0d 100644 --- a/esphome/components/lvgl/layout.py +++ b/esphome/components/lvgl/layout.py @@ -36,6 +36,8 @@ from .defines import ( ) from .lv_validation import padding, size +CONF_MULTIPLE_WIDGETS_PER_CELL = "multiple_widgets_per_cell" + cell_alignments = LV_CELL_ALIGNMENTS.one_of grid_alignments = LV_GRID_ALIGNMENTS.one_of flex_alignments = LV_FLEX_ALIGNMENTS.one_of @@ -220,6 +222,7 @@ class GridLayout(Layout): cv.Optional(CONF_GRID_ROW_ALIGN): grid_alignments, cv.Optional(CONF_PAD_ROW): padding, cv.Optional(CONF_PAD_COLUMN): padding, + cv.Optional(CONF_MULTIPLE_WIDGETS_PER_CELL, default=False): cv.boolean, }, { cv.Optional(CONF_GRID_CELL_ROW_POS): cv.positive_int, @@ -263,6 +266,7 @@ class GridLayout(Layout): # should be guaranteed to be a dict at this point assert isinstance(layout, dict) assert layout.get(CONF_TYPE).lower() == TYPE_GRID + allow_multiple = layout.get(CONF_MULTIPLE_WIDGETS_PER_CELL, False) rows = len(layout[CONF_GRID_ROWS]) columns = len(layout[CONF_GRID_COLUMNS]) used_cells = [[None] * columns for _ in range(rows)] @@ -299,7 +303,10 @@ class GridLayout(Layout): f"exceeds grid size {rows}x{columns}", [CONF_WIDGETS, index], ) - if used_cells[row + i][column + j] is not None: + if ( + not allow_multiple + and used_cells[row + i][column + j] is not None + ): raise cv.Invalid( f"Cell span {row + i}/{column + j} already occupied by widget at index {used_cells[row + i][column + j]}", [CONF_WIDGETS, index], diff --git a/tests/components/lvgl/lvgl-package.yaml b/tests/components/lvgl/lvgl-package.yaml index cb5b6f59b1..70afd5b3d9 100644 --- a/tests/components/lvgl/lvgl-package.yaml +++ b/tests/components/lvgl/lvgl-package.yaml @@ -881,6 +881,7 @@ lvgl: grid_columns: [40, fr(1), fr(1)] pad_row: 6px pad_column: 0 + multiple_widgets_per_cell: true widgets: - image: grid_cell_row_pos: 0 @@ -905,6 +906,10 @@ lvgl: grid_cell_row_pos: 1 grid_cell_column_pos: 0 text: "Grid cell 1/0" + - label: + grid_cell_row_pos: 1 + grid_cell_column_pos: 0 + text: "Duplicate for 1/0" - label: styles: bdr_style grid_cell_row_pos: 1 From ae140f52e3dbfab83c516a9b5e7d1997fe7b3149 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Wed, 26 Nov 2025 11:47:27 +1000 Subject: [PATCH 332/415] [lvgl] Fix position of errors in widget config (#12111) Co-authored-by: J. Nick Koston --- esphome/components/lvgl/schemas.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/esphome/components/lvgl/schemas.py b/esphome/components/lvgl/schemas.py index 6b77f66abb..b2d463c5fd 100644 --- a/esphome/components/lvgl/schemas.py +++ b/esphome/components/lvgl/schemas.py @@ -1,6 +1,7 @@ from esphome import config_validation as cv from esphome.automation import Trigger, validate_automation from esphome.components.time import RealTimeClock +from esphome.config_validation import prepend_path from esphome.const import ( CONF_ARGS, CONF_FORMAT, @@ -422,7 +423,10 @@ def any_widget_schema(extras=None): def validator(value): if isinstance(value, dict): # Convert to list + is_dict = True value = [{k: v} for k, v in value.items()] + else: + is_dict = False if not isinstance(value, list): raise cv.Invalid("Expected a list of widgets") result = [] @@ -443,7 +447,9 @@ def any_widget_schema(extras=None): ) # Apply custom validation value = widget_type.validate(value or {}) - result.append({key: container_validator(value)}) + path = [key] if is_dict else [index, key] + with prepend_path(path): + result.append({key: container_validator(value)}) return result return validator From 6645994700cd0c9b3bd25e2799bfdc4a2f606fb9 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Wed, 26 Nov 2025 20:01:35 -0500 Subject: [PATCH 333/415] [esp32] Fix hosted update when there is no wifi (#12123) --- .../components/esp32_hosted/update/esp32_hosted_update.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/esphome/components/esp32_hosted/update/esp32_hosted_update.cpp b/esphome/components/esp32_hosted/update/esp32_hosted_update.cpp index adbcc5bf11..6f91d1b3e6 100644 --- a/esphome/components/esp32_hosted/update/esp32_hosted_update.cpp +++ b/esphome/components/esp32_hosted/update/esp32_hosted_update.cpp @@ -22,6 +22,11 @@ constexpr size_t CHUNK_SIZE = 1500; void Esp32HostedUpdate::setup() { this->update_info_.title = "ESP32 Hosted Coprocessor"; + // if wifi is not present, connect to the coprocessor +#ifndef USE_WIFI + esp_hosted_connect_to_slave(); // NOLINT +#endif + // get coprocessor version esp_hosted_coprocessor_fwver_t ver_info; if (esp_hosted_get_coprocessor_fwversion(&ver_info) == ESP_OK) { From b4b34aee13874eb4d3ce4062fa90ff50c1636cdd Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Fri, 28 Nov 2025 02:30:03 +1100 Subject: [PATCH 334/415] [wifi] Restore blocking setup until connected for RP2040 (#12142) --- esphome/components/wifi/wifi_component.cpp | 14 ++++++++++++++ esphome/components/wifi/wifi_component.h | 4 ++++ 2 files changed, 18 insertions(+) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index e31d7bbf32..abf62cb063 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -1555,6 +1555,20 @@ void WiFiComponent::retry_connect() { } } +#ifdef USE_RP2040 +// RP2040's mDNS library (LEAmDNS) relies on LwipIntf::stateUpCB() to restart +// mDNS when the network interface reconnects. However, this callback is disabled +// in the arduino-pico framework. As a workaround, we block component setup until +// WiFi is connected, ensuring mDNS.begin() is called with an active connection. + +bool WiFiComponent::can_proceed() { + if (!this->has_sta() || this->state_ == WIFI_COMPONENT_STATE_DISABLED || this->ap_setup_) { + return true; + } + return this->is_connected(); +} +#endif + void WiFiComponent::set_reboot_timeout(uint32_t reboot_timeout) { this->reboot_timeout_ = reboot_timeout; } bool WiFiComponent::is_connected() { return this->state_ == WIFI_COMPONENT_STATE_STA_CONNECTED && diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index 2e0a9816c6..28eef211d3 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -280,6 +280,10 @@ class WiFiComponent : public Component { void retry_connect(); +#ifdef USE_RP2040 + bool can_proceed() override; +#endif + void set_reboot_timeout(uint32_t reboot_timeout); bool is_connected(); From d5e2543751105d90b31e4e0d6dc9bacd08ca9827 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 27 Nov 2025 10:50:21 -0600 Subject: [PATCH 335/415] [scheduler] Fix use-after-move crash in heap operations (#12124) --- esphome/core/scheduler.cpp | 26 +++++++++++++------------- esphome/core/scheduler.h | 4 +++- 2 files changed, 16 insertions(+), 14 deletions(-) diff --git a/esphome/core/scheduler.cpp b/esphome/core/scheduler.cpp index 09d50ee7c8..352587bf10 100644 --- a/esphome/core/scheduler.cpp +++ b/esphome/core/scheduler.cpp @@ -359,8 +359,7 @@ void HOT Scheduler::call(uint32_t now) { std::unique_ptr item; { LockGuard guard{this->lock_}; - item = std::move(this->items_[0]); - this->pop_raw_(); + item = this->pop_raw_locked_(); } const char *name = item->get_name(); @@ -401,7 +400,7 @@ void HOT Scheduler::call(uint32_t now) { // Don't run on failed components if (item->component != nullptr && item->component->is_failed()) { LockGuard guard{this->lock_}; - this->pop_raw_(); + this->recycle_item_(this->pop_raw_locked_()); continue; } @@ -414,7 +413,7 @@ void HOT Scheduler::call(uint32_t now) { { LockGuard guard{this->lock_}; if (is_item_removed_(item.get())) { - this->pop_raw_(); + this->recycle_item_(this->pop_raw_locked_()); this->to_remove_--; continue; } @@ -423,7 +422,7 @@ void HOT Scheduler::call(uint32_t now) { // Single-threaded or multi-threaded with atomics: can check without lock if (is_item_removed_(item.get())) { LockGuard guard{this->lock_}; - this->pop_raw_(); + this->recycle_item_(this->pop_raw_locked_()); this->to_remove_--; continue; } @@ -443,14 +442,14 @@ void HOT Scheduler::call(uint32_t now) { LockGuard guard{this->lock_}; - auto executed_item = std::move(this->items_[0]); // Only pop after function call, this ensures we were reachable // during the function call and know if we were cancelled. - this->pop_raw_(); + auto executed_item = this->pop_raw_locked_(); if (executed_item->remove) { - // We were removed/cancelled in the function call, stop + // We were removed/cancelled in the function call, recycle and continue this->to_remove_--; + this->recycle_item_(std::move(executed_item)); continue; } @@ -497,7 +496,7 @@ size_t HOT Scheduler::cleanup_() { return this->items_.size(); // We must hold the lock for the entire cleanup operation because: - // 1. We're modifying items_ (via pop_raw_) which requires exclusive access + // 1. We're modifying items_ (via pop_raw_locked_) which requires exclusive access // 2. We're decrementing to_remove_ which is also modified by other threads // (though all modifications are already under lock) // 3. Other threads read items_ when searching for items to cancel in cancel_item_locked_() @@ -510,17 +509,18 @@ size_t HOT Scheduler::cleanup_() { if (!item->remove) break; this->to_remove_--; - this->pop_raw_(); + this->recycle_item_(this->pop_raw_locked_()); } return this->items_.size(); } -void HOT Scheduler::pop_raw_() { +std::unique_ptr HOT Scheduler::pop_raw_locked_() { std::pop_heap(this->items_.begin(), this->items_.end(), SchedulerItem::cmp); - // Instead of destroying, recycle the item - this->recycle_item_(std::move(this->items_.back())); + // Move the item out before popping - this is the item that was at the front of the heap + auto item = std::move(this->items_.back()); this->items_.pop_back(); + return item; } // Helper to execute a scheduler item diff --git a/esphome/core/scheduler.h b/esphome/core/scheduler.h index bea1503df0..08e003c9fb 100644 --- a/esphome/core/scheduler.h +++ b/esphome/core/scheduler.h @@ -219,7 +219,9 @@ class Scheduler { // Returns the number of items remaining after cleanup // IMPORTANT: This method should only be called from the main thread (loop task). size_t cleanup_(); - void pop_raw_(); + // Remove and return the front item from the heap + // IMPORTANT: Caller must hold the scheduler lock before calling this function. + std::unique_ptr pop_raw_locked_(); private: // Helper to cancel items by name - must be called with lock held From 25e3d5bf91a047d06e0b410ed6472d1ca8c01f08 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 27 Nov 2025 16:23:28 -0600 Subject: [PATCH 336/415] [usb_uart] Wake main loop immediately when USB data arrives --- esphome/components/usb_uart/__init__.py | 7 ++++++- esphome/components/usb_uart/usb_uart.cpp | 6 ++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/esphome/components/usb_uart/__init__.py b/esphome/components/usb_uart/__init__.py index a852e1f78b..d9bb58ae3a 100644 --- a/esphome/components/usb_uart/__init__.py +++ b/esphome/components/usb_uart/__init__.py @@ -1,4 +1,5 @@ import esphome.codegen as cg +from esphome.components import socket from esphome.components.uart import ( CONF_DATA_BITS, CONF_PARITY, @@ -17,7 +18,7 @@ from esphome.const import ( ) from esphome.cpp_types import Component -AUTO_LOAD = ["uart", "usb_host", "bytebuffer"] +AUTO_LOAD = ["uart", "usb_host", "bytebuffer", "socket"] CODEOWNERS = ["@clydebarrow"] usb_uart_ns = cg.esphome_ns.namespace("usb_uart") @@ -116,6 +117,10 @@ CONFIG_SCHEMA = cv.ensure_list( async def to_code(config): + # Enable wake_loop_threadsafe for low-latency USB data processing + # The USB task queues data events that need immediate processing + socket.require_wake_loop_threadsafe() + for device in config: var = await register_usb_client(device) for index, channel in enumerate(device[CONF_CHANNELS]): diff --git a/esphome/components/usb_uart/usb_uart.cpp b/esphome/components/usb_uart/usb_uart.cpp index 6720c1e690..fefccd3645 100644 --- a/esphome/components/usb_uart/usb_uart.cpp +++ b/esphome/components/usb_uart/usb_uart.cpp @@ -2,6 +2,7 @@ #if defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) || defined(USE_ESP32_VARIANT_ESP32P4) #include "usb_uart.h" #include "esphome/core/log.h" +#include "esphome/core/application.h" #include "esphome/components/uart/uart_debugger.h" #include @@ -262,6 +263,11 @@ void USBUartComponent::start_input(USBUartChannel *channel) { // Push to lock-free queue for main loop processing // Push always succeeds because pool size == queue size this->usb_data_queue_.push(chunk); + + // Wake main loop immediately to process USB data instead of waiting for select() timeout +#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) + App.wake_loop_threadsafe(); +#endif } // On success, restart input immediately from USB task for performance From 4115dd7222088bae60d3995c06c876be4a1b5e88 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Thu, 27 Nov 2025 17:23:28 -0500 Subject: [PATCH 337/415] Bump version to 2025.11.2 --- Doxyfile | 2 +- esphome/const.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Doxyfile b/Doxyfile index a2b6efcfae..d30bd84257 100644 --- a/Doxyfile +++ b/Doxyfile @@ -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.1 +PROJECT_NUMBER = 2025.11.2 # 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 diff --git a/esphome/const.py b/esphome/const.py index f4ddd01c09..45b726e599 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -4,7 +4,7 @@ from enum import Enum from esphome.enum import StrEnum -__version__ = "2025.11.1" +__version__ = "2025.11.2" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" VALID_SUBSTITUTIONS_CHARACTERS = ( From 4c549798bc9faca98036748b220d122fe55eccd0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 27 Nov 2025 16:33:08 -0600 Subject: [PATCH 338/415] [usb_uart] Wake main loop immediately when USB data arrives (#12148) --- esphome/components/usb_uart/__init__.py | 7 ++++++- esphome/components/usb_uart/usb_uart.cpp | 6 ++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/esphome/components/usb_uart/__init__.py b/esphome/components/usb_uart/__init__.py index a852e1f78b..d9bb58ae3a 100644 --- a/esphome/components/usb_uart/__init__.py +++ b/esphome/components/usb_uart/__init__.py @@ -1,4 +1,5 @@ import esphome.codegen as cg +from esphome.components import socket from esphome.components.uart import ( CONF_DATA_BITS, CONF_PARITY, @@ -17,7 +18,7 @@ from esphome.const import ( ) from esphome.cpp_types import Component -AUTO_LOAD = ["uart", "usb_host", "bytebuffer"] +AUTO_LOAD = ["uart", "usb_host", "bytebuffer", "socket"] CODEOWNERS = ["@clydebarrow"] usb_uart_ns = cg.esphome_ns.namespace("usb_uart") @@ -116,6 +117,10 @@ CONFIG_SCHEMA = cv.ensure_list( async def to_code(config): + # Enable wake_loop_threadsafe for low-latency USB data processing + # The USB task queues data events that need immediate processing + socket.require_wake_loop_threadsafe() + for device in config: var = await register_usb_client(device) for index, channel in enumerate(device[CONF_CHANNELS]): diff --git a/esphome/components/usb_uart/usb_uart.cpp b/esphome/components/usb_uart/usb_uart.cpp index 6720c1e690..fefccd3645 100644 --- a/esphome/components/usb_uart/usb_uart.cpp +++ b/esphome/components/usb_uart/usb_uart.cpp @@ -2,6 +2,7 @@ #if defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) || defined(USE_ESP32_VARIANT_ESP32P4) #include "usb_uart.h" #include "esphome/core/log.h" +#include "esphome/core/application.h" #include "esphome/components/uart/uart_debugger.h" #include @@ -262,6 +263,11 @@ void USBUartComponent::start_input(USBUartChannel *channel) { // Push to lock-free queue for main loop processing // Push always succeeds because pool size == queue size this->usb_data_queue_.push(chunk); + + // Wake main loop immediately to process USB data instead of waiting for select() timeout +#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) + App.wake_loop_threadsafe(); +#endif } // On success, restart input immediately from USB task for performance From 09151e6814116c0e46416dfcd1e1a8764ad0be25 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 27 Nov 2025 18:16:56 -0600 Subject: [PATCH 339/415] [api] Reduce heap usage for Home Assistant service call string storage --- .../components/api/homeassistant_service.h | 26 +++++++------- esphome/core/automation.h | 35 ++++++++++++++----- 2 files changed, 40 insertions(+), 21 deletions(-) diff --git a/esphome/components/api/homeassistant_service.h b/esphome/components/api/homeassistant_service.h index d00e9e6257..01e53031e7 100644 --- a/esphome/components/api/homeassistant_service.h +++ b/esphome/components/api/homeassistant_service.h @@ -46,10 +46,10 @@ template class TemplatableKeyValuePair { // Keys are always string literals from YAML dictionary keys (e.g., "code", "event") // and never templatable values or lambdas. Only the value parameter can be a lambda/template. - // Using pass-by-value with std::move allows optimal performance for both lvalues and rvalues. - template TemplatableKeyValuePair(std::string key, T value) : key(std::move(key)), value(value) {} + // Using const char* avoids std::string heap allocation - keys remain in flash. + template TemplatableKeyValuePair(const char *key, T value) : key(key), value(value) {} - std::string key; + const char *key{nullptr}; TemplatableStringValue value; }; @@ -105,14 +105,15 @@ template class HomeAssistantServiceCallAction : public Action void add_data(K &&key, V &&value) { - this->add_kv_(this->data_, std::forward(key), std::forward(value)); + // Using const char* for keys avoids std::string heap allocation - keys remain in flash. + template void add_data(const char *key, V &&value) { + this->add_kv_(this->data_, key, std::forward(value)); } - template void add_data_template(K &&key, V &&value) { - this->add_kv_(this->data_template_, std::forward(key), std::forward(value)); + template void add_data_template(const char *key, V &&value) { + this->add_kv_(this->data_template_, key, std::forward(value)); } - template void add_variable(K &&key, V &&value) { - this->add_kv_(this->variables_, std::forward(key), std::forward(value)); + template void add_variable(const char *key, V &&value) { + this->add_kv_(this->variables_, key, std::forward(value)); } #ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES @@ -185,10 +186,11 @@ template class HomeAssistantServiceCallAction : public Action void add_kv_(FixedVector> &vec, K &&key, V &&value) { + // Helper to add key-value pairs to FixedVectors + // Keys are always string literals (const char*), values can be lambdas/templates + template void add_kv_(FixedVector> &vec, const char *key, V &&value) { auto &kv = vec.emplace_back(); - kv.key = std::forward(key); + kv.key = key; kv.value = std::forward(value); } diff --git a/esphome/core/automation.h b/esphome/core/automation.h index dacadd35e8..298bb80569 100644 --- a/esphome/core/automation.h +++ b/esphome/core/automation.h @@ -45,6 +45,12 @@ template class TemplatableValue { public: TemplatableValue() : type_(NONE) {} + // For const char* when T is std::string: store pointer directly, no heap allocation + // String remains in flash and is only converted to std::string when value() is called + TemplatableValue(const char *str) requires std::same_as : type_(STATIC_STRING) { + this->static_str_ = str; + } + template TemplatableValue(F value) requires(!std::invocable) : type_(VALUE) { new (&this->value_) T(std::move(value)); } @@ -64,24 +70,28 @@ template class TemplatableValue { // Copy constructor TemplatableValue(const TemplatableValue &other) : type_(other.type_) { - if (type_ == VALUE) { + if (this->type_ == VALUE) { new (&this->value_) T(other.value_); - } else if (type_ == LAMBDA) { + } else if (this->type_ == LAMBDA) { this->f_ = new std::function(*other.f_); - } else if (type_ == STATELESS_LAMBDA) { + } else if (this->type_ == STATELESS_LAMBDA) { this->stateless_f_ = other.stateless_f_; + } else if (this->type_ == STATIC_STRING) { + this->static_str_ = other.static_str_; } } // Move constructor TemplatableValue(TemplatableValue &&other) noexcept : type_(other.type_) { - if (type_ == VALUE) { + if (this->type_ == VALUE) { new (&this->value_) T(std::move(other.value_)); - } else if (type_ == LAMBDA) { + } else if (this->type_ == LAMBDA) { this->f_ = other.f_; other.f_ = nullptr; - } else if (type_ == STATELESS_LAMBDA) { + } else if (this->type_ == STATELESS_LAMBDA) { this->stateless_f_ = other.stateless_f_; + } else if (this->type_ == STATIC_STRING) { + this->static_str_ = other.static_str_; } other.type_ = NONE; } @@ -104,12 +114,12 @@ template class TemplatableValue { } ~TemplatableValue() { - if (type_ == VALUE) { + if (this->type_ == VALUE) { this->value_.~T(); - } else if (type_ == LAMBDA) { + } else if (this->type_ == LAMBDA) { delete this->f_; } - // STATELESS_LAMBDA/NONE: no cleanup needed (function pointer or empty, not heap-allocated) + // STATELESS_LAMBDA/STATIC_STRING/NONE: no cleanup needed (pointers, not heap-allocated) } bool has_value() { return this->type_ != NONE; } @@ -122,6 +132,11 @@ template class TemplatableValue { return (*this->f_)(x...); // std::function call case VALUE: return this->value_; + case STATIC_STRING: + if constexpr (std::same_as) { + return std::string(this->static_str_); // Convert to string only when needed + } + [[fallthrough]]; case NONE: default: return T{}; @@ -148,12 +163,14 @@ template class TemplatableValue { VALUE, LAMBDA, STATELESS_LAMBDA, + STATIC_STRING, // For const char* when T is std::string - avoids heap allocation } type_; union { T value_; std::function *f_; T (*stateless_f_)(X...); + const char *static_str_; // For STATIC_STRING type }; }; From dbc2078b2e817209894107c5fb4331fe86fd6ce4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 27 Nov 2025 18:27:27 -0600 Subject: [PATCH 340/415] assert --- esphome/components/api/homeassistant_service.h | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/esphome/components/api/homeassistant_service.h b/esphome/components/api/homeassistant_service.h index 01e53031e7..fbfd94398a 100644 --- a/esphome/components/api/homeassistant_service.h +++ b/esphome/components/api/homeassistant_service.h @@ -16,6 +16,12 @@ namespace esphome::api { template class TemplatableStringValue : public TemplatableValue { + // Verify that const char* uses the base class STATIC_STRING optimization (no heap allocation) + // rather than being wrapped in a lambda. The base class constructor for const char* is more + // specialized than the templated constructor here, so it should be selected. + static_assert(std::is_constructible_v, const char *>, + "Base class must have const char* constructor for STATIC_STRING optimization"); + private: // Helper to convert value to string - handles the case where value is already a string template static std::string value_to_string(T &&val) { return to_string(std::forward(val)); } From f94e4a30ac9e7afa9d52d67b06c3a7cf31f9d5a5 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 27 Nov 2025 18:41:36 -0600 Subject: [PATCH 341/415] safer --- esphome/core/automation.h | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/esphome/core/automation.h b/esphome/core/automation.h index 298bb80569..61d2944acf 100644 --- a/esphome/core/automation.h +++ b/esphome/core/automation.h @@ -133,10 +133,12 @@ template class TemplatableValue { case VALUE: return this->value_; case STATIC_STRING: + // if constexpr required: code must compile for all T, but STATIC_STRING + // can only be set when T is std::string (enforced by constructor constraint) if constexpr (std::same_as) { - return std::string(this->static_str_); // Convert to string only when needed + return std::string(this->static_str_); } - [[fallthrough]]; + __builtin_unreachable(); case NONE: default: return T{}; From eb2aa62d0d107547eb8ea5732b5b85a07c897a50 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 27 Nov 2025 20:01:15 -0600 Subject: [PATCH 342/415] [logger] Replace std::function callbacks with LogListener interface --- esphome/components/api/api_server.cpp | 29 +++++++----- esphome/components/api/api_server.h | 14 +++++- esphome/components/ble_nus/ble_nus.cpp | 18 ++++--- esphome/components/ble_nus/ble_nus.h | 13 ++++- esphome/components/logger/logger.cpp | 11 ++--- esphome/components/logger/logger.h | 50 ++++++++++++++------ esphome/components/mqtt/mqtt_client.cpp | 22 +++++---- esphome/components/mqtt/mqtt_client.h | 14 +++++- esphome/components/syslog/esphome_syslog.cpp | 9 ++-- esphome/components/syslog/esphome_syslog.h | 4 +- esphome/components/web_server/web_server.cpp | 17 ++++--- esphome/components/web_server/web_server.h | 16 ++++++- 12 files changed, 152 insertions(+), 65 deletions(-) diff --git a/esphome/components/api/api_server.cpp b/esphome/components/api/api_server.cpp index 64f8751c35..de0c4b24c9 100644 --- a/esphome/components/api/api_server.cpp +++ b/esphome/components/api/api_server.cpp @@ -101,19 +101,7 @@ void APIServer::setup() { #ifdef USE_LOGGER if (logger::global_logger != nullptr) { - logger::global_logger->add_on_log_callback( - [this](int level, const char *tag, const char *message, size_t message_len) { - if (this->shutting_down_) { - // Don't try to send logs during shutdown - // as it could result in a recursion and - // we would be filling a buffer we are trying to clear - return; - } - for (auto &c : this->clients_) { - if (!c->flags_.remove && c->get_log_subscription_level() >= level) - c->try_send_log_message(level, tag, message, message_len); - } - }); + logger::global_logger->add_log_listener(this); } #endif @@ -541,6 +529,21 @@ bool APIServer::is_connected(bool state_subscription_only) const { return false; } +#ifdef USE_LOGGER +void APIServer::on_log(uint8_t level, const char *tag, const char *message, size_t message_len) { + if (this->shutting_down_) { + // Don't try to send logs during shutdown + // as it could result in a recursion and + // we would be filling a buffer we are trying to clear + return; + } + for (auto &c : this->clients_) { + if (!c->flags_.remove && c->get_log_subscription_level() >= level) + c->try_send_log_message(level, tag, message, message_len); + } +} +#endif + void APIServer::on_shutdown() { this->shutting_down_ = true; diff --git a/esphome/components/api/api_server.h b/esphome/components/api/api_server.h index 428429418a..57aea6ad0e 100644 --- a/esphome/components/api/api_server.h +++ b/esphome/components/api/api_server.h @@ -15,6 +15,9 @@ #ifdef USE_API_USER_DEFINED_ACTIONS #include "user_services.h" #endif +#ifdef USE_LOGGER +#include "esphome/components/logger/logger.h" +#endif #include #include @@ -27,7 +30,13 @@ struct SavedNoisePsk { } PACKED; // NOLINT #endif -class APIServer : public Component, public Controller { +class APIServer : public Component, + public Controller +#ifdef USE_LOGGER + , + public logger::LogListener +#endif +{ public: APIServer(); void setup() override; @@ -37,6 +46,9 @@ class APIServer : public Component, public Controller { void dump_config() override; void on_shutdown() override; bool teardown() override; +#ifdef USE_LOGGER + void on_log(uint8_t level, const char *tag, const char *message, size_t message_len) override; +#endif #ifdef USE_API_PASSWORD bool check_password(const uint8_t *password_data, size_t password_len) const; void set_password(const std::string &password); diff --git a/esphome/components/ble_nus/ble_nus.cpp b/esphome/components/ble_nus/ble_nus.cpp index 9c4d0a3938..bd80592d89 100644 --- a/esphome/components/ble_nus/ble_nus.cpp +++ b/esphome/components/ble_nus/ble_nus.cpp @@ -87,17 +87,21 @@ void BLENUS::setup() { global_ble_nus = this; #ifdef USE_LOGGER if (logger::global_logger != nullptr && this->expose_log_) { - logger::global_logger->add_on_log_callback( - [this](int level, const char *tag, const char *message, size_t message_len) { - this->write_array(reinterpret_cast(message), message_len); - const char c = '\n'; - this->write_array(reinterpret_cast(&c), 1); - }); + logger::global_logger->add_log_listener(this); } - #endif } +#ifdef USE_LOGGER +void BLENUS::on_log(uint8_t level, const char *tag, const char *message, size_t message_len) { + (void) level; + (void) tag; + this->write_array(reinterpret_cast(message), message_len); + const char c = '\n'; + this->write_array(reinterpret_cast(&c), 1); +} +#endif + void BLENUS::dump_config() { ESP_LOGCONFIG(TAG, "ble nus:"); ESP_LOGCONFIG(TAG, " log: %s", YESNO(this->expose_log_)); diff --git a/esphome/components/ble_nus/ble_nus.h b/esphome/components/ble_nus/ble_nus.h index e8cba32b4c..ef20fc5e5b 100644 --- a/esphome/components/ble_nus/ble_nus.h +++ b/esphome/components/ble_nus/ble_nus.h @@ -2,12 +2,20 @@ #ifdef USE_ZEPHYR #include "esphome/core/defines.h" #include "esphome/core/component.h" +#ifdef USE_LOGGER +#include "esphome/components/logger/logger.h" +#endif #include #include namespace esphome::ble_nus { -class BLENUS : public Component { +class BLENUS : public Component +#ifdef USE_LOGGER + , + public logger::LogListener +#endif +{ enum TxStatus { TX_DISABLED, TX_ENABLED, @@ -20,6 +28,9 @@ class BLENUS : public Component { void loop() override; size_t write_array(const uint8_t *data, size_t len); void set_expose_log(bool expose_log) { this->expose_log_ = expose_log; } +#ifdef USE_LOGGER + void on_log(uint8_t level, const char *tag, const char *message, size_t message_len) override; +#endif protected: static void send_enabled_callback(bt_nus_send_status status); diff --git a/esphome/components/logger/logger.cpp b/esphome/components/logger/logger.cpp index 9803bf528c..f925e85e11 100644 --- a/esphome/components/logger/logger.cpp +++ b/esphome/components/logger/logger.cpp @@ -140,8 +140,9 @@ void Logger::log_vprintf_(uint8_t level, const char *tag, int line, const __Flas uint16_t msg_length = this->tx_buffer_at_ - msg_start; // Don't subtract 1 - tx_buffer_at_ is already at the null terminator position - // Callbacks get message first (before console write) - this->log_callback_.call(level, tag, this->tx_buffer_ + msg_start, msg_length); + // Listeners get message first (before console write) + for (auto *listener : this->log_listeners_) + listener->on_log(level, tag, this->tx_buffer_ + msg_start, msg_length); // Write to console starting at the msg_start this->write_tx_buffer_to_console_(msg_start, &msg_length); @@ -203,7 +204,8 @@ void Logger::process_messages_() { this->write_footer_to_buffer_(this->tx_buffer_, &this->tx_buffer_at_, this->tx_buffer_size_); this->tx_buffer_[this->tx_buffer_at_] = '\0'; size_t msg_len = this->tx_buffer_at_; // We already know the length from tx_buffer_at_ - this->log_callback_.call(message->level, message->tag, this->tx_buffer_, msg_len); + for (auto *listener : this->log_listeners_) + listener->on_log(message->level, message->tag, this->tx_buffer_, msg_len); // At this point all the data we need from message has been transferred to the tx_buffer // so we can release the message to allow other tasks to use it as soon as possible. this->log_buffer_->release_message_main_loop(received_token); @@ -231,9 +233,6 @@ void Logger::set_log_level(const char *tag, uint8_t log_level) { this->log_level UARTSelection Logger::get_uart() const { return this->uart_; } #endif -void Logger::add_on_log_callback(std::function &&callback) { - this->log_callback_.add(std::move(callback)); -} float Logger::get_setup_priority() const { return setup_priority::BUS + 500.0f; } #ifdef USE_STORE_LOG_STR_IN_FLASH diff --git a/esphome/components/logger/logger.h b/esphome/components/logger/logger.h index 6a8b640331..87a485ca52 100644 --- a/esphome/components/logger/logger.h +++ b/esphome/components/logger/logger.h @@ -36,6 +36,28 @@ struct device; namespace esphome::logger { +/** Interface for receiving log messages without std::function overhead. + * + * Components can implement this interface instead of using lambdas with std::function + * to avoid the ~600 bytes per-type cost of std::function type erasure machinery. + * + * Usage: + * class MyComponent : public Component, public LogListener { + * public: + * void setup() override { + * if (logger::global_logger != nullptr) + * logger::global_logger->add_log_listener(this); + * } + * void on_log(uint8_t level, const char *tag, const char *message, size_t message_len) override { + * // Handle log message + * } + * }; + */ +class LogListener { + public: + virtual void on_log(uint8_t level, const char *tag, const char *message, size_t message_len) = 0; +}; + #ifdef USE_LOGGER_RUNTIME_TAG_LEVELS // Comparison function for const char* keys in log_levels_ map struct CStrCompare { @@ -168,8 +190,8 @@ class Logger : public Component { inline uint8_t level_for(const char *tag); - /// Register a callback that will be called for every log message sent - void add_on_log_callback(std::function &&callback); + /// Register a log listener to receive log messages + void add_log_listener(LogListener *listener) { this->log_listeners_.push_back(listener); } // add a listener for log level changes void add_listener(std::function &&callback) { this->level_callback_.add(std::move(callback)); } @@ -240,7 +262,7 @@ class Logger : public Component { } } - // Helper to format and send a log message to both console and callbacks + // Helper to format and send a log message to both console and listeners inline void HOT log_message_to_buffer_and_send_(uint8_t level, const char *tag, int line, const char *format, va_list args) { // Format to tx_buffer and prepare for output @@ -248,8 +270,9 @@ class Logger : public Component { this->format_log_to_buffer_with_terminator_(level, tag, line, format, args, this->tx_buffer_, &this->tx_buffer_at_, this->tx_buffer_size_); - // Callbacks get message WITHOUT newline (for API/MQTT/syslog) - this->log_callback_.call(level, tag, this->tx_buffer_, this->tx_buffer_at_); + // Listeners get message WITHOUT newline (for API/MQTT/syslog) + for (auto *listener : this->log_listeners_) + listener->on_log(level, tag, this->tx_buffer_, this->tx_buffer_at_); // Console gets message WITH newline (if platform needs it) this->write_tx_buffer_to_console_(); @@ -301,7 +324,7 @@ class Logger : public Component { #ifdef USE_LOGGER_RUNTIME_TAG_LEVELS std::map log_levels_{}; #endif - CallbackManager log_callback_{}; + std::vector log_listeners_; // Log message listeners (API, MQTT, syslog, etc.) CallbackManager level_callback_{}; #ifdef USE_ESPHOME_TASK_LOG_BUFFER std::unique_ptr log_buffer_; // Will be initialized with init_log_buffer @@ -496,15 +519,14 @@ class Logger : public Component { }; extern Logger *global_logger; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) -class LoggerMessageTrigger : public Trigger { +class LoggerMessageTrigger : public Trigger, public LogListener { public: - explicit LoggerMessageTrigger(Logger *parent, uint8_t level) { - this->level_ = level; - parent->add_on_log_callback([this](uint8_t level, const char *tag, const char *message, size_t message_len) { - if (level <= this->level_) { - this->trigger(level, tag, message); - } - }); + explicit LoggerMessageTrigger(Logger *parent, uint8_t level) : level_(level) { parent->add_log_listener(this); } + + void on_log(uint8_t level, const char *tag, const char *message, size_t message_len) override { + if (level <= this->level_) { + this->trigger(level, tag, message); + } } protected: diff --git a/esphome/components/mqtt/mqtt_client.cpp b/esphome/components/mqtt/mqtt_client.cpp index a810d98adf..ba701b90a3 100644 --- a/esphome/components/mqtt/mqtt_client.cpp +++ b/esphome/components/mqtt/mqtt_client.cpp @@ -57,15 +57,7 @@ void MQTTClientComponent::setup() { }); #ifdef USE_LOGGER if (this->is_log_message_enabled() && logger::global_logger != nullptr) { - logger::global_logger->add_on_log_callback( - [this](int level, const char *tag, const char *message, size_t message_len) { - if (level <= this->log_level_ && this->is_connected()) { - this->publish({.topic = this->log_message_.topic, - .payload = std::string(message, message_len), - .qos = this->log_message_.qos, - .retain = this->log_message_.retain}); - } - }); + logger::global_logger->add_log_listener(this); } #endif @@ -148,6 +140,18 @@ void MQTTClientComponent::send_device_info_() { // NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks) } +#ifdef USE_LOGGER +void MQTTClientComponent::on_log(uint8_t level, const char *tag, const char *message, size_t message_len) { + (void) tag; + if (level <= this->log_level_ && this->is_connected()) { + this->publish({.topic = this->log_message_.topic, + .payload = std::string(message, message_len), + .qos = this->log_message_.qos, + .retain = this->log_message_.retain}); + } +} +#endif + void MQTTClientComponent::dump_config() { ESP_LOGCONFIG(TAG, "MQTT:\n" diff --git a/esphome/components/mqtt/mqtt_client.h b/esphome/components/mqtt/mqtt_client.h index 79383ee857..8547fe337f 100644 --- a/esphome/components/mqtt/mqtt_client.h +++ b/esphome/components/mqtt/mqtt_client.h @@ -10,6 +10,9 @@ #include "esphome/core/component.h" #include "esphome/core/helpers.h" #include "esphome/core/log.h" +#ifdef USE_LOGGER +#include "esphome/components/logger/logger.h" +#endif #if defined(USE_ESP32) #include "mqtt_backend_esp32.h" #elif defined(USE_ESP8266) @@ -97,7 +100,12 @@ enum MQTTClientState { class MQTTComponent; -class MQTTClientComponent : public Component { +class MQTTClientComponent : public Component +#ifdef USE_LOGGER + , + public logger::LogListener +#endif +{ public: MQTTClientComponent(); @@ -238,6 +246,10 @@ class MQTTClientComponent : public Component { /// MQTT client setup priority float get_setup_priority() const override; +#ifdef USE_LOGGER + void on_log(uint8_t level, const char *tag, const char *message, size_t message_len) override; +#endif + void on_message(const std::string &topic, const std::string &payload); bool can_proceed() override; diff --git a/esphome/components/syslog/esphome_syslog.cpp b/esphome/components/syslog/esphome_syslog.cpp index 71468fa932..f5c20c891e 100644 --- a/esphome/components/syslog/esphome_syslog.cpp +++ b/esphome/components/syslog/esphome_syslog.cpp @@ -19,11 +19,10 @@ constexpr int LOG_LEVEL_TO_SYSLOG_SEVERITY[] = { 7 // VERY_VERBOSE }; -void Syslog::setup() { - logger::global_logger->add_on_log_callback( - [this](int level, const char *tag, const char *message, size_t message_len) { - this->log_(level, tag, message, message_len); - }); +void Syslog::setup() { logger::global_logger->add_log_listener(this); } + +void Syslog::on_log(uint8_t level, const char *tag, const char *message, size_t message_len) { + this->log_(level, tag, message, message_len); } void Syslog::log_(const int level, const char *tag, const char *message, size_t message_len) const { diff --git a/esphome/components/syslog/esphome_syslog.h b/esphome/components/syslog/esphome_syslog.h index e3b2f7dae5..1010993265 100644 --- a/esphome/components/syslog/esphome_syslog.h +++ b/esphome/components/syslog/esphome_syslog.h @@ -2,16 +2,18 @@ #include "esphome/core/component.h" #include "esphome/core/helpers.h" #include "esphome/core/log.h" +#include "esphome/components/logger/logger.h" #include "esphome/components/udp/udp_component.h" #include "esphome/components/time/real_time_clock.h" #ifdef USE_NETWORK namespace esphome { namespace syslog { -class Syslog : public Component, public Parented { +class Syslog : public Component, public Parented, public logger::LogListener { public: Syslog(int level, time::RealTimeClock *time) : log_level_(level), time_(time) {} void setup() override; + void on_log(uint8_t level, const char *tag, const char *message, size_t message_len) override; void set_strip(bool strip) { this->strip_ = strip; } void set_facility(int facility) { this->facility_ = facility; } diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index 6bf6524fbc..f5ca674161 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -301,12 +301,7 @@ void WebServer::setup() { #ifdef USE_LOGGER if (logger::global_logger != nullptr && this->expose_log_) { - logger::global_logger->add_on_log_callback( - // logs are not deferred, the memory overhead would be too large - [this](int level, const char *tag, const char *message, size_t message_len) { - (void) message_len; - this->events_.try_send_nodefer(message, "log", millis()); - }); + logger::global_logger->add_log_listener(this); } #endif @@ -322,6 +317,16 @@ void WebServer::setup() { this->set_interval(10000, [this]() { this->events_.try_send_nodefer("", "ping", millis(), 30000); }); } void WebServer::loop() { this->events_.loop(); } + +#ifdef USE_LOGGER +void WebServer::on_log(uint8_t level, const char *tag, const char *message, size_t message_len) { + (void) level; + (void) tag; + (void) message_len; + this->events_.try_send_nodefer(message, "log", millis()); +} +#endif + void WebServer::dump_config() { ESP_LOGCONFIG(TAG, "Web Server:\n" diff --git a/esphome/components/web_server/web_server.h b/esphome/components/web_server/web_server.h index 7e1af88645..52cf0bedea 100644 --- a/esphome/components/web_server/web_server.h +++ b/esphome/components/web_server/web_server.h @@ -7,6 +7,9 @@ #include "esphome/core/component.h" #include "esphome/core/controller.h" #include "esphome/core/entity_base.h" +#ifdef USE_LOGGER +#include "esphome/components/logger/logger.h" +#endif #include #include @@ -170,7 +173,14 @@ class DeferredUpdateEventSourceList : public std::list Date: Thu, 27 Nov 2025 20:02:31 -0600 Subject: [PATCH 343/415] [logger] Replace std::function callbacks with LogListener interface --- esphome/components/logger/logger.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/logger/logger.h b/esphome/components/logger/logger.h index 87a485ca52..3b7710a64a 100644 --- a/esphome/components/logger/logger.h +++ b/esphome/components/logger/logger.h @@ -39,7 +39,7 @@ namespace esphome::logger { /** Interface for receiving log messages without std::function overhead. * * Components can implement this interface instead of using lambdas with std::function - * to avoid the ~600 bytes per-type cost of std::function type erasure machinery. + * to reduce flash usage from std::function type erasure machinery. * * Usage: * class MyComponent : public Component, public LogListener { From 5c6d60ca2e702d494ce47b2f68059eeddce47594 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 27 Nov 2025 20:53:49 -0600 Subject: [PATCH 344/415] unused param --- esphome/components/logger/logger.h | 1 + 1 file changed, 1 insertion(+) diff --git a/esphome/components/logger/logger.h b/esphome/components/logger/logger.h index 3b7710a64a..a0024411d7 100644 --- a/esphome/components/logger/logger.h +++ b/esphome/components/logger/logger.h @@ -524,6 +524,7 @@ class LoggerMessageTrigger : public Trigger explicit LoggerMessageTrigger(Logger *parent, uint8_t level) : level_(level) { parent->add_log_listener(this); } void on_log(uint8_t level, const char *tag, const char *message, size_t message_len) override { + (void) message_len; if (level <= this->level_) { this->trigger(level, tag, message); } From 71dc402a30190fa91af8d5d55d48f697a121ba64 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 27 Nov 2025 22:00:33 -0600 Subject: [PATCH 345/415] [logger] Replace std::function callbacks with LogListener interface (#12153) --- esphome/components/api/api_server.cpp | 29 ++++++----- esphome/components/api/api_server.h | 14 +++++- esphome/components/ble_nus/ble_nus.cpp | 18 ++++--- esphome/components/ble_nus/ble_nus.h | 13 ++++- esphome/components/logger/logger.cpp | 11 ++--- esphome/components/logger/logger.h | 51 ++++++++++++++------ esphome/components/mqtt/mqtt_client.cpp | 22 +++++---- esphome/components/mqtt/mqtt_client.h | 14 +++++- esphome/components/syslog/esphome_syslog.cpp | 9 ++-- esphome/components/syslog/esphome_syslog.h | 4 +- esphome/components/web_server/web_server.cpp | 17 ++++--- esphome/components/web_server/web_server.h | 16 +++++- 12 files changed, 153 insertions(+), 65 deletions(-) diff --git a/esphome/components/api/api_server.cpp b/esphome/components/api/api_server.cpp index 64f8751c35..de0c4b24c9 100644 --- a/esphome/components/api/api_server.cpp +++ b/esphome/components/api/api_server.cpp @@ -101,19 +101,7 @@ void APIServer::setup() { #ifdef USE_LOGGER if (logger::global_logger != nullptr) { - logger::global_logger->add_on_log_callback( - [this](int level, const char *tag, const char *message, size_t message_len) { - if (this->shutting_down_) { - // Don't try to send logs during shutdown - // as it could result in a recursion and - // we would be filling a buffer we are trying to clear - return; - } - for (auto &c : this->clients_) { - if (!c->flags_.remove && c->get_log_subscription_level() >= level) - c->try_send_log_message(level, tag, message, message_len); - } - }); + logger::global_logger->add_log_listener(this); } #endif @@ -541,6 +529,21 @@ bool APIServer::is_connected(bool state_subscription_only) const { return false; } +#ifdef USE_LOGGER +void APIServer::on_log(uint8_t level, const char *tag, const char *message, size_t message_len) { + if (this->shutting_down_) { + // Don't try to send logs during shutdown + // as it could result in a recursion and + // we would be filling a buffer we are trying to clear + return; + } + for (auto &c : this->clients_) { + if (!c->flags_.remove && c->get_log_subscription_level() >= level) + c->try_send_log_message(level, tag, message, message_len); + } +} +#endif + void APIServer::on_shutdown() { this->shutting_down_ = true; diff --git a/esphome/components/api/api_server.h b/esphome/components/api/api_server.h index 428429418a..57aea6ad0e 100644 --- a/esphome/components/api/api_server.h +++ b/esphome/components/api/api_server.h @@ -15,6 +15,9 @@ #ifdef USE_API_USER_DEFINED_ACTIONS #include "user_services.h" #endif +#ifdef USE_LOGGER +#include "esphome/components/logger/logger.h" +#endif #include #include @@ -27,7 +30,13 @@ struct SavedNoisePsk { } PACKED; // NOLINT #endif -class APIServer : public Component, public Controller { +class APIServer : public Component, + public Controller +#ifdef USE_LOGGER + , + public logger::LogListener +#endif +{ public: APIServer(); void setup() override; @@ -37,6 +46,9 @@ class APIServer : public Component, public Controller { void dump_config() override; void on_shutdown() override; bool teardown() override; +#ifdef USE_LOGGER + void on_log(uint8_t level, const char *tag, const char *message, size_t message_len) override; +#endif #ifdef USE_API_PASSWORD bool check_password(const uint8_t *password_data, size_t password_len) const; void set_password(const std::string &password); diff --git a/esphome/components/ble_nus/ble_nus.cpp b/esphome/components/ble_nus/ble_nus.cpp index 9c4d0a3938..bd80592d89 100644 --- a/esphome/components/ble_nus/ble_nus.cpp +++ b/esphome/components/ble_nus/ble_nus.cpp @@ -87,17 +87,21 @@ void BLENUS::setup() { global_ble_nus = this; #ifdef USE_LOGGER if (logger::global_logger != nullptr && this->expose_log_) { - logger::global_logger->add_on_log_callback( - [this](int level, const char *tag, const char *message, size_t message_len) { - this->write_array(reinterpret_cast(message), message_len); - const char c = '\n'; - this->write_array(reinterpret_cast(&c), 1); - }); + logger::global_logger->add_log_listener(this); } - #endif } +#ifdef USE_LOGGER +void BLENUS::on_log(uint8_t level, const char *tag, const char *message, size_t message_len) { + (void) level; + (void) tag; + this->write_array(reinterpret_cast(message), message_len); + const char c = '\n'; + this->write_array(reinterpret_cast(&c), 1); +} +#endif + void BLENUS::dump_config() { ESP_LOGCONFIG(TAG, "ble nus:"); ESP_LOGCONFIG(TAG, " log: %s", YESNO(this->expose_log_)); diff --git a/esphome/components/ble_nus/ble_nus.h b/esphome/components/ble_nus/ble_nus.h index e8cba32b4c..ef20fc5e5b 100644 --- a/esphome/components/ble_nus/ble_nus.h +++ b/esphome/components/ble_nus/ble_nus.h @@ -2,12 +2,20 @@ #ifdef USE_ZEPHYR #include "esphome/core/defines.h" #include "esphome/core/component.h" +#ifdef USE_LOGGER +#include "esphome/components/logger/logger.h" +#endif #include #include namespace esphome::ble_nus { -class BLENUS : public Component { +class BLENUS : public Component +#ifdef USE_LOGGER + , + public logger::LogListener +#endif +{ enum TxStatus { TX_DISABLED, TX_ENABLED, @@ -20,6 +28,9 @@ class BLENUS : public Component { void loop() override; size_t write_array(const uint8_t *data, size_t len); void set_expose_log(bool expose_log) { this->expose_log_ = expose_log; } +#ifdef USE_LOGGER + void on_log(uint8_t level, const char *tag, const char *message, size_t message_len) override; +#endif protected: static void send_enabled_callback(bt_nus_send_status status); diff --git a/esphome/components/logger/logger.cpp b/esphome/components/logger/logger.cpp index 9803bf528c..f925e85e11 100644 --- a/esphome/components/logger/logger.cpp +++ b/esphome/components/logger/logger.cpp @@ -140,8 +140,9 @@ void Logger::log_vprintf_(uint8_t level, const char *tag, int line, const __Flas uint16_t msg_length = this->tx_buffer_at_ - msg_start; // Don't subtract 1 - tx_buffer_at_ is already at the null terminator position - // Callbacks get message first (before console write) - this->log_callback_.call(level, tag, this->tx_buffer_ + msg_start, msg_length); + // Listeners get message first (before console write) + for (auto *listener : this->log_listeners_) + listener->on_log(level, tag, this->tx_buffer_ + msg_start, msg_length); // Write to console starting at the msg_start this->write_tx_buffer_to_console_(msg_start, &msg_length); @@ -203,7 +204,8 @@ void Logger::process_messages_() { this->write_footer_to_buffer_(this->tx_buffer_, &this->tx_buffer_at_, this->tx_buffer_size_); this->tx_buffer_[this->tx_buffer_at_] = '\0'; size_t msg_len = this->tx_buffer_at_; // We already know the length from tx_buffer_at_ - this->log_callback_.call(message->level, message->tag, this->tx_buffer_, msg_len); + for (auto *listener : this->log_listeners_) + listener->on_log(message->level, message->tag, this->tx_buffer_, msg_len); // At this point all the data we need from message has been transferred to the tx_buffer // so we can release the message to allow other tasks to use it as soon as possible. this->log_buffer_->release_message_main_loop(received_token); @@ -231,9 +233,6 @@ void Logger::set_log_level(const char *tag, uint8_t log_level) { this->log_level UARTSelection Logger::get_uart() const { return this->uart_; } #endif -void Logger::add_on_log_callback(std::function &&callback) { - this->log_callback_.add(std::move(callback)); -} float Logger::get_setup_priority() const { return setup_priority::BUS + 500.0f; } #ifdef USE_STORE_LOG_STR_IN_FLASH diff --git a/esphome/components/logger/logger.h b/esphome/components/logger/logger.h index 6a8b640331..a0024411d7 100644 --- a/esphome/components/logger/logger.h +++ b/esphome/components/logger/logger.h @@ -36,6 +36,28 @@ struct device; namespace esphome::logger { +/** Interface for receiving log messages without std::function overhead. + * + * Components can implement this interface instead of using lambdas with std::function + * to reduce flash usage from std::function type erasure machinery. + * + * Usage: + * class MyComponent : public Component, public LogListener { + * public: + * void setup() override { + * if (logger::global_logger != nullptr) + * logger::global_logger->add_log_listener(this); + * } + * void on_log(uint8_t level, const char *tag, const char *message, size_t message_len) override { + * // Handle log message + * } + * }; + */ +class LogListener { + public: + virtual void on_log(uint8_t level, const char *tag, const char *message, size_t message_len) = 0; +}; + #ifdef USE_LOGGER_RUNTIME_TAG_LEVELS // Comparison function for const char* keys in log_levels_ map struct CStrCompare { @@ -168,8 +190,8 @@ class Logger : public Component { inline uint8_t level_for(const char *tag); - /// Register a callback that will be called for every log message sent - void add_on_log_callback(std::function &&callback); + /// Register a log listener to receive log messages + void add_log_listener(LogListener *listener) { this->log_listeners_.push_back(listener); } // add a listener for log level changes void add_listener(std::function &&callback) { this->level_callback_.add(std::move(callback)); } @@ -240,7 +262,7 @@ class Logger : public Component { } } - // Helper to format and send a log message to both console and callbacks + // Helper to format and send a log message to both console and listeners inline void HOT log_message_to_buffer_and_send_(uint8_t level, const char *tag, int line, const char *format, va_list args) { // Format to tx_buffer and prepare for output @@ -248,8 +270,9 @@ class Logger : public Component { this->format_log_to_buffer_with_terminator_(level, tag, line, format, args, this->tx_buffer_, &this->tx_buffer_at_, this->tx_buffer_size_); - // Callbacks get message WITHOUT newline (for API/MQTT/syslog) - this->log_callback_.call(level, tag, this->tx_buffer_, this->tx_buffer_at_); + // Listeners get message WITHOUT newline (for API/MQTT/syslog) + for (auto *listener : this->log_listeners_) + listener->on_log(level, tag, this->tx_buffer_, this->tx_buffer_at_); // Console gets message WITH newline (if platform needs it) this->write_tx_buffer_to_console_(); @@ -301,7 +324,7 @@ class Logger : public Component { #ifdef USE_LOGGER_RUNTIME_TAG_LEVELS std::map log_levels_{}; #endif - CallbackManager log_callback_{}; + std::vector log_listeners_; // Log message listeners (API, MQTT, syslog, etc.) CallbackManager level_callback_{}; #ifdef USE_ESPHOME_TASK_LOG_BUFFER std::unique_ptr log_buffer_; // Will be initialized with init_log_buffer @@ -496,15 +519,15 @@ class Logger : public Component { }; extern Logger *global_logger; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) -class LoggerMessageTrigger : public Trigger { +class LoggerMessageTrigger : public Trigger, public LogListener { public: - explicit LoggerMessageTrigger(Logger *parent, uint8_t level) { - this->level_ = level; - parent->add_on_log_callback([this](uint8_t level, const char *tag, const char *message, size_t message_len) { - if (level <= this->level_) { - this->trigger(level, tag, message); - } - }); + explicit LoggerMessageTrigger(Logger *parent, uint8_t level) : level_(level) { parent->add_log_listener(this); } + + void on_log(uint8_t level, const char *tag, const char *message, size_t message_len) override { + (void) message_len; + if (level <= this->level_) { + this->trigger(level, tag, message); + } } protected: diff --git a/esphome/components/mqtt/mqtt_client.cpp b/esphome/components/mqtt/mqtt_client.cpp index a810d98adf..ba701b90a3 100644 --- a/esphome/components/mqtt/mqtt_client.cpp +++ b/esphome/components/mqtt/mqtt_client.cpp @@ -57,15 +57,7 @@ void MQTTClientComponent::setup() { }); #ifdef USE_LOGGER if (this->is_log_message_enabled() && logger::global_logger != nullptr) { - logger::global_logger->add_on_log_callback( - [this](int level, const char *tag, const char *message, size_t message_len) { - if (level <= this->log_level_ && this->is_connected()) { - this->publish({.topic = this->log_message_.topic, - .payload = std::string(message, message_len), - .qos = this->log_message_.qos, - .retain = this->log_message_.retain}); - } - }); + logger::global_logger->add_log_listener(this); } #endif @@ -148,6 +140,18 @@ void MQTTClientComponent::send_device_info_() { // NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks) } +#ifdef USE_LOGGER +void MQTTClientComponent::on_log(uint8_t level, const char *tag, const char *message, size_t message_len) { + (void) tag; + if (level <= this->log_level_ && this->is_connected()) { + this->publish({.topic = this->log_message_.topic, + .payload = std::string(message, message_len), + .qos = this->log_message_.qos, + .retain = this->log_message_.retain}); + } +} +#endif + void MQTTClientComponent::dump_config() { ESP_LOGCONFIG(TAG, "MQTT:\n" diff --git a/esphome/components/mqtt/mqtt_client.h b/esphome/components/mqtt/mqtt_client.h index 79383ee857..8547fe337f 100644 --- a/esphome/components/mqtt/mqtt_client.h +++ b/esphome/components/mqtt/mqtt_client.h @@ -10,6 +10,9 @@ #include "esphome/core/component.h" #include "esphome/core/helpers.h" #include "esphome/core/log.h" +#ifdef USE_LOGGER +#include "esphome/components/logger/logger.h" +#endif #if defined(USE_ESP32) #include "mqtt_backend_esp32.h" #elif defined(USE_ESP8266) @@ -97,7 +100,12 @@ enum MQTTClientState { class MQTTComponent; -class MQTTClientComponent : public Component { +class MQTTClientComponent : public Component +#ifdef USE_LOGGER + , + public logger::LogListener +#endif +{ public: MQTTClientComponent(); @@ -238,6 +246,10 @@ class MQTTClientComponent : public Component { /// MQTT client setup priority float get_setup_priority() const override; +#ifdef USE_LOGGER + void on_log(uint8_t level, const char *tag, const char *message, size_t message_len) override; +#endif + void on_message(const std::string &topic, const std::string &payload); bool can_proceed() override; diff --git a/esphome/components/syslog/esphome_syslog.cpp b/esphome/components/syslog/esphome_syslog.cpp index 71468fa932..f5c20c891e 100644 --- a/esphome/components/syslog/esphome_syslog.cpp +++ b/esphome/components/syslog/esphome_syslog.cpp @@ -19,11 +19,10 @@ constexpr int LOG_LEVEL_TO_SYSLOG_SEVERITY[] = { 7 // VERY_VERBOSE }; -void Syslog::setup() { - logger::global_logger->add_on_log_callback( - [this](int level, const char *tag, const char *message, size_t message_len) { - this->log_(level, tag, message, message_len); - }); +void Syslog::setup() { logger::global_logger->add_log_listener(this); } + +void Syslog::on_log(uint8_t level, const char *tag, const char *message, size_t message_len) { + this->log_(level, tag, message, message_len); } void Syslog::log_(const int level, const char *tag, const char *message, size_t message_len) const { diff --git a/esphome/components/syslog/esphome_syslog.h b/esphome/components/syslog/esphome_syslog.h index e3b2f7dae5..1010993265 100644 --- a/esphome/components/syslog/esphome_syslog.h +++ b/esphome/components/syslog/esphome_syslog.h @@ -2,16 +2,18 @@ #include "esphome/core/component.h" #include "esphome/core/helpers.h" #include "esphome/core/log.h" +#include "esphome/components/logger/logger.h" #include "esphome/components/udp/udp_component.h" #include "esphome/components/time/real_time_clock.h" #ifdef USE_NETWORK namespace esphome { namespace syslog { -class Syslog : public Component, public Parented { +class Syslog : public Component, public Parented, public logger::LogListener { public: Syslog(int level, time::RealTimeClock *time) : log_level_(level), time_(time) {} void setup() override; + void on_log(uint8_t level, const char *tag, const char *message, size_t message_len) override; void set_strip(bool strip) { this->strip_ = strip; } void set_facility(int facility) { this->facility_ = facility; } diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index 6bf6524fbc..f5ca674161 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -301,12 +301,7 @@ void WebServer::setup() { #ifdef USE_LOGGER if (logger::global_logger != nullptr && this->expose_log_) { - logger::global_logger->add_on_log_callback( - // logs are not deferred, the memory overhead would be too large - [this](int level, const char *tag, const char *message, size_t message_len) { - (void) message_len; - this->events_.try_send_nodefer(message, "log", millis()); - }); + logger::global_logger->add_log_listener(this); } #endif @@ -322,6 +317,16 @@ void WebServer::setup() { this->set_interval(10000, [this]() { this->events_.try_send_nodefer("", "ping", millis(), 30000); }); } void WebServer::loop() { this->events_.loop(); } + +#ifdef USE_LOGGER +void WebServer::on_log(uint8_t level, const char *tag, const char *message, size_t message_len) { + (void) level; + (void) tag; + (void) message_len; + this->events_.try_send_nodefer(message, "log", millis()); +} +#endif + void WebServer::dump_config() { ESP_LOGCONFIG(TAG, "Web Server:\n" diff --git a/esphome/components/web_server/web_server.h b/esphome/components/web_server/web_server.h index 7e1af88645..52cf0bedea 100644 --- a/esphome/components/web_server/web_server.h +++ b/esphome/components/web_server/web_server.h @@ -7,6 +7,9 @@ #include "esphome/core/component.h" #include "esphome/core/controller.h" #include "esphome/core/entity_base.h" +#ifdef USE_LOGGER +#include "esphome/components/logger/logger.h" +#endif #include #include @@ -170,7 +173,14 @@ class DeferredUpdateEventSourceList : public std::list Date: Thu, 27 Nov 2025 22:09:27 -0600 Subject: [PATCH 346/415] [light] Replace sparse enum switch with linear search to save 156 bytes RAM (#12140) --- .../components/light/light_json_schema.cpp | 45 +++++++++---------- 1 file changed, 22 insertions(+), 23 deletions(-) diff --git a/esphome/components/light/light_json_schema.cpp b/esphome/components/light/light_json_schema.cpp index 1c9b92f504..41cb855630 100644 --- a/esphome/components/light/light_json_schema.cpp +++ b/esphome/components/light/light_json_schema.cpp @@ -7,30 +7,29 @@ namespace esphome::light { // See https://www.home-assistant.io/integrations/light.mqtt/#json-schema for documentation on the schema -// Lookup table for color mode strings -static constexpr const char *get_color_mode_json_str(ColorMode mode) { - switch (mode) { - case ColorMode::ON_OFF: - return "onoff"; - case ColorMode::BRIGHTNESS: - return "brightness"; - case ColorMode::WHITE: - return "white"; // not supported by HA in MQTT - case ColorMode::COLOR_TEMPERATURE: - return "color_temp"; - case ColorMode::COLD_WARM_WHITE: - return "cwww"; // not supported by HA - case ColorMode::RGB: - return "rgb"; - case ColorMode::RGB_WHITE: - return "rgbw"; - case ColorMode::RGB_COLOR_TEMPERATURE: - return "rgbct"; // not supported by HA - case ColorMode::RGB_COLD_WARM_WHITE: - return "rgbww"; - default: - return nullptr; +// Get JSON string for color mode using linear search (avoids large switch jump table) +static const char *get_color_mode_json_str(ColorMode mode) { + // Parallel arrays: mode values and their corresponding strings + // Uses less RAM than a switch jump table on sparse enum values + static constexpr ColorMode MODES[] = { + ColorMode::ON_OFF, + ColorMode::BRIGHTNESS, + ColorMode::WHITE, + ColorMode::COLOR_TEMPERATURE, + ColorMode::COLD_WARM_WHITE, + ColorMode::RGB, + ColorMode::RGB_WHITE, + ColorMode::RGB_COLOR_TEMPERATURE, + ColorMode::RGB_COLD_WARM_WHITE, + }; + static constexpr const char *STRINGS[] = { + "onoff", "brightness", "white", "color_temp", "cwww", "rgb", "rgbw", "rgbct", "rgbww", + }; + for (size_t i = 0; i < sizeof(MODES) / sizeof(MODES[0]); i++) { + if (MODES[i] == mode) + return STRINGS[i]; } + return nullptr; } void LightJSONSchema::dump_json(LightState &state, JsonObject root) { From e1ec6146c0d19d7d4dc021ed2afdd96d8d492518 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 27 Nov 2025 22:09:41 -0600 Subject: [PATCH 347/415] [wifi] Save 112 bytes BSS on ESP8266 by calling SDK directly for BSSID (#12137) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> --- esphome/components/wifi/wifi_component_esp8266.cpp | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/esphome/components/wifi/wifi_component_esp8266.cpp b/esphome/components/wifi/wifi_component_esp8266.cpp index 540ad3a585..2d1f979c83 100644 --- a/esphome/components/wifi/wifi_component_esp8266.cpp +++ b/esphome/components/wifi/wifi_component_esp8266.cpp @@ -878,10 +878,9 @@ network::IPAddress WiFiComponent::wifi_soft_ap_ip() { bssid_t WiFiComponent::wifi_bssid() { bssid_t bssid{}; - uint8_t *raw_bssid = WiFi.BSSID(); - if (raw_bssid != nullptr) { - for (size_t i = 0; i < bssid.size(); i++) - bssid[i] = raw_bssid[i]; + struct station_config conf {}; + if (wifi_station_get_config(&conf)) { + std::copy_n(conf.bssid, bssid.size(), bssid.begin()); } return bssid; } From c9bb9c4d244f17c03a73d2bfee2afbb95abc5855 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 27 Nov 2025 22:17:56 -0600 Subject: [PATCH 348/415] [wifi] Replace std::function callbacks with listener interfaces --- esphome/components/wifi/__init__.py | 16 ++-- esphome/components/wifi/wifi_component.h | 75 +++++++++++++------ .../wifi/wifi_component_esp8266.cpp | 28 ++++--- .../wifi/wifi_component_esp_idf.cpp | 30 +++++--- .../wifi/wifi_component_libretiny.cpp | 30 +++++--- .../components/wifi/wifi_component_pico_w.cpp | 24 ++++-- esphome/components/wifi_info/text_sensor.py | 6 +- .../wifi_info/wifi_info_text_sensor.cpp | 46 ++++-------- .../wifi_info/wifi_info_text_sensor.h | 32 ++++---- esphome/core/defines.h | 2 +- 10 files changed, 171 insertions(+), 118 deletions(-) diff --git a/esphome/components/wifi/__init__.py b/esphome/components/wifi/__init__.py index 31d9ca0f70..2c10506011 100644 --- a/esphome/components/wifi/__init__.py +++ b/esphome/components/wifi/__init__.py @@ -608,7 +608,7 @@ async def wifi_disable_to_code(config, action_id, template_arg, args): KEEP_SCAN_RESULTS_KEY = "wifi_keep_scan_results" RUNTIME_POWER_SAVE_KEY = "wifi_runtime_power_save" -WIFI_CALLBACKS_KEY = "wifi_callbacks" +WIFI_LISTENERS_KEY = "wifi_listeners" def request_wifi_scan_results(): @@ -634,15 +634,15 @@ def enable_runtime_power_save_control(): CORE.data[RUNTIME_POWER_SAVE_KEY] = True -def request_wifi_callbacks() -> None: - """Request that WiFi callbacks be compiled in. +def request_wifi_listeners() -> None: + """Request that WiFi state listeners be compiled in. Components that need to be notified about WiFi state changes (IP address changes, scan results, connection state) should call this function during their code generation. - This enables the add_on_ip_state_callback(), add_on_wifi_scan_state_callback(), - and add_on_wifi_connect_state_callback() APIs. + This enables the add_ip_state_listener(), add_scan_results_listener(), + and add_connect_state_listener() APIs. """ - CORE.data[WIFI_CALLBACKS_KEY] = True + CORE.data[WIFI_LISTENERS_KEY] = True @coroutine_with_priority(CoroPriority.FINAL) @@ -654,8 +654,8 @@ async def final_step(): ) if CORE.data.get(RUNTIME_POWER_SAVE_KEY, False): cg.add_define("USE_WIFI_RUNTIME_POWER_SAVE") - if CORE.data.get(WIFI_CALLBACKS_KEY, False): - cg.add_define("USE_WIFI_CALLBACKS") + if CORE.data.get(WIFI_LISTENERS_KEY, False): + cg.add_define("USE_WIFI_LISTENERS") @automation.register_action( diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index a9b03a8b8d..a182126b54 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -242,6 +242,39 @@ enum WifiMinAuthMode : uint8_t { struct IDFWiFiEvent; #endif +#ifdef USE_WIFI_LISTENERS +/** Listener interface for WiFi IP state changes. + * + * Components can implement this interface to receive IP address updates + * without the overhead of std::function callbacks. + */ +class WiFiIPStateListener { + public: + virtual void on_ip_state(const network::IPAddresses &ips, const network::IPAddress &dns1, + const network::IPAddress &dns2) = 0; +}; + +/** Listener interface for WiFi scan results. + * + * Components can implement this interface to receive scan results + * without the overhead of std::function callbacks. + */ +class WiFiScanResultsListener { + public: + virtual void on_wifi_scan_results(const wifi_scan_vector_t &results) = 0; +}; + +/** Listener interface for WiFi connection state changes. + * + * Components can implement this interface to receive connection updates + * without the overhead of std::function callbacks. + */ +class WiFiConnectStateListener { + public: + virtual void on_wifi_connect_state(const std::string &ssid, const bssid_t &bssid) = 0; +}; +#endif // USE_WIFI_LISTENERS + /// This component is responsible for managing the ESP WiFi interface. class WiFiComponent : public Component { public: @@ -373,26 +406,22 @@ class WiFiComponent : public Component { int32_t get_wifi_channel(); -#ifdef USE_WIFI_CALLBACKS - /// Add a callback that will be called on configuration changes (IP change, SSID change, etc.) - /// @param callback The callback to be called; template arguments are: - /// - IP addresses - /// - DNS address 1 - /// - DNS address 2 - void add_on_ip_state_callback( - std::function &&callback) { - this->ip_state_callback_.add(std::move(callback)); +#ifdef USE_WIFI_LISTENERS + /** Add a listener for IP state changes. + * Listener receives: IP addresses, DNS address 1, DNS address 2 + */ + void add_ip_state_listener(WiFiIPStateListener *listener) { this->ip_state_listeners_.push_back(listener); } + /// Add a listener for WiFi scan results + void add_scan_results_listener(WiFiScanResultsListener *listener) { + this->scan_results_listeners_.push_back(listener); } - /// - Wi-Fi scan results - void add_on_wifi_scan_state_callback(std::function &)> &&callback) { - this->wifi_scan_state_callback_.add(std::move(callback)); + /** Add a listener for WiFi connection state changes. + * Listener receives: SSID, BSSID + */ + void add_connect_state_listener(WiFiConnectStateListener *listener) { + this->connect_state_listeners_.push_back(listener); } - /// - Wi-Fi SSID - /// - Wi-Fi BSSID - void add_on_wifi_connect_state_callback(std::function &&callback) { - this->wifi_connect_state_callback_.add(std::move(callback)); - } -#endif // USE_WIFI_CALLBACKS +#endif // USE_WIFI_LISTENERS #ifdef USE_WIFI_RUNTIME_POWER_SAVE /** Request high-performance mode (no power saving) for improved WiFi latency. @@ -550,11 +579,11 @@ class WiFiComponent : public Component { WiFiAP ap_; #endif optional output_power_; -#ifdef USE_WIFI_CALLBACKS - CallbackManager ip_state_callback_; - CallbackManager &)> wifi_scan_state_callback_; - CallbackManager wifi_connect_state_callback_; -#endif // USE_WIFI_CALLBACKS +#ifdef USE_WIFI_LISTENERS + std::vector ip_state_listeners_; + std::vector scan_results_listeners_; + std::vector connect_state_listeners_; +#endif // USE_WIFI_LISTENERS ESPPreferenceObject pref_; #ifdef USE_WIFI_FAST_CONNECT ESPPreferenceObject fast_connect_pref_; diff --git a/esphome/components/wifi/wifi_component_esp8266.cpp b/esphome/components/wifi/wifi_component_esp8266.cpp index 540ad3a585..192af497f8 100644 --- a/esphome/components/wifi/wifi_component_esp8266.cpp +++ b/esphome/components/wifi/wifi_component_esp8266.cpp @@ -513,9 +513,10 @@ void WiFiComponent::wifi_event_callback(System_Event_t *event) { ESP_LOGV(TAG, "Connected ssid='%s' bssid=%s channel=%u", buf, format_mac_address_pretty(it.bssid).c_str(), it.channel); s_sta_connected = true; -#ifdef USE_WIFI_CALLBACKS - global_wifi_component->wifi_connect_state_callback_.call(global_wifi_component->wifi_ssid(), - global_wifi_component->wifi_bssid()); +#ifdef USE_WIFI_LISTENERS + for (auto *listener : global_wifi_component->connect_state_listeners_) { + listener->on_wifi_connect_state(global_wifi_component->wifi_ssid(), global_wifi_component->wifi_bssid()); + } #endif break; } @@ -536,8 +537,10 @@ void WiFiComponent::wifi_event_callback(System_Event_t *event) { } s_sta_connected = false; s_sta_connecting = false; -#ifdef USE_WIFI_CALLBACKS - global_wifi_component->wifi_connect_state_callback_.call("", bssid_t({0, 0, 0, 0, 0, 0})); +#ifdef USE_WIFI_LISTENERS + for (auto *listener : global_wifi_component->connect_state_listeners_) { + listener->on_wifi_connect_state("", bssid_t({0, 0, 0, 0, 0, 0})); + } #endif break; } @@ -561,10 +564,11 @@ void WiFiComponent::wifi_event_callback(System_Event_t *event) { ESP_LOGV(TAG, "static_ip=%s gateway=%s netmask=%s", format_ip_addr(it.ip).c_str(), format_ip_addr(it.gw).c_str(), format_ip_addr(it.mask).c_str()); s_sta_got_ip = true; -#ifdef USE_WIFI_CALLBACKS - global_wifi_component->ip_state_callback_.call(global_wifi_component->wifi_sta_ip_addresses(), - global_wifi_component->get_dns_address(0), - global_wifi_component->get_dns_address(1)); +#ifdef USE_WIFI_LISTENERS + for (auto *listener : global_wifi_component->ip_state_listeners_) { + listener->on_ip_state(global_wifi_component->wifi_sta_ip_addresses(), global_wifi_component->get_dns_address(0), + global_wifi_component->get_dns_address(1)); + } #endif break; } @@ -740,8 +744,10 @@ void WiFiComponent::wifi_scan_done_callback_(void *arg, STATUS status) { it->is_hidden != 0); } this->scan_done_ = true; -#ifdef USE_WIFI_CALLBACKS - global_wifi_component->wifi_scan_state_callback_.call(global_wifi_component->scan_result_); +#ifdef USE_WIFI_LISTENERS + for (auto *listener : global_wifi_component->scan_results_listeners_) { + listener->on_wifi_scan_results(global_wifi_component->scan_result_); + } #endif } diff --git a/esphome/components/wifi/wifi_component_esp_idf.cpp b/esphome/components/wifi/wifi_component_esp_idf.cpp index c20c96ced0..3d25d2890f 100644 --- a/esphome/components/wifi/wifi_component_esp_idf.cpp +++ b/esphome/components/wifi/wifi_component_esp_idf.cpp @@ -727,8 +727,10 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) { ESP_LOGV(TAG, "Connected ssid='%s' bssid=" LOG_SECRET("%s") " channel=%u, authmode=%s", buf, format_mac_address_pretty(it.bssid).c_str(), it.channel, get_auth_mode_str(it.authmode)); s_sta_connected = true; -#ifdef USE_WIFI_CALLBACKS - this->wifi_connect_state_callback_.call(this->wifi_ssid(), this->wifi_bssid()); +#ifdef USE_WIFI_LISTENERS + for (auto *listener : this->connect_state_listeners_) { + listener->on_wifi_connect_state(this->wifi_ssid(), this->wifi_bssid()); + } #endif } else if (data->event_base == WIFI_EVENT && data->event_id == WIFI_EVENT_STA_DISCONNECTED) { @@ -753,8 +755,10 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) { s_sta_connected = false; s_sta_connecting = false; error_from_callback_ = true; -#ifdef USE_WIFI_CALLBACKS - this->wifi_connect_state_callback_.call("", bssid_t({0, 0, 0, 0, 0, 0})); +#ifdef USE_WIFI_LISTENERS + for (auto *listener : this->connect_state_listeners_) { + listener->on_wifi_connect_state("", bssid_t({0, 0, 0, 0, 0, 0})); + } #endif } else if (data->event_base == IP_EVENT && data->event_id == IP_EVENT_STA_GOT_IP) { @@ -764,8 +768,10 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) { #endif /* USE_NETWORK_IPV6 */ ESP_LOGV(TAG, "static_ip=" IPSTR " gateway=" IPSTR, IP2STR(&it.ip_info.ip), IP2STR(&it.ip_info.gw)); this->got_ipv4_address_ = true; -#ifdef USE_WIFI_CALLBACKS - this->ip_state_callback_.call(this->wifi_sta_ip_addresses(), this->get_dns_address(0), this->get_dns_address(1)); +#ifdef USE_WIFI_LISTENERS + for (auto *listener : this->ip_state_listeners_) { + listener->on_ip_state(this->wifi_sta_ip_addresses(), this->get_dns_address(0), this->get_dns_address(1)); + } #endif #if USE_NETWORK_IPV6 @@ -773,8 +779,10 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) { const auto &it = data->data.ip_got_ip6; ESP_LOGV(TAG, "IPv6 address=" IPV6STR, IPV62STR(it.ip6_info.ip)); this->num_ipv6_addresses_++; -#ifdef USE_WIFI_CALLBACKS - this->ip_state_callback_.call(this->wifi_sta_ip_addresses(), this->get_dns_address(0), this->get_dns_address(1)); +#ifdef USE_WIFI_LISTENERS + for (auto *listener : this->ip_state_listeners_) { + listener->on_ip_state(this->wifi_sta_ip_addresses(), this->get_dns_address(0), this->get_dns_address(1)); + } #endif #endif /* USE_NETWORK_IPV6 */ @@ -815,8 +823,10 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) { scan_result_.emplace_back(bssid, ssid, record.primary, record.rssi, record.authmode != WIFI_AUTH_OPEN, ssid.empty()); } -#ifdef USE_WIFI_CALLBACKS - this->wifi_scan_state_callback_.call(this->scan_result_); +#ifdef USE_WIFI_LISTENERS + for (auto *listener : this->scan_results_listeners_) { + listener->on_wifi_scan_results(this->scan_result_); + } #endif } else if (data->event_base == WIFI_EVENT && data->event_id == WIFI_EVENT_AP_START) { diff --git a/esphome/components/wifi/wifi_component_libretiny.cpp b/esphome/components/wifi/wifi_component_libretiny.cpp index 04d0d4fa85..f1405d3bef 100644 --- a/esphome/components/wifi/wifi_component_libretiny.cpp +++ b/esphome/components/wifi/wifi_component_libretiny.cpp @@ -287,8 +287,10 @@ void WiFiComponent::wifi_event_callback_(esphome_wifi_event_id_t event, esphome_ buf[it.ssid_len] = '\0'; ESP_LOGV(TAG, "Connected ssid='%s' bssid=" LOG_SECRET("%s") " channel=%u, authmode=%s", buf, format_mac_address_pretty(it.bssid).c_str(), it.channel, get_auth_mode_str(it.authmode)); -#ifdef USE_WIFI_CALLBACKS - this->wifi_connect_state_callback_.call(this->wifi_ssid(), this->wifi_bssid()); +#ifdef USE_WIFI_LISTENERS + for (auto *listener : this->connect_state_listeners_) { + listener->on_wifi_connect_state(this->wifi_ssid(), this->wifi_bssid()); + } #endif break; } @@ -315,8 +317,10 @@ void WiFiComponent::wifi_event_callback_(esphome_wifi_event_id_t event, esphome_ } s_sta_connecting = false; -#ifdef USE_WIFI_CALLBACKS - this->wifi_connect_state_callback_.call("", bssid_t({0, 0, 0, 0, 0, 0})); +#ifdef USE_WIFI_LISTENERS + for (auto *listener : this->connect_state_listeners_) { + listener->on_wifi_connect_state("", bssid_t({0, 0, 0, 0, 0, 0})); + } #endif break; } @@ -339,16 +343,20 @@ void WiFiComponent::wifi_event_callback_(esphome_wifi_event_id_t event, esphome_ ESP_LOGV(TAG, "static_ip=%s gateway=%s", format_ip4_addr(WiFi.localIP()).c_str(), format_ip4_addr(WiFi.gatewayIP()).c_str()); s_sta_connecting = false; -#ifdef USE_WIFI_CALLBACKS - this->ip_state_callback_.call(this->wifi_sta_ip_addresses(), this->get_dns_address(0), this->get_dns_address(1)); +#ifdef USE_WIFI_LISTENERS + for (auto *listener : this->ip_state_listeners_) { + listener->on_ip_state(this->wifi_sta_ip_addresses(), this->get_dns_address(0), this->get_dns_address(1)); + } #endif break; } case ESPHOME_EVENT_ID_WIFI_STA_GOT_IP6: { // auto it = info.got_ip.ip_info; ESP_LOGV(TAG, "Got IPv6"); -#ifdef USE_WIFI_CALLBACKS - this->ip_state_callback_.call(this->wifi_sta_ip_addresses(), this->get_dns_address(0), this->get_dns_address(1)); +#ifdef USE_WIFI_LISTENERS + for (auto *listener : this->ip_state_listeners_) { + listener->on_ip_state(this->wifi_sta_ip_addresses(), this->get_dns_address(0), this->get_dns_address(1)); + } #endif break; } @@ -443,8 +451,10 @@ void WiFiComponent::wifi_scan_done_callback_() { } WiFi.scanDelete(); this->scan_done_ = true; -#ifdef USE_WIFI_CALLBACKS - this->wifi_scan_state_callback_.call(this->scan_result_); +#ifdef USE_WIFI_LISTENERS + for (auto *listener : this->scan_results_listeners_) { + listener->on_wifi_scan_results(this->scan_result_); + } #endif } diff --git a/esphome/components/wifi/wifi_component_pico_w.cpp b/esphome/components/wifi/wifi_component_pico_w.cpp index 326883c0c4..1a8b75213c 100644 --- a/esphome/components/wifi/wifi_component_pico_w.cpp +++ b/esphome/components/wifi/wifi_component_pico_w.cpp @@ -225,8 +225,10 @@ void WiFiComponent::wifi_loop_() { if (this->state_ == WIFI_COMPONENT_STATE_STA_SCANNING && !cyw43_wifi_scan_active(&cyw43_state)) { this->scan_done_ = true; ESP_LOGV(TAG, "Scan done"); -#ifdef USE_WIFI_CALLBACKS - this->wifi_scan_state_callback_.call(this->scan_result_); +#ifdef USE_WIFI_LISTENERS + for (auto *listener : this->scan_results_listeners_) { + listener->on_wifi_scan_results(this->scan_result_); + } #endif } @@ -241,16 +243,20 @@ void WiFiComponent::wifi_loop_() { // Just connected s_sta_was_connected = true; ESP_LOGV(TAG, "Connected"); -#ifdef USE_WIFI_CALLBACKS - this->wifi_connect_state_callback_.call(this->wifi_ssid(), this->wifi_bssid()); +#ifdef USE_WIFI_LISTENERS + for (auto *listener : this->connect_state_listeners_) { + listener->on_wifi_connect_state(this->wifi_ssid(), this->wifi_bssid()); + } #endif } else if (!is_connected && s_sta_was_connected) { // Just disconnected s_sta_was_connected = false; s_sta_had_ip = false; ESP_LOGV(TAG, "Disconnected"); -#ifdef USE_WIFI_CALLBACKS - this->wifi_connect_state_callback_.call("", bssid_t({0, 0, 0, 0, 0, 0})); +#ifdef USE_WIFI_LISTENERS + for (auto *listener : this->connect_state_listeners_) { + listener->on_wifi_connect_state("", bssid_t({0, 0, 0, 0, 0, 0})); + } #endif } @@ -267,8 +273,10 @@ void WiFiComponent::wifi_loop_() { // Just got IP address s_sta_had_ip = true; ESP_LOGV(TAG, "Got IP address"); -#ifdef USE_WIFI_CALLBACKS - this->ip_state_callback_.call(this->wifi_sta_ip_addresses(), this->get_dns_address(0), this->get_dns_address(1)); +#ifdef USE_WIFI_LISTENERS + for (auto *listener : this->ip_state_listeners_) { + listener->on_ip_state(this->wifi_sta_ip_addresses(), this->get_dns_address(0), this->get_dns_address(1)); + } #endif } } diff --git a/esphome/components/wifi_info/text_sensor.py b/esphome/components/wifi_info/text_sensor.py index 0feee3d4a9..bc0c038f80 100644 --- a/esphome/components/wifi_info/text_sensor.py +++ b/esphome/components/wifi_info/text_sensor.py @@ -61,7 +61,7 @@ CONFIG_SCHEMA = cv.Schema( } ) -# Keys that require WiFi callbacks +# Keys that require WiFi listeners _NETWORK_INFO_KEYS = { CONF_SSID, CONF_BSSID, @@ -79,9 +79,9 @@ async def setup_conf(config, key): async def to_code(config): - # Request WiFi callbacks for any sensor that needs them + # Request WiFi listeners for any sensor that needs them if _NETWORK_INFO_KEYS.intersection(config): - wifi.request_wifi_callbacks() + wifi.request_wifi_listeners() await setup_conf(config, CONF_SSID) await setup_conf(config, CONF_BSSID) diff --git a/esphome/components/wifi_info/wifi_info_text_sensor.cpp b/esphome/components/wifi_info/wifi_info_text_sensor.cpp index abd590b168..92d3ea29f5 100644 --- a/esphome/components/wifi_info/wifi_info_text_sensor.cpp +++ b/esphome/components/wifi_info/wifi_info_text_sensor.cpp @@ -12,16 +12,12 @@ static constexpr size_t MAX_STATE_LENGTH = 255; * IPAddressWiFiInfo *******************/ -void IPAddressWiFiInfo::setup() { - wifi::global_wifi_component->add_on_ip_state_callback( - [this](const network::IPAddresses &ips, const network::IPAddress &dns1_ip, const network::IPAddress &dns2_ip) { - this->state_callback_(ips); - }); -} +void IPAddressWiFiInfo::setup() { wifi::global_wifi_component->add_ip_state_listener(this); } void IPAddressWiFiInfo::dump_config() { LOG_TEXT_SENSOR("", "IP Address", this); } -void IPAddressWiFiInfo::state_callback_(const network::IPAddresses &ips) { +void IPAddressWiFiInfo::on_ip_state(const network::IPAddresses &ips, const network::IPAddress &dns1, + const network::IPAddress &dns2) { this->publish_state(ips[0].str()); uint8_t sensor = 0; for (const auto &ip : ips) { @@ -38,17 +34,13 @@ void IPAddressWiFiInfo::state_callback_(const network::IPAddresses &ips) { * DNSAddressWifiInfo ********************/ -void DNSAddressWifiInfo::setup() { - wifi::global_wifi_component->add_on_ip_state_callback( - [this](const network::IPAddresses &ips, const network::IPAddress &dns1_ip, const network::IPAddress &dns2_ip) { - this->state_callback_(dns1_ip, dns2_ip); - }); -} +void DNSAddressWifiInfo::setup() { wifi::global_wifi_component->add_ip_state_listener(this); } void DNSAddressWifiInfo::dump_config() { LOG_TEXT_SENSOR("", "DNS Address", this); } -void DNSAddressWifiInfo::state_callback_(const network::IPAddress &dns1_ip, const network::IPAddress &dns2_ip) { - std::string dns_results = dns1_ip.str() + " " + dns2_ip.str(); +void DNSAddressWifiInfo::on_ip_state(const network::IPAddresses &ips, const network::IPAddress &dns1, + const network::IPAddress &dns2) { + std::string dns_results = dns1.str() + " " + dns2.str(); this->publish_state(dns_results); } @@ -56,14 +48,11 @@ void DNSAddressWifiInfo::state_callback_(const network::IPAddress &dns1_ip, cons * ScanResultsWiFiInfo *********************/ -void ScanResultsWiFiInfo::setup() { - wifi::global_wifi_component->add_on_wifi_scan_state_callback( - [this](const wifi::wifi_scan_vector_t &results) { this->state_callback_(results); }); -} +void ScanResultsWiFiInfo::setup() { wifi::global_wifi_component->add_scan_results_listener(this); } void ScanResultsWiFiInfo::dump_config() { LOG_TEXT_SENSOR("", "Scan Results", this); } -void ScanResultsWiFiInfo::state_callback_(const wifi::wifi_scan_vector_t &results) { +void ScanResultsWiFiInfo::on_wifi_scan_results(const wifi::wifi_scan_vector_t &results) { std::string scan_results; for (const auto &scan : results) { if (scan.get_is_hidden()) @@ -85,33 +74,30 @@ void ScanResultsWiFiInfo::state_callback_(const wifi::wifi_scan_vector_tadd_on_wifi_connect_state_callback( - [this](const std::string &ssid, const wifi::bssid_t &bssid) { this->state_callback_(ssid); }); -} +void SSIDWiFiInfo::setup() { wifi::global_wifi_component->add_connect_state_listener(this); } void SSIDWiFiInfo::dump_config() { LOG_TEXT_SENSOR("", "SSID", this); } -void SSIDWiFiInfo::state_callback_(const std::string &ssid) { this->publish_state(ssid); } +void SSIDWiFiInfo::on_wifi_connect_state(const std::string &ssid, const wifi::bssid_t &bssid) { + this->publish_state(ssid); +} /**************** * BSSIDWiFiInfo ***************/ -void BSSIDWiFiInfo::setup() { - wifi::global_wifi_component->add_on_wifi_connect_state_callback( - [this](const std::string &ssid, const wifi::bssid_t &bssid) { this->state_callback_(bssid); }); -} +void BSSIDWiFiInfo::setup() { wifi::global_wifi_component->add_connect_state_listener(this); } void BSSIDWiFiInfo::dump_config() { LOG_TEXT_SENSOR("", "BSSID", this); } -void BSSIDWiFiInfo::state_callback_(const wifi::bssid_t &bssid) { +void BSSIDWiFiInfo::on_wifi_connect_state(const std::string &ssid, const wifi::bssid_t &bssid) { char buf[18] = "unknown"; if (mac_address_is_valid(bssid.data())) { format_mac_addr_upper(bssid.data(), buf); } this->publish_state(buf); } + /********************* * MacAddressWifiInfo ********************/ diff --git a/esphome/components/wifi_info/wifi_info_text_sensor.h b/esphome/components/wifi_info/wifi_info_text_sensor.h index 12666b4059..ac0489a4b8 100644 --- a/esphome/components/wifi_info/wifi_info_text_sensor.h +++ b/esphome/components/wifi_info/wifi_info_text_sensor.h @@ -9,52 +9,56 @@ namespace esphome::wifi_info { -class IPAddressWiFiInfo : public Component, public text_sensor::TextSensor { +class IPAddressWiFiInfo : public Component, public text_sensor::TextSensor, public wifi::WiFiIPStateListener { public: void setup() override; void dump_config() override; void add_ip_sensors(uint8_t index, text_sensor::TextSensor *s) { this->ip_sensors_[index] = s; } + // WiFiIPStateListener interface + void on_ip_state(const network::IPAddresses &ips, const network::IPAddress &dns1, + const network::IPAddress &dns2) override; + protected: - void state_callback_(const network::IPAddresses &ips); std::array ip_sensors_; }; -class DNSAddressWifiInfo : public Component, public text_sensor::TextSensor { +class DNSAddressWifiInfo : public Component, public text_sensor::TextSensor, public wifi::WiFiIPStateListener { public: void setup() override; void dump_config() override; - protected: - void state_callback_(const network::IPAddress &dns1_ip, const network::IPAddress &dns2_ip); + // WiFiIPStateListener interface + void on_ip_state(const network::IPAddresses &ips, const network::IPAddress &dns1, + const network::IPAddress &dns2) override; }; -class ScanResultsWiFiInfo : public Component, public text_sensor::TextSensor { +class ScanResultsWiFiInfo : public Component, public text_sensor::TextSensor, public wifi::WiFiScanResultsListener { public: void setup() override; float get_setup_priority() const override { return setup_priority::AFTER_WIFI; } void dump_config() override; - protected: - void state_callback_(const wifi::wifi_scan_vector_t &results); + // WiFiScanResultsListener interface + void on_wifi_scan_results(const wifi::wifi_scan_vector_t &results) override; }; -class SSIDWiFiInfo : public Component, public text_sensor::TextSensor { +class SSIDWiFiInfo : public Component, public text_sensor::TextSensor, public wifi::WiFiConnectStateListener { public: void setup() override; void dump_config() override; - protected: - void state_callback_(const std::string &ssid); + // WiFiConnectStateListener interface + void on_wifi_connect_state(const std::string &ssid, const wifi::bssid_t &bssid) override; }; -class BSSIDWiFiInfo : public Component, public text_sensor::TextSensor { +class BSSIDWiFiInfo : public Component, public text_sensor::TextSensor, public wifi::WiFiConnectStateListener { public: void setup() override; void dump_config() override; - protected: - void state_callback_(const wifi::bssid_t &bssid); + // WiFiConnectStateListener interface + void on_wifi_connect_state(const std::string &ssid, const wifi::bssid_t &bssid) override; }; class MacAddressWifiInfo : public Component, public text_sensor::TextSensor { diff --git a/esphome/core/defines.h b/esphome/core/defines.h index 1373ea6366..f4026aad96 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -210,7 +210,7 @@ #define USE_WEBSERVER_SORTING #define USE_WIFI_11KV_SUPPORT #define USE_WIFI_FAST_CONNECT -#define USE_WIFI_CALLBACKS +#define USE_WIFI_LISTENERS #define USE_WIFI_RUNTIME_POWER_SAVE #define USB_HOST_MAX_REQUESTS 16 From 3752d5d2abe5ef1beeefd9c52b839a4672d662f7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 27 Nov 2025 22:28:43 -0600 Subject: [PATCH 349/415] tweaks --- esphome/components/wifi/wifi_component.h | 2 -- .../components/wifi_info/wifi_info_text_sensor.h | 14 ++++++++------ 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index a182126b54..97cc3961fe 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -242,7 +242,6 @@ enum WifiMinAuthMode : uint8_t { struct IDFWiFiEvent; #endif -#ifdef USE_WIFI_LISTENERS /** Listener interface for WiFi IP state changes. * * Components can implement this interface to receive IP address updates @@ -273,7 +272,6 @@ class WiFiConnectStateListener { public: virtual void on_wifi_connect_state(const std::string &ssid, const bssid_t &bssid) = 0; }; -#endif // USE_WIFI_LISTENERS /// This component is responsible for managing the ESP WiFi interface. class WiFiComponent : public Component { diff --git a/esphome/components/wifi_info/wifi_info_text_sensor.h b/esphome/components/wifi_info/wifi_info_text_sensor.h index ac0489a4b8..74d951f922 100644 --- a/esphome/components/wifi_info/wifi_info_text_sensor.h +++ b/esphome/components/wifi_info/wifi_info_text_sensor.h @@ -9,7 +9,7 @@ namespace esphome::wifi_info { -class IPAddressWiFiInfo : public Component, public text_sensor::TextSensor, public wifi::WiFiIPStateListener { +class IPAddressWiFiInfo final : public Component, public text_sensor::TextSensor, public wifi::WiFiIPStateListener { public: void setup() override; void dump_config() override; @@ -23,7 +23,7 @@ class IPAddressWiFiInfo : public Component, public text_sensor::TextSensor, publ std::array ip_sensors_; }; -class DNSAddressWifiInfo : public Component, public text_sensor::TextSensor, public wifi::WiFiIPStateListener { +class DNSAddressWifiInfo final : public Component, public text_sensor::TextSensor, public wifi::WiFiIPStateListener { public: void setup() override; void dump_config() override; @@ -33,7 +33,9 @@ class DNSAddressWifiInfo : public Component, public text_sensor::TextSensor, pub const network::IPAddress &dns2) override; }; -class ScanResultsWiFiInfo : public Component, public text_sensor::TextSensor, public wifi::WiFiScanResultsListener { +class ScanResultsWiFiInfo final : public Component, + public text_sensor::TextSensor, + public wifi::WiFiScanResultsListener { public: void setup() override; float get_setup_priority() const override { return setup_priority::AFTER_WIFI; } @@ -43,7 +45,7 @@ class ScanResultsWiFiInfo : public Component, public text_sensor::TextSensor, pu void on_wifi_scan_results(const wifi::wifi_scan_vector_t &results) override; }; -class SSIDWiFiInfo : public Component, public text_sensor::TextSensor, public wifi::WiFiConnectStateListener { +class SSIDWiFiInfo final : public Component, public text_sensor::TextSensor, public wifi::WiFiConnectStateListener { public: void setup() override; void dump_config() override; @@ -52,7 +54,7 @@ class SSIDWiFiInfo : public Component, public text_sensor::TextSensor, public wi void on_wifi_connect_state(const std::string &ssid, const wifi::bssid_t &bssid) override; }; -class BSSIDWiFiInfo : public Component, public text_sensor::TextSensor, public wifi::WiFiConnectStateListener { +class BSSIDWiFiInfo final : public Component, public text_sensor::TextSensor, public wifi::WiFiConnectStateListener { public: void setup() override; void dump_config() override; @@ -61,7 +63,7 @@ class BSSIDWiFiInfo : public Component, public text_sensor::TextSensor, public w void on_wifi_connect_state(const std::string &ssid, const wifi::bssid_t &bssid) override; }; -class MacAddressWifiInfo : public Component, public text_sensor::TextSensor { +class MacAddressWifiInfo final : public Component, public text_sensor::TextSensor { public: void setup() override { char mac_s[18]; From e3ea585d543887d99e7214d7968fabfa2b199e1a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 27 Nov 2025 22:42:21 -0600 Subject: [PATCH 350/415] [esp32_ble_tracker] Replace scanner state callback with listener interface --- .../bluetooth_proxy/bluetooth_proxy.cpp | 12 +++++++----- .../bluetooth_proxy/bluetooth_proxy.h | 7 ++++++- .../esp32_ble_tracker/esp32_ble_tracker.cpp | 4 +++- .../esp32_ble_tracker/esp32_ble_tracker.h | 19 +++++++++++++++---- 4 files changed, 31 insertions(+), 11 deletions(-) diff --git a/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp b/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp index 71f8da75a7..d45377b3f6 100644 --- a/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp +++ b/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp @@ -27,11 +27,13 @@ void BluetoothProxy::setup() { // Capture the configured scan mode from YAML before any API changes this->configured_scan_active_ = this->parent_->get_scan_active(); - this->parent_->add_scanner_state_callback([this](esp32_ble_tracker::ScannerState state) { - if (this->api_connection_ != nullptr) { - this->send_bluetooth_scanner_state_(state); - } - }); + this->parent_->add_scanner_state_listener(this); +} + +void BluetoothProxy::on_scanner_state(esp32_ble_tracker::ScannerState state) { + if (this->api_connection_ != nullptr) { + this->send_bluetooth_scanner_state_(state); + } } void BluetoothProxy::send_bluetooth_scanner_state_(esp32_ble_tracker::ScannerState state) { diff --git a/esphome/components/bluetooth_proxy/bluetooth_proxy.h b/esphome/components/bluetooth_proxy/bluetooth_proxy.h index 4363c508ec..ab9aee2d81 100644 --- a/esphome/components/bluetooth_proxy/bluetooth_proxy.h +++ b/esphome/components/bluetooth_proxy/bluetooth_proxy.h @@ -52,7 +52,9 @@ enum BluetoothProxySubscriptionFlag : uint32_t { SUBSCRIPTION_RAW_ADVERTISEMENTS = 1 << 0, }; -class BluetoothProxy final : public esp32_ble_tracker::ESPBTDeviceListener, public Component { +class BluetoothProxy final : public esp32_ble_tracker::ESPBTDeviceListener, + public esp32_ble_tracker::BLEScannerStateListener, + public Component { friend class BluetoothConnection; // Allow connection to update connections_free_response_ public: BluetoothProxy(); @@ -108,6 +110,9 @@ class BluetoothProxy final : public esp32_ble_tracker::ESPBTDeviceListener, publ void set_active(bool active) { this->active_ = active; } bool has_active() { return this->active_; } + /// BLEScannerStateListener interface + void on_scanner_state(esp32_ble_tracker::ScannerState state) override; + uint32_t get_legacy_version() const { if (this->active_) { return LEGACY_ACTIVE_CONNECTIONS_VERSION; diff --git a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp index 8577f12a92..d3c5edfb94 100644 --- a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp +++ b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp @@ -373,7 +373,9 @@ void ESP32BLETracker::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_i void ESP32BLETracker::set_scanner_state_(ScannerState state) { this->scanner_state_ = state; - this->scanner_state_callbacks_.call(state); + for (auto *listener : this->scanner_state_listeners_) { + listener->on_scanner_state(state); + } } #ifdef USE_ESP32_BLE_DEVICE diff --git a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h index f80f3e2670..92d13a62ad 100644 --- a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h +++ b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h @@ -180,6 +180,16 @@ enum class ScannerState { STOPPING, }; +/** Listener interface for BLE scanner state changes. + * + * Components can implement this interface to receive scanner state updates + * without the overhead of std::function callbacks. + */ +class BLEScannerStateListener { + public: + virtual void on_scanner_state(ScannerState state) = 0; +}; + // Helper function to convert ClientState to string const char *client_state_to_string(ClientState state); @@ -264,8 +274,9 @@ class ESP32BLETracker : public Component, void gap_scan_event_handler(const BLEScanResult &scan_result) override; void ble_before_disabled_event_handler() override; - void add_scanner_state_callback(std::function &&callback) { - this->scanner_state_callbacks_.add(std::move(callback)); + /// Add a listener for scanner state changes + void add_scanner_state_listener(BLEScannerStateListener *listener) { + this->scanner_state_listeners_.push_back(listener); } ScannerState get_scanner_state() const { return this->scanner_state_; } @@ -322,14 +333,14 @@ class ESP32BLETracker : public Component, return counts; } - // Group 1: Large objects (12+ bytes) - vectors and callback manager + // Group 1: Large objects (12+ bytes) - vectors #ifdef ESPHOME_ESP32_BLE_TRACKER_LISTENER_COUNT StaticVector listeners_; #endif #ifdef ESPHOME_ESP32_BLE_TRACKER_CLIENT_COUNT StaticVector clients_; #endif - CallbackManager scanner_state_callbacks_; + std::vector scanner_state_listeners_; #ifdef USE_ESP32_BLE_DEVICE /// Vector of addresses that have already been printed in print_bt_device_info std::vector already_discovered_; From 3c854a02d7a6cbad0efc2aa55e0f5ca9dc90f70a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 28 Nov 2025 09:39:32 -0600 Subject: [PATCH 351/415] add ram --- esphome/__main__.py | 19 +- esphome/analyze_memory/__init__.py | 173 +-------- esphome/analyze_memory/demangle.py | 172 +++++++++ esphome/analyze_memory/ram_strings.py | 491 ++++++++++++++++++++++++++ esphome/analyze_memory/toolchain.py | 54 +++ 5 files changed, 737 insertions(+), 172 deletions(-) create mode 100644 esphome/analyze_memory/demangle.py create mode 100644 esphome/analyze_memory/ram_strings.py create mode 100644 esphome/analyze_memory/toolchain.py diff --git a/esphome/__main__.py b/esphome/__main__.py index f8fb678cb2..ff1769c8f2 100644 --- a/esphome/__main__.py +++ b/esphome/__main__.py @@ -944,6 +944,7 @@ def command_analyze_memory(args: ArgsProtocol, config: ConfigType) -> int: """ from esphome import platformio_api from esphome.analyze_memory.cli import MemoryAnalyzerCLI + from esphome.analyze_memory.ram_strings import RamStringsAnalyzer # Always compile to ensure fresh data (fast if no changes - just relinks) exit_code = write_cpp(config) @@ -966,7 +967,7 @@ def command_analyze_memory(args: ArgsProtocol, config: ConfigType) -> int: external_components = detect_external_components(config) _LOGGER.debug("Detected external components: %s", external_components) - # Perform memory analysis + # Perform component memory analysis _LOGGER.info("Analyzing memory usage...") analyzer = MemoryAnalyzerCLI( str(firmware_elf), @@ -976,11 +977,25 @@ def command_analyze_memory(args: ArgsProtocol, config: ConfigType) -> int: ) analyzer.analyze() - # Generate and display report + # Generate and display component report report = analyzer.generate_report() print() print(report) + # Perform RAM strings analysis + _LOGGER.info("Analyzing RAM strings...") + ram_analyzer = RamStringsAnalyzer( + str(firmware_elf), + objdump_path=idedata.objdump_path, + platform=CORE.target_platform, + ) + ram_analyzer.analyze() + + # Generate and display RAM strings report + ram_report = ram_analyzer.generate_report() + print() + print(ram_report) + return 0 diff --git a/esphome/analyze_memory/__init__.py b/esphome/analyze_memory/__init__.py index 71e86e3788..9632a68913 100644 --- a/esphome/analyze_memory/__init__.py +++ b/esphome/analyze_memory/__init__.py @@ -15,6 +15,7 @@ from .const import ( SECTION_TO_ATTR, SYMBOL_PATTERNS, ) +from .demangle import batch_demangle from .helpers import ( get_component_class_patterns, get_esphome_components, @@ -27,15 +28,6 @@ if TYPE_CHECKING: _LOGGER = logging.getLogger(__name__) -# GCC global constructor/destructor prefix annotations -_GCC_PREFIX_ANNOTATIONS = { - "_GLOBAL__sub_I_": "global constructor for", - "_GLOBAL__sub_D_": "global destructor for", -} - -# GCC optimization suffix pattern (e.g., $isra$0, $part$1, $constprop$2) -_GCC_OPTIMIZATION_SUFFIX_PATTERN = re.compile(r"(\$(?:isra|part|constprop)\$\d+)") - # C++ runtime patterns for categorization _CPP_RUNTIME_PATTERNS = frozenset(["vtable", "typeinfo", "thunk"]) @@ -312,168 +304,9 @@ class MemoryAnalyzer: if not symbols: return - # Try to find the appropriate c++filt for the platform - cppfilt_cmd = "c++filt" - _LOGGER.info("Demangling %d symbols", len(symbols)) - _LOGGER.debug("objdump_path = %s", self.objdump_path) - - # Check if we have a toolchain-specific c++filt - if self.objdump_path and self.objdump_path != "objdump": - # Replace objdump with c++filt in the path - potential_cppfilt = self.objdump_path.replace("objdump", "c++filt") - _LOGGER.info("Checking for toolchain c++filt at: %s", potential_cppfilt) - if Path(potential_cppfilt).exists(): - cppfilt_cmd = potential_cppfilt - _LOGGER.info("✓ Using toolchain c++filt: %s", cppfilt_cmd) - else: - _LOGGER.info( - "✗ Toolchain c++filt not found at %s, using system c++filt", - potential_cppfilt, - ) - else: - _LOGGER.info("✗ Using system c++filt (objdump_path=%s)", self.objdump_path) - - # Strip GCC optimization suffixes and prefixes before demangling - # Suffixes like $isra$0, $part$0, $constprop$0 confuse c++filt - # Prefixes like _GLOBAL__sub_I_ need to be removed and tracked - symbols_stripped: list[str] = [] - symbols_prefixes: list[str] = [] # Track removed prefixes - for symbol in symbols: - # Remove GCC optimization markers - stripped = _GCC_OPTIMIZATION_SUFFIX_PATTERN.sub("", symbol) - - # Handle GCC global constructor/initializer prefixes - # _GLOBAL__sub_I_ -> extract for demangling - prefix = "" - for gcc_prefix in _GCC_PREFIX_ANNOTATIONS: - if stripped.startswith(gcc_prefix): - prefix = gcc_prefix - stripped = stripped[len(prefix) :] - break - - symbols_stripped.append(stripped) - symbols_prefixes.append(prefix) - - try: - # Send all symbols to c++filt at once - result = subprocess.run( - [cppfilt_cmd], - input="\n".join(symbols_stripped), - capture_output=True, - text=True, - check=False, - ) - except (subprocess.SubprocessError, OSError, UnicodeDecodeError) as e: - # On error, cache originals - _LOGGER.warning("Failed to batch demangle symbols: %s", e) - for symbol in symbols: - self._demangle_cache[symbol] = symbol - return - - if result.returncode != 0: - _LOGGER.warning( - "c++filt exited with code %d: %s", - result.returncode, - result.stderr[:200] if result.stderr else "(no error output)", - ) - # Cache originals on failure - for symbol in symbols: - self._demangle_cache[symbol] = symbol - return - - # Process demangled output - self._process_demangled_output( - symbols, symbols_stripped, symbols_prefixes, result.stdout, cppfilt_cmd - ) - - def _process_demangled_output( - self, - symbols: list[str], - symbols_stripped: list[str], - symbols_prefixes: list[str], - demangled_output: str, - cppfilt_cmd: str, - ) -> None: - """Process demangled symbol output and populate cache. - - Args: - symbols: Original symbol names - symbols_stripped: Stripped symbol names sent to c++filt - symbols_prefixes: Removed prefixes to restore - demangled_output: Output from c++filt - cppfilt_cmd: Path to c++filt command (for logging) - """ - demangled_lines = demangled_output.strip().split("\n") - failed_count = 0 - - for original, stripped, prefix, demangled in zip( - symbols, symbols_stripped, symbols_prefixes, demangled_lines - ): - # Add back any prefix that was removed - demangled = self._restore_symbol_prefix(prefix, stripped, demangled) - - # If we stripped a suffix, add it back to the demangled name for clarity - if original != stripped and not prefix: - demangled = self._restore_symbol_suffix(original, demangled) - - self._demangle_cache[original] = demangled - - # Log symbols that failed to demangle (stayed the same as stripped version) - if stripped == demangled and stripped.startswith("_Z"): - failed_count += 1 - if failed_count <= 5: # Only log first 5 failures - _LOGGER.warning("Failed to demangle: %s", original) - - if failed_count == 0: - _LOGGER.info("Successfully demangled all %d symbols", len(symbols)) - return - - _LOGGER.warning( - "Failed to demangle %d/%d symbols using %s", - failed_count, - len(symbols), - cppfilt_cmd, - ) - - @staticmethod - def _restore_symbol_prefix(prefix: str, stripped: str, demangled: str) -> str: - """Restore prefix that was removed before demangling. - - Args: - prefix: Prefix that was removed (e.g., "_GLOBAL__sub_I_") - stripped: Stripped symbol name - demangled: Demangled symbol name - - Returns: - Demangled name with prefix restored/annotated - """ - if not prefix: - return demangled - - # Successfully demangled - add descriptive prefix - if demangled != stripped and ( - annotation := _GCC_PREFIX_ANNOTATIONS.get(prefix) - ): - return f"[{annotation}: {demangled}]" - - # Failed to demangle - restore original prefix - return prefix + demangled - - @staticmethod - def _restore_symbol_suffix(original: str, demangled: str) -> str: - """Restore GCC optimization suffix that was removed before demangling. - - Args: - original: Original symbol name with suffix - demangled: Demangled symbol name without suffix - - Returns: - Demangled name with suffix annotation - """ - if suffix_match := _GCC_OPTIMIZATION_SUFFIX_PATTERN.search(original): - return f"{demangled} [{suffix_match.group(1)}]" - return demangled + self._demangle_cache = batch_demangle(symbols, objdump_path=self.objdump_path) + _LOGGER.info("Successfully demangled %d symbols", len(self._demangle_cache)) def _demangle_symbol(self, symbol: str) -> str: """Get demangled C++ symbol name from cache.""" diff --git a/esphome/analyze_memory/demangle.py b/esphome/analyze_memory/demangle.py new file mode 100644 index 0000000000..7ff06ce870 --- /dev/null +++ b/esphome/analyze_memory/demangle.py @@ -0,0 +1,172 @@ +"""Symbol demangling utilities for memory analysis. + +This module provides functions for demangling C++ symbol names using c++filt. +""" + +from __future__ import annotations + +import logging +import re +import subprocess + +from .toolchain import find_tool + +_LOGGER = logging.getLogger(__name__) + +# GCC global constructor/destructor prefix annotations +GCC_PREFIX_ANNOTATIONS = { + "_GLOBAL__sub_I_": "global constructor for", + "_GLOBAL__sub_D_": "global destructor for", +} + +# GCC optimization suffix pattern (e.g., $isra$0, $part$1, $constprop$2) +GCC_OPTIMIZATION_SUFFIX_PATTERN = re.compile(r"(\$(?:isra|part|constprop)\$\d+)") + + +def _strip_gcc_annotations(symbol: str) -> tuple[str, str]: + """Strip GCC optimization suffixes and prefixes from a symbol. + + Args: + symbol: The mangled symbol name + + Returns: + Tuple of (stripped_symbol, removed_prefix) + """ + # Remove GCC optimization markers + stripped = GCC_OPTIMIZATION_SUFFIX_PATTERN.sub("", symbol) + + # Handle GCC global constructor/initializer prefixes + prefix = "" + for gcc_prefix in GCC_PREFIX_ANNOTATIONS: + if stripped.startswith(gcc_prefix): + prefix = gcc_prefix + stripped = stripped[len(prefix) :] + break + + return stripped, prefix + + +def _restore_symbol_prefix(prefix: str, stripped: str, demangled: str) -> str: + """Restore prefix that was removed before demangling. + + Args: + prefix: Prefix that was removed (e.g., "_GLOBAL__sub_I_") + stripped: Stripped symbol name + demangled: Demangled symbol name + + Returns: + Demangled name with prefix restored/annotated + """ + if not prefix: + return demangled + + # Successfully demangled - add descriptive prefix + if demangled != stripped and (annotation := GCC_PREFIX_ANNOTATIONS.get(prefix)): + return f"[{annotation}: {demangled}]" + + # Failed to demangle - restore original prefix + return prefix + demangled + + +def _restore_symbol_suffix(original: str, demangled: str) -> str: + """Restore GCC optimization suffix that was removed before demangling. + + Args: + original: Original symbol name with suffix + demangled: Demangled symbol name without suffix + + Returns: + Demangled name with suffix annotation + """ + if suffix_match := GCC_OPTIMIZATION_SUFFIX_PATTERN.search(original): + return f"{demangled} [{suffix_match.group(1)}]" + return demangled + + +def batch_demangle( + symbols: list[str], + cppfilt_path: str | None = None, + objdump_path: str | None = None, +) -> dict[str, str]: + """Batch demangle C++ symbol names. + + Args: + symbols: List of symbol names to demangle + cppfilt_path: Path to c++filt binary (auto-detected if not provided) + objdump_path: Path to objdump binary to derive c++filt path from + + Returns: + Dictionary mapping original symbol names to demangled names + """ + cache: dict[str, str] = {} + + if not symbols: + return cache + + # Find c++filt tool + cppfilt_cmd = cppfilt_path or find_tool("c++filt", objdump_path) + if not cppfilt_cmd: + _LOGGER.warning("Could not find c++filt, symbols will not be demangled") + return {s: s for s in symbols} + + _LOGGER.debug("Demangling %d symbols using %s", len(symbols), cppfilt_cmd) + + # Strip GCC optimization suffixes and prefixes before demangling + symbols_stripped: list[str] = [] + symbols_prefixes: list[str] = [] + for symbol in symbols: + stripped, prefix = _strip_gcc_annotations(symbol) + symbols_stripped.append(stripped) + symbols_prefixes.append(prefix) + + try: + result = subprocess.run( + [cppfilt_cmd], + input="\n".join(symbols_stripped), + capture_output=True, + text=True, + check=False, + ) + except (subprocess.SubprocessError, OSError, UnicodeDecodeError) as e: + _LOGGER.warning("Failed to batch demangle symbols: %s", e) + return {s: s for s in symbols} + + if result.returncode != 0: + _LOGGER.warning( + "c++filt exited with code %d: %s", + result.returncode, + result.stderr[:200] if result.stderr else "(no error output)", + ) + return {s: s for s in symbols} + + # Process demangled output + demangled_lines = result.stdout.strip().split("\n") + failed_count = 0 + + for original, stripped, prefix, demangled in zip( + symbols, symbols_stripped, symbols_prefixes, demangled_lines + ): + # Add back any prefix that was removed + demangled = _restore_symbol_prefix(prefix, stripped, demangled) + + # If we stripped a suffix, add it back to the demangled name for clarity + if original != stripped and not prefix: + demangled = _restore_symbol_suffix(original, demangled) + + cache[original] = demangled + + # Count symbols that failed to demangle + if stripped == demangled and stripped.startswith("_Z"): + failed_count += 1 + if failed_count <= 5: + _LOGGER.debug("Failed to demangle: %s", original) + + if failed_count > 0: + _LOGGER.debug( + "Failed to demangle %d/%d symbols using %s", + failed_count, + len(symbols), + cppfilt_cmd, + ) + + return cache diff --git a/esphome/analyze_memory/ram_strings.py b/esphome/analyze_memory/ram_strings.py new file mode 100644 index 0000000000..9260e9b528 --- /dev/null +++ b/esphome/analyze_memory/ram_strings.py @@ -0,0 +1,491 @@ +"""Analyzer for RAM-stored strings in ESP8266/ESP32 firmware ELF files. + +This module identifies strings that are stored in RAM sections (.data, .bss, .rodata) +rather than in flash sections (.irom0.text, .irom.text), which is important for +memory-constrained platforms like ESP8266. +""" + +from __future__ import annotations + +from collections import defaultdict +from dataclasses import dataclass +import logging +from pathlib import Path +import re +import subprocess + +from .demangle import batch_demangle +from .toolchain import find_tool + +_LOGGER = logging.getLogger(__name__) + +# ESP8266: .rodata is in RAM (DRAM), not flash +# ESP32: .rodata is in flash, mapped to data bus +ESP8266_RAM_SECTIONS = frozenset([".data", ".rodata", ".bss"]) +ESP8266_FLASH_SECTIONS = frozenset([".irom0.text", ".irom.text", ".text"]) + +# ESP32: .rodata is memory-mapped from flash +ESP32_RAM_SECTIONS = frozenset([".data", ".bss", ".dram0.data", ".dram0.bss"]) +ESP32_FLASH_SECTIONS = frozenset([".text", ".rodata", ".flash.text", ".flash.rodata"]) + + +@dataclass +class SectionInfo: + """Information about an ELF section.""" + + name: str + address: int + size: int + + +@dataclass +class RamString: + """A string found in RAM.""" + + section: str + address: int + content: str + + @property + def size(self) -> int: + """Size in bytes including null terminator.""" + return len(self.content) + 1 + + +@dataclass +class RamSymbol: + """A symbol found in RAM.""" + + name: str + sym_type: str + address: int + size: int + section: str + demangled: str = "" # Demangled name, set after batch demangling + + +class RamStringsAnalyzer: + """Analyzes ELF files to find strings stored in RAM.""" + + def __init__( + self, + elf_path: str, + objdump_path: str | None = None, + min_length: int = 8, + platform: str = "esp32", + ) -> None: + """Initialize the RAM strings analyzer. + + Args: + elf_path: Path to the ELF file to analyze + objdump_path: Path to objdump binary (used to find other tools) + min_length: Minimum string length to report (default: 8) + platform: Platform name ("esp8266", "esp32", etc.) for section mapping + """ + self.elf_path = Path(elf_path) + if not self.elf_path.exists(): + raise FileNotFoundError(f"ELF file not found: {elf_path}") + + self.objdump_path = objdump_path + self.min_length = min_length + self.platform = platform + + # Set RAM/flash sections based on platform + if self.platform == "esp8266": + self.ram_sections = ESP8266_RAM_SECTIONS + self.flash_sections = ESP8266_FLASH_SECTIONS + else: + # ESP32 and other platforms + self.ram_sections = ESP32_RAM_SECTIONS + self.flash_sections = ESP32_FLASH_SECTIONS + + self.sections: dict[str, SectionInfo] = {} + self.ram_strings: list[RamString] = [] + self.ram_symbols: list[RamSymbol] = [] + + def _run_command(self, cmd: list[str]) -> str: + """Run a command and return its output.""" + try: + result = subprocess.run(cmd, capture_output=True, text=True, check=True) + return result.stdout + except subprocess.CalledProcessError as e: + _LOGGER.debug("Command failed: %s - %s", " ".join(cmd), e.stderr) + raise + except FileNotFoundError: + _LOGGER.warning("Command not found: %s", cmd[0]) + raise + + def analyze(self) -> None: + """Perform the full RAM analysis.""" + self._parse_sections() + self._extract_strings() + self._analyze_symbols() + self._demangle_symbols() + + def _parse_sections(self) -> None: + """Parse section headers from ELF file.""" + objdump = find_tool("objdump", self.objdump_path) + if not objdump: + _LOGGER.error("Could not find objdump command") + return + + try: + output = self._run_command([objdump, "-h", str(self.elf_path)]) + except (subprocess.CalledProcessError, FileNotFoundError): + return + + # Parse section headers + # Format: Idx Name Size VMA LMA File off Algn + section_pattern = re.compile( + r"^\s*\d+\s+(\S+)\s+([0-9a-fA-F]+)\s+([0-9a-fA-F]+)" + ) + + for line in output.split("\n"): + match = section_pattern.match(line) + if match: + name = match.group(1) + size = int(match.group(2), 16) + vma = int(match.group(3), 16) + self.sections[name] = SectionInfo(name, vma, size) + + def _extract_strings(self) -> None: + """Extract strings from RAM sections.""" + objdump = find_tool("objdump", self.objdump_path) + if not objdump: + return + + for section_name in self.ram_sections: + if section_name not in self.sections: + continue + + try: + output = self._run_command( + [objdump, "-s", "-j", section_name, str(self.elf_path)] + ) + except subprocess.CalledProcessError: + # Section may exist but have no content (e.g., .bss) + continue + except FileNotFoundError: + continue + + strings = self._parse_hex_dump(output, section_name) + self.ram_strings.extend(strings) + + def _parse_hex_dump(self, output: str, section_name: str) -> list[RamString]: + """Parse hex dump output to extract strings. + + Args: + output: Output from objdump -s + section_name: Name of the section being parsed + + Returns: + List of RamString objects + """ + strings: list[RamString] = [] + current_string = bytearray() + string_start_addr = 0 + + for line in output.split("\n"): + # Lines look like: " 3ffef8a0 00000000 00000000 00000000 00000000 ................" + match = re.match(r"^\s+([0-9a-fA-F]+)\s+((?:[0-9a-fA-F]{2,8}\s*)+)", line) + if not match: + continue + + addr = int(match.group(1), 16) + hex_data = match.group(2).strip() + + # Convert hex to bytes + hex_bytes = hex_data.split() + byte_offset = 0 + for hex_chunk in hex_bytes: + # Handle both byte-by-byte and word formats + for i in range(0, len(hex_chunk), 2): + byte_val = int(hex_chunk[i : i + 2], 16) + if 0x20 <= byte_val <= 0x7E: # Printable ASCII + if not current_string: + string_start_addr = addr + byte_offset + current_string.append(byte_val) + else: + if byte_val == 0 and len(current_string) >= self.min_length: + # Found null terminator + strings.append( + RamString( + section=section_name, + address=string_start_addr, + content=current_string.decode( + "ascii", errors="ignore" + ), + ) + ) + current_string = bytearray() + byte_offset += 1 + + return strings + + def _analyze_symbols(self) -> None: + """Analyze symbols in RAM sections.""" + nm = find_tool("nm", self.objdump_path) + if not nm: + return + + try: + output = self._run_command([nm, "-S", "--size-sort", str(self.elf_path)]) + except (subprocess.CalledProcessError, FileNotFoundError): + return + + for line in output.split("\n"): + parts = line.split() + if len(parts) < 4: + continue + + try: + addr = int(parts[0], 16) + size = int(parts[1], 16) if parts[1] != "?" else 0 + except ValueError: + continue + + sym_type = parts[2] + name = " ".join(parts[3:]) + + # Filter for data symbols + if sym_type not in ["D", "d", "R", "r", "B", "b"]: + continue + + # Check if symbol is in a RAM section + for section_name in self.ram_sections: + if section_name not in self.sections: + continue + + section = self.sections[section_name] + if section.address <= addr < section.address + section.size: + self.ram_symbols.append( + RamSymbol( + name=name, + sym_type=sym_type, + address=addr, + size=size, + section=section_name, + ) + ) + break + + def _demangle_symbols(self) -> None: + """Batch demangle all RAM symbol names.""" + if not self.ram_symbols: + return + + # Collect all symbol names and demangle them + symbol_names = [s.name for s in self.ram_symbols] + demangle_cache = batch_demangle(symbol_names, objdump_path=self.objdump_path) + + # Assign demangled names to symbols + for symbol in self.ram_symbols: + symbol.demangled = demangle_cache.get(symbol.name, symbol.name) + + def get_total_ram_usage(self) -> int: + """Get total RAM usage from RAM sections.""" + return sum( + section.size + for name, section in self.sections.items() + if name in self.ram_sections + ) + + def get_total_flash_usage(self) -> int: + """Get total flash usage from flash sections.""" + return sum( + section.size + for name, section in self.sections.items() + if name in self.flash_sections + ) + + def get_total_string_bytes(self) -> int: + """Get total bytes used by strings in RAM.""" + return sum(s.size for s in self.ram_strings) + + def get_repeated_strings(self) -> list[tuple[str, int]]: + """Find strings that appear multiple times. + + Returns: + List of (string, count) tuples sorted by potential savings + """ + string_counts: dict[str, int] = defaultdict(int) + for ram_string in self.ram_strings: + string_counts[ram_string.content] += 1 + + return sorted( + [(s, c) for s, c in string_counts.items() if c > 1], + key=lambda x: x[1] * (len(x[0]) + 1), + reverse=True, + ) + + def get_long_strings(self, min_len: int = 20) -> list[RamString]: + """Get strings longer than the specified length. + + Args: + min_len: Minimum string length + + Returns: + List of RamString objects sorted by length + """ + return sorted( + [s for s in self.ram_strings if len(s.content) >= min_len], + key=lambda x: len(x.content), + reverse=True, + ) + + def get_largest_symbols(self, limit: int = 20) -> list[RamSymbol]: + """Get the largest RAM symbols. + + Args: + limit: Maximum number of symbols to return + + Returns: + List of RamSymbol objects sorted by size + """ + return sorted( + [s for s in self.ram_symbols if s.size > 0], + key=lambda x: x.size, + reverse=True, + )[:limit] + + def generate_report(self, show_all_sections: bool = False) -> str: + """Generate a formatted RAM strings analysis report. + + Args: + show_all_sections: If True, show all sections, not just RAM + + Returns: + Formatted report string + """ + lines: list[str] = [] + table_width = 80 + + lines.append("=" * table_width) + lines.append( + f"RAM Strings Analysis ({self.platform.upper()})".center(table_width) + ) + lines.append("=" * table_width) + lines.append("") + + # Section Analysis + lines.append("SECTION ANALYSIS") + lines.append("-" * table_width) + lines.append(f"{'Section':<20} {'Address':<12} {'Size':<12} {'Location'}") + lines.append("-" * table_width) + + total_ram_usage = 0 + total_flash_usage = 0 + + for name, section in sorted(self.sections.items(), key=lambda x: x[1].address): + if name in self.ram_sections: + location = "RAM" + total_ram_usage += section.size + elif name in self.flash_sections: + location = "FLASH" + total_flash_usage += section.size + else: + location = "OTHER" + + if show_all_sections or name in self.ram_sections: + lines.append( + f"{name:<20} 0x{section.address:08x} {section.size:>8} B {location}" + ) + + lines.append("-" * table_width) + lines.append(f"Total RAM sections size: {total_ram_usage:,} bytes") + lines.append(f"Total Flash sections size: {total_flash_usage:,} bytes") + + # Strings in RAM + lines.append("") + lines.append("=" * table_width) + lines.append("STRINGS IN RAM SECTIONS") + lines.append("=" * table_width) + lines.append( + "Note: .bss sections contain uninitialized data (no strings to extract)" + ) + + # Group strings by section + strings_by_section: dict[str, list[RamString]] = defaultdict(list) + for ram_string in self.ram_strings: + strings_by_section[ram_string.section].append(ram_string) + + for section_name in sorted(strings_by_section.keys()): + section_strings = strings_by_section[section_name] + lines.append(f"\nSection: {section_name}") + lines.append("-" * 40) + for ram_string in sorted(section_strings, key=lambda x: x.address): + clean_string = ram_string.content[:100] + ( + "..." if len(ram_string.content) > 100 else "" + ) + lines.append( + f' 0x{ram_string.address:08x}: "{clean_string}" (len={len(ram_string.content)})' + ) + + # Large RAM symbols + lines.append("") + lines.append("=" * table_width) + lines.append("LARGE DATA SYMBOLS IN RAM") + lines.append("=" * table_width) + + largest_symbols = self.get_largest_symbols(20) + lines.append(f"\n{'Symbol':<50} {'Type':<6} {'Size':<10} {'Section'}") + lines.append("-" * table_width) + + for symbol in largest_symbols: + # Use demangled name if available, otherwise raw name + display_name = symbol.demangled or symbol.name + name_display = display_name[:49] if len(display_name) > 49 else display_name + lines.append( + f"{name_display:<50} {symbol.sym_type:<6} {symbol.size:>8} B {symbol.section}" + ) + + # Summary + lines.append("") + lines.append("=" * table_width) + lines.append("SUMMARY") + lines.append("=" * table_width) + lines.append(f"Total strings found in RAM: {len(self.ram_strings)}") + total_string_bytes = self.get_total_string_bytes() + lines.append(f"Total bytes used by strings: {total_string_bytes:,}") + + # Optimization targets + lines.append("") + lines.append("=" * table_width) + lines.append("POTENTIAL OPTIMIZATION TARGETS") + lines.append("=" * table_width) + + # Repeated strings + repeated = self.get_repeated_strings()[:10] + if repeated: + lines.append("\nRepeated strings (could be deduplicated):") + for string, count in repeated: + savings = (count - 1) * (len(string) + 1) + clean_string = string[:50] + ("..." if len(string) > 50 else "") + lines.append( + f' "{clean_string}" - appears {count} times (potential savings: {savings} bytes)' + ) + + # Long strings - platform-specific advice + long_strings = self.get_long_strings(20)[:10] + if long_strings: + if self.platform == "esp8266": + lines.append( + "\nLong strings that could be moved to PROGMEM (>= 20 chars):" + ) + else: + # ESP32: strings in DRAM are typically there for a reason + # (interrupt handlers, pre-flash-init code, etc.) + lines.append("\nLong strings in DRAM (>= 20 chars):") + lines.append( + "Note: ESP32 DRAM strings may be required for interrupt/early-boot contexts" + ) + for ram_string in long_strings: + clean_string = ram_string.content[:60] + ( + "..." if len(ram_string.content) > 60 else "" + ) + lines.append( + f' {ram_string.section} @ 0x{ram_string.address:08x}: "{clean_string}" ({len(ram_string.content)} bytes)' + ) + + lines.append("") + return "\n".join(lines) diff --git a/esphome/analyze_memory/toolchain.py b/esphome/analyze_memory/toolchain.py new file mode 100644 index 0000000000..c6cb2bde81 --- /dev/null +++ b/esphome/analyze_memory/toolchain.py @@ -0,0 +1,54 @@ +"""Toolchain utilities for memory analysis.""" + +from __future__ import annotations + +import logging +from pathlib import Path +import subprocess + +_LOGGER = logging.getLogger(__name__) + +# Platform-specific toolchain prefixes +TOOLCHAIN_PREFIXES = [ + "xtensa-lx106-elf-", # ESP8266 + "xtensa-esp32-elf-", # ESP32 + "xtensa-esp-elf-", # ESP32 (newer IDF) + "", # System default (no prefix) +] + + +def find_tool( + tool_name: str, + objdump_path: str | None = None, +) -> str | None: + """Find a toolchain tool by name. + + First tries to derive the tool path from objdump_path (if provided), + then falls back to searching for platform-specific tools. + + Args: + tool_name: Name of the tool (e.g., "objdump", "nm", "c++filt") + objdump_path: Path to objdump binary to derive other tool paths from + + Returns: + Path to the tool or None if not found + """ + # Try to derive from objdump path first (most reliable) + if objdump_path and objdump_path != "objdump": + potential_path = objdump_path.replace("objdump", tool_name) + if Path(potential_path).exists(): + _LOGGER.debug("Found %s at: %s", tool_name, potential_path) + return potential_path + + # Try platform-specific tools + for prefix in TOOLCHAIN_PREFIXES: + cmd = f"{prefix}{tool_name}" + try: + subprocess.run([cmd, "--version"], capture_output=True, check=True) + _LOGGER.debug("Found %s: %s", tool_name, cmd) + return cmd + except (subprocess.CalledProcessError, FileNotFoundError): + continue + + _LOGGER.warning("Could not find %s tool", tool_name) + return None From eea02a5f0bc91849bd02bf62a8e107e4009a9d0c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 28 Nov 2025 09:44:29 -0600 Subject: [PATCH 352/415] ram --- esphome/analyze_memory/ram_strings.py | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/esphome/analyze_memory/ram_strings.py b/esphome/analyze_memory/ram_strings.py index 9260e9b528..90579f93e5 100644 --- a/esphome/analyze_memory/ram_strings.py +++ b/esphome/analyze_memory/ram_strings.py @@ -28,6 +28,9 @@ ESP8266_FLASH_SECTIONS = frozenset([".irom0.text", ".irom.text", ".text"]) ESP32_RAM_SECTIONS = frozenset([".data", ".bss", ".dram0.data", ".dram0.bss"]) ESP32_FLASH_SECTIONS = frozenset([".text", ".rodata", ".flash.text", ".flash.rodata"]) +# nm symbol types for data symbols (D=global data, d=local data, R=rodata, B=bss) +DATA_SYMBOL_TYPES = frozenset(["D", "d", "R", "r", "B", "b"]) + @dataclass class SectionInfo: @@ -141,8 +144,7 @@ class RamStringsAnalyzer: ) for line in output.split("\n"): - match = section_pattern.match(line) - if match: + if match := section_pattern.match(line): name = match.group(1) size = int(match.group(2), 16) vma = int(match.group(3), 16) @@ -248,7 +250,7 @@ class RamStringsAnalyzer: name = " ".join(parts[3:]) # Filter for data symbols - if sym_type not in ["D", "d", "R", "r", "B", "b"]: + if sym_type not in DATA_SYMBOL_TYPES: continue # Check if symbol is in a RAM section @@ -282,21 +284,21 @@ class RamStringsAnalyzer: for symbol in self.ram_symbols: symbol.demangled = demangle_cache.get(symbol.name, symbol.name) - def get_total_ram_usage(self) -> int: - """Get total RAM usage from RAM sections.""" + def _get_sections_size(self, section_names: frozenset[str]) -> int: + """Get total size of specified sections.""" return sum( section.size for name, section in self.sections.items() - if name in self.ram_sections + if name in section_names ) + def get_total_ram_usage(self) -> int: + """Get total RAM usage from RAM sections.""" + return self._get_sections_size(self.ram_sections) + def get_total_flash_usage(self) -> int: """Get total flash usage from flash sections.""" - return sum( - section.size - for name, section in self.sections.items() - if name in self.flash_sections - ) + return self._get_sections_size(self.flash_sections) def get_total_string_bytes(self) -> int: """Get total bytes used by strings in RAM.""" From e22d78cf4cca142399225ac9d265a28697b8e10f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 28 Nov 2025 09:47:25 -0600 Subject: [PATCH 353/415] tweaks --- esphome/analyze_memory/ram_strings.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/esphome/analyze_memory/ram_strings.py b/esphome/analyze_memory/ram_strings.py index 90579f93e5..fbcbeeca61 100644 --- a/esphome/analyze_memory/ram_strings.py +++ b/esphome/analyze_memory/ram_strings.py @@ -335,20 +335,20 @@ class RamStringsAnalyzer: reverse=True, ) - def get_largest_symbols(self, limit: int = 20) -> list[RamSymbol]: - """Get the largest RAM symbols. + def get_largest_symbols(self, min_size: int = 100) -> list[RamSymbol]: + """Get RAM symbols larger than the specified size. Args: - limit: Maximum number of symbols to return + min_size: Minimum symbol size in bytes Returns: List of RamSymbol objects sorted by size """ return sorted( - [s for s in self.ram_symbols if s.size > 0], + [s for s in self.ram_symbols if s.size >= min_size], key=lambda x: x.size, reverse=True, - )[:limit] + ) def generate_report(self, show_all_sections: bool = False) -> str: """Generate a formatted RAM strings analysis report. @@ -426,10 +426,10 @@ class RamStringsAnalyzer: # Large RAM symbols lines.append("") lines.append("=" * table_width) - lines.append("LARGE DATA SYMBOLS IN RAM") + lines.append("LARGE DATA SYMBOLS IN RAM (>= 50 bytes)") lines.append("=" * table_width) - largest_symbols = self.get_largest_symbols(20) + largest_symbols = self.get_largest_symbols(50) lines.append(f"\n{'Symbol':<50} {'Type':<6} {'Size':<10} {'Section'}") lines.append("-" * table_width) From cd11f31887034b65fb8d139a7d9e2b689ef3d9f5 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 28 Nov 2025 09:57:34 -0600 Subject: [PATCH 354/415] address bot review --- esphome/__main__.py | 23 +++++++++++++---------- esphome/analyze_memory/demangle.py | 10 ++++++++++ esphome/analyze_memory/toolchain.py | 5 ++++- tests/unit_tests/test_main.py | 25 ++++++++++++++++++++++++- 4 files changed, 51 insertions(+), 12 deletions(-) diff --git a/esphome/__main__.py b/esphome/__main__.py index ff1769c8f2..55fbbc6c8a 100644 --- a/esphome/__main__.py +++ b/esphome/__main__.py @@ -984,17 +984,20 @@ def command_analyze_memory(args: ArgsProtocol, config: ConfigType) -> int: # Perform RAM strings analysis _LOGGER.info("Analyzing RAM strings...") - ram_analyzer = RamStringsAnalyzer( - str(firmware_elf), - objdump_path=idedata.objdump_path, - platform=CORE.target_platform, - ) - ram_analyzer.analyze() + try: + ram_analyzer = RamStringsAnalyzer( + str(firmware_elf), + objdump_path=idedata.objdump_path, + platform=CORE.target_platform, + ) + ram_analyzer.analyze() - # Generate and display RAM strings report - ram_report = ram_analyzer.generate_report() - print() - print(ram_report) + # Generate and display RAM strings report + ram_report = ram_analyzer.generate_report() + print() + print(ram_report) + except Exception as e: # pylint: disable=broad-except + _LOGGER.warning("RAM strings analysis failed: %s", e) return 0 diff --git a/esphome/analyze_memory/demangle.py b/esphome/analyze_memory/demangle.py index 7ff06ce870..8999108b51 100644 --- a/esphome/analyze_memory/demangle.py +++ b/esphome/analyze_memory/demangle.py @@ -141,6 +141,16 @@ def batch_demangle( # Process demangled output demangled_lines = result.stdout.strip().split("\n") + + # Check for output length mismatch + if len(demangled_lines) != len(symbols): + _LOGGER.warning( + "c++filt output mismatch: expected %d lines, got %d", + len(symbols), + len(demangled_lines), + ) + return {s: s for s in symbols} + failed_count = 0 for original, stripped, prefix, demangled in zip( diff --git a/esphome/analyze_memory/toolchain.py b/esphome/analyze_memory/toolchain.py index c6cb2bde81..e766252412 100644 --- a/esphome/analyze_memory/toolchain.py +++ b/esphome/analyze_memory/toolchain.py @@ -35,7 +35,10 @@ def find_tool( """ # Try to derive from objdump path first (most reliable) if objdump_path and objdump_path != "objdump": - potential_path = objdump_path.replace("objdump", tool_name) + objdump_file = Path(objdump_path) + # Replace just the filename portion, preserving any prefix (e.g., xtensa-esp32-elf-) + new_name = objdump_file.name.replace("objdump", tool_name) + potential_path = str(objdump_file.with_name(new_name)) if Path(potential_path).exists(): _LOGGER.debug("Found %s at: %s", tool_name, potential_path) return potential_path diff --git a/tests/unit_tests/test_main.py b/tests/unit_tests/test_main.py index ccbc5a1306..670d6c16fc 100644 --- a/tests/unit_tests/test_main.py +++ b/tests/unit_tests/test_main.py @@ -269,6 +269,16 @@ def mock_memory_analyzer_cli() -> Generator[Mock]: yield mock_class +@pytest.fixture +def mock_ram_strings_analyzer() -> Generator[Mock]: + """Mock RamStringsAnalyzer for testing.""" + with patch("esphome.analyze_memory.ram_strings.RamStringsAnalyzer") as mock_class: + mock_analyzer = MagicMock() + mock_analyzer.generate_report.return_value = "Mock RAM Strings Report" + mock_class.return_value = mock_analyzer + yield mock_class + + def test_choose_upload_log_host_with_string_default() -> None: """Test with a single string default device.""" setup_core() @@ -2424,6 +2434,7 @@ def test_command_analyze_memory_success( mock_get_idedata: Mock, mock_get_esphome_components: Mock, mock_memory_analyzer_cli: Mock, + mock_ram_strings_analyzer: Mock, ) -> None: """Test command_analyze_memory with successful compilation and analysis.""" setup_core(platform=PLATFORM_ESP32, tmp_path=tmp_path, name="test_device") @@ -2471,9 +2482,20 @@ def test_command_analyze_memory_success( mock_analyzer.analyze.assert_called_once() mock_analyzer.generate_report.assert_called_once() - # Verify report was printed + # Verify RAM strings analyzer was created and run + mock_ram_strings_analyzer.assert_called_once_with( + str(firmware_elf), + objdump_path="/path/to/objdump", + platform="esp32", + ) + mock_ram_analyzer = mock_ram_strings_analyzer.return_value + mock_ram_analyzer.analyze.assert_called_once() + mock_ram_analyzer.generate_report.assert_called_once() + + # Verify reports were printed captured = capfd.readouterr() assert "Mock Memory Report" in captured.out + assert "Mock RAM Strings Report" in captured.out def test_command_analyze_memory_with_external_components( @@ -2483,6 +2505,7 @@ def test_command_analyze_memory_with_external_components( mock_get_idedata: Mock, mock_get_esphome_components: Mock, mock_memory_analyzer_cli: Mock, + mock_ram_strings_analyzer: Mock, ) -> None: """Test command_analyze_memory detects external components.""" setup_core(platform=PLATFORM_ESP32, tmp_path=tmp_path, name="test_device") From 632af6bda36e0da064f4d1426b09664eca7eaa62 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 28 Nov 2025 10:13:03 -0600 Subject: [PATCH 355/415] [lock] Store lock state strings in flash on ESP8266 --- esphome/components/lock/lock.cpp | 21 ++++++++++---------- esphome/components/lock/lock.h | 5 ++++- esphome/components/mqtt/mqtt_lock.cpp | 10 ++++++++-- esphome/components/web_server/web_server.cpp | 3 ++- 4 files changed, 24 insertions(+), 15 deletions(-) diff --git a/esphome/components/lock/lock.cpp b/esphome/components/lock/lock.cpp index b8f0fbe011..018f5113e3 100644 --- a/esphome/components/lock/lock.cpp +++ b/esphome/components/lock/lock.cpp @@ -7,21 +7,21 @@ namespace esphome::lock { static const char *const TAG = "lock"; -const char *lock_state_to_string(LockState state) { +const LogString *lock_state_to_string(LockState state) { switch (state) { case LOCK_STATE_LOCKED: - return "LOCKED"; + return LOG_STR("LOCKED"); case LOCK_STATE_UNLOCKED: - return "UNLOCKED"; + return LOG_STR("UNLOCKED"); case LOCK_STATE_JAMMED: - return "JAMMED"; + return LOG_STR("JAMMED"); case LOCK_STATE_LOCKING: - return "LOCKING"; + return LOG_STR("LOCKING"); case LOCK_STATE_UNLOCKING: - return "UNLOCKING"; + return LOG_STR("UNLOCKING"); case LOCK_STATE_NONE: default: - return "UNKNOWN"; + return LOG_STR("UNKNOWN"); } } @@ -52,7 +52,7 @@ void Lock::publish_state(LockState state) { this->state = state; this->rtc_.save(&this->state); - ESP_LOGD(TAG, "'%s': Sending state %s", this->name_.c_str(), lock_state_to_string(state)); + ESP_LOGD(TAG, "'%s': Sending state %s", this->name_.c_str(), LOG_STR_ARG(lock_state_to_string(state))); this->state_callback_.call(); #if defined(USE_LOCK) && defined(USE_CONTROLLER_REGISTRY) ControllerRegistry::notify_lock_update(this); @@ -65,8 +65,7 @@ void LockCall::perform() { ESP_LOGD(TAG, "'%s' - Setting", this->parent_->get_name().c_str()); this->validate_(); if (this->state_.has_value()) { - const char *state_s = lock_state_to_string(*this->state_); - ESP_LOGD(TAG, " State: %s", state_s); + ESP_LOGD(TAG, " State: %s", LOG_STR_ARG(lock_state_to_string(*this->state_))); } this->parent_->control(*this); } @@ -74,7 +73,7 @@ void LockCall::validate_() { if (this->state_.has_value()) { auto state = *this->state_; if (!this->parent_->traits.supports_state(state)) { - ESP_LOGW(TAG, " State %s is not supported by this device!", lock_state_to_string(*this->state_)); + ESP_LOGW(TAG, " State %s is not supported by this device!", LOG_STR_ARG(lock_state_to_string(*this->state_))); this->state_.reset(); } } diff --git a/esphome/components/lock/lock.h b/esphome/components/lock/lock.h index 8a906ef9fc..4001a182b8 100644 --- a/esphome/components/lock/lock.h +++ b/esphome/components/lock/lock.h @@ -30,7 +30,10 @@ enum LockState : uint8_t { LOCK_STATE_LOCKING = 4, LOCK_STATE_UNLOCKING = 5 }; -const char *lock_state_to_string(LockState state); +const LogString *lock_state_to_string(LockState state); + +/// Maximum length of lock state string (including null terminator): "UNLOCKING" = 10 +static constexpr size_t LOCK_STATE_STR_SIZE = 10; class LockTraits { public: diff --git a/esphome/components/mqtt/mqtt_lock.cpp b/esphome/components/mqtt/mqtt_lock.cpp index 0e15377ba4..95efbf60e1 100644 --- a/esphome/components/mqtt/mqtt_lock.cpp +++ b/esphome/components/mqtt/mqtt_lock.cpp @@ -48,8 +48,14 @@ void MQTTLockComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfi bool MQTTLockComponent::send_initial_state() { return this->publish_state(); } bool MQTTLockComponent::publish_state() { - std::string payload = lock_state_to_string(this->lock_->state); - return this->publish(this->get_state_topic_(), payload); +#ifdef USE_STORE_LOG_STR_IN_FLASH + char buf[LOCK_STATE_STR_SIZE]; + strncpy_P(buf, (PGM_P) lock_state_to_string(this->lock_->state), sizeof(buf) - 1); + buf[sizeof(buf) - 1] = '\0'; + return this->publish(this->get_state_topic_(), buf); +#else + return this->publish(this->get_state_topic_(), LOG_STR_ARG(lock_state_to_string(this->lock_->state))); +#endif } } // namespace mqtt diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index f5ca674161..4bb55891ee 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -1482,7 +1482,8 @@ std::string WebServer::lock_json(lock::Lock *obj, lock::LockState value, JsonDet json::JsonBuilder builder; JsonObject root = builder.root(); - set_json_icon_state_value(root, obj, "lock", lock::lock_state_to_string(value), value, start_config); + char buf[lock::LOCK_STATE_STR_SIZE]; + set_json_icon_state_value(root, obj, "lock", PSTR_LOCAL(lock::lock_state_to_string(value)), value, start_config); if (start_config == DETAIL_ALL) { this->add_sorting_info_(root, obj); } From a212161f68c6f01ada85e42cf925d009a28bbc63 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 28 Nov 2025 10:30:12 -0600 Subject: [PATCH 356/415] cleanup --- esphome/components/web_server/web_server.cpp | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index 4bb55891ee..ecc128bf2c 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -1272,8 +1272,9 @@ std::string WebServer::select_json(select::Select *obj, const char *value, JsonD } #endif -// Longest: HORIZONTAL -#define PSTR_LOCAL(mode_s) ESPHOME_strncpy_P(buf, (ESPHOME_PGM_P) ((mode_s)), 15) +// Longest: HORIZONTAL (10 chars + null terminator, rounded up) +static constexpr size_t PSTR_LOCAL_SIZE = 16; +#define PSTR_LOCAL(mode_s) ESPHOME_strncpy_P(buf, (ESPHOME_PGM_P) ((mode_s)), PSTR_LOCAL_SIZE - 1) #ifdef USE_CLIMATE void WebServer::on_climate_update(climate::Climate *obj) { @@ -1482,7 +1483,7 @@ std::string WebServer::lock_json(lock::Lock *obj, lock::LockState value, JsonDet json::JsonBuilder builder; JsonObject root = builder.root(); - char buf[lock::LOCK_STATE_STR_SIZE]; + char buf[PSTR_LOCAL_SIZE]; set_json_icon_state_value(root, obj, "lock", PSTR_LOCAL(lock::lock_state_to_string(value)), value, start_config); if (start_config == DETAIL_ALL) { this->add_sorting_info_(root, obj); From 2a67933062916fcc06f3896448d7bd55747c06a8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 28 Nov 2025 10:30:33 -0600 Subject: [PATCH 357/415] small cleanups --- esphome/components/web_server/web_server.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index ecc128bf2c..77e604fdc6 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -1333,7 +1333,7 @@ std::string WebServer::climate_json(climate::Climate *obj, JsonDetail start_conf const auto traits = obj->get_traits(); int8_t target_accuracy = traits.get_target_temperature_accuracy_decimals(); int8_t current_accuracy = traits.get_current_temperature_accuracy_decimals(); - char buf[16]; + char buf[PSTR_LOCAL_SIZE]; if (start_config == DETAIL_ALL) { JsonArray opt = root["modes"].to(); @@ -1645,7 +1645,7 @@ std::string WebServer::alarm_control_panel_json(alarm_control_panel::AlarmContro json::JsonBuilder builder; JsonObject root = builder.root(); - char buf[16]; + char buf[PSTR_LOCAL_SIZE]; set_json_icon_state_value(root, obj, "alarm-control-panel", PSTR_LOCAL(alarm_control_panel_state_to_string(value)), value, start_config); if (start_config == DETAIL_ALL) { From 60ffa0e52ef4a83c23052467e53c9d5d3ab20b29 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 28 Nov 2025 11:27:08 -0600 Subject: [PATCH 358/415] [esp32_ble_tracker] Replace scanner state callback with listener interface (#12156) --- .../bluetooth_proxy/bluetooth_proxy.cpp | 12 +++++++----- .../bluetooth_proxy/bluetooth_proxy.h | 7 ++++++- .../esp32_ble_tracker/esp32_ble_tracker.cpp | 4 +++- .../esp32_ble_tracker/esp32_ble_tracker.h | 19 +++++++++++++++---- 4 files changed, 31 insertions(+), 11 deletions(-) diff --git a/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp b/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp index 71f8da75a7..d45377b3f6 100644 --- a/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp +++ b/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp @@ -27,11 +27,13 @@ void BluetoothProxy::setup() { // Capture the configured scan mode from YAML before any API changes this->configured_scan_active_ = this->parent_->get_scan_active(); - this->parent_->add_scanner_state_callback([this](esp32_ble_tracker::ScannerState state) { - if (this->api_connection_ != nullptr) { - this->send_bluetooth_scanner_state_(state); - } - }); + this->parent_->add_scanner_state_listener(this); +} + +void BluetoothProxy::on_scanner_state(esp32_ble_tracker::ScannerState state) { + if (this->api_connection_ != nullptr) { + this->send_bluetooth_scanner_state_(state); + } } void BluetoothProxy::send_bluetooth_scanner_state_(esp32_ble_tracker::ScannerState state) { diff --git a/esphome/components/bluetooth_proxy/bluetooth_proxy.h b/esphome/components/bluetooth_proxy/bluetooth_proxy.h index 4363c508ec..ab9aee2d81 100644 --- a/esphome/components/bluetooth_proxy/bluetooth_proxy.h +++ b/esphome/components/bluetooth_proxy/bluetooth_proxy.h @@ -52,7 +52,9 @@ enum BluetoothProxySubscriptionFlag : uint32_t { SUBSCRIPTION_RAW_ADVERTISEMENTS = 1 << 0, }; -class BluetoothProxy final : public esp32_ble_tracker::ESPBTDeviceListener, public Component { +class BluetoothProxy final : public esp32_ble_tracker::ESPBTDeviceListener, + public esp32_ble_tracker::BLEScannerStateListener, + public Component { friend class BluetoothConnection; // Allow connection to update connections_free_response_ public: BluetoothProxy(); @@ -108,6 +110,9 @@ class BluetoothProxy final : public esp32_ble_tracker::ESPBTDeviceListener, publ void set_active(bool active) { this->active_ = active; } bool has_active() { return this->active_; } + /// BLEScannerStateListener interface + void on_scanner_state(esp32_ble_tracker::ScannerState state) override; + uint32_t get_legacy_version() const { if (this->active_) { return LEGACY_ACTIVE_CONNECTIONS_VERSION; diff --git a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp index 8577f12a92..d3c5edfb94 100644 --- a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp +++ b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp @@ -373,7 +373,9 @@ void ESP32BLETracker::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_i void ESP32BLETracker::set_scanner_state_(ScannerState state) { this->scanner_state_ = state; - this->scanner_state_callbacks_.call(state); + for (auto *listener : this->scanner_state_listeners_) { + listener->on_scanner_state(state); + } } #ifdef USE_ESP32_BLE_DEVICE diff --git a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h index f80f3e2670..92d13a62ad 100644 --- a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h +++ b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h @@ -180,6 +180,16 @@ enum class ScannerState { STOPPING, }; +/** Listener interface for BLE scanner state changes. + * + * Components can implement this interface to receive scanner state updates + * without the overhead of std::function callbacks. + */ +class BLEScannerStateListener { + public: + virtual void on_scanner_state(ScannerState state) = 0; +}; + // Helper function to convert ClientState to string const char *client_state_to_string(ClientState state); @@ -264,8 +274,9 @@ class ESP32BLETracker : public Component, void gap_scan_event_handler(const BLEScanResult &scan_result) override; void ble_before_disabled_event_handler() override; - void add_scanner_state_callback(std::function &&callback) { - this->scanner_state_callbacks_.add(std::move(callback)); + /// Add a listener for scanner state changes + void add_scanner_state_listener(BLEScannerStateListener *listener) { + this->scanner_state_listeners_.push_back(listener); } ScannerState get_scanner_state() const { return this->scanner_state_; } @@ -322,14 +333,14 @@ class ESP32BLETracker : public Component, return counts; } - // Group 1: Large objects (12+ bytes) - vectors and callback manager + // Group 1: Large objects (12+ bytes) - vectors #ifdef ESPHOME_ESP32_BLE_TRACKER_LISTENER_COUNT StaticVector listeners_; #endif #ifdef ESPHOME_ESP32_BLE_TRACKER_CLIENT_COUNT StaticVector clients_; #endif - CallbackManager scanner_state_callbacks_; + std::vector scanner_state_listeners_; #ifdef USE_ESP32_BLE_DEVICE /// Vector of addresses that have already been printed in print_bt_device_info std::vector already_discovered_; From 26e979d3d5a1d80c479ba4729a37875d8580ec64 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 28 Nov 2025 11:27:17 -0600 Subject: [PATCH 359/415] [wifi] Replace std::function callbacks with listener interfaces (#12155) --- esphome/components/wifi/__init__.py | 16 ++-- esphome/components/wifi/wifi_component.h | 73 +++++++++++++------ .../wifi/wifi_component_esp8266.cpp | 28 ++++--- .../wifi/wifi_component_esp_idf.cpp | 30 +++++--- .../wifi/wifi_component_libretiny.cpp | 30 +++++--- .../components/wifi/wifi_component_pico_w.cpp | 24 ++++-- esphome/components/wifi_info/text_sensor.py | 6 +- .../wifi_info/wifi_info_text_sensor.cpp | 46 ++++-------- .../wifi_info/wifi_info_text_sensor.h | 36 +++++---- esphome/core/defines.h | 2 +- 10 files changed, 172 insertions(+), 119 deletions(-) diff --git a/esphome/components/wifi/__init__.py b/esphome/components/wifi/__init__.py index 31d9ca0f70..2c10506011 100644 --- a/esphome/components/wifi/__init__.py +++ b/esphome/components/wifi/__init__.py @@ -608,7 +608,7 @@ async def wifi_disable_to_code(config, action_id, template_arg, args): KEEP_SCAN_RESULTS_KEY = "wifi_keep_scan_results" RUNTIME_POWER_SAVE_KEY = "wifi_runtime_power_save" -WIFI_CALLBACKS_KEY = "wifi_callbacks" +WIFI_LISTENERS_KEY = "wifi_listeners" def request_wifi_scan_results(): @@ -634,15 +634,15 @@ def enable_runtime_power_save_control(): CORE.data[RUNTIME_POWER_SAVE_KEY] = True -def request_wifi_callbacks() -> None: - """Request that WiFi callbacks be compiled in. +def request_wifi_listeners() -> None: + """Request that WiFi state listeners be compiled in. Components that need to be notified about WiFi state changes (IP address changes, scan results, connection state) should call this function during their code generation. - This enables the add_on_ip_state_callback(), add_on_wifi_scan_state_callback(), - and add_on_wifi_connect_state_callback() APIs. + This enables the add_ip_state_listener(), add_scan_results_listener(), + and add_connect_state_listener() APIs. """ - CORE.data[WIFI_CALLBACKS_KEY] = True + CORE.data[WIFI_LISTENERS_KEY] = True @coroutine_with_priority(CoroPriority.FINAL) @@ -654,8 +654,8 @@ async def final_step(): ) if CORE.data.get(RUNTIME_POWER_SAVE_KEY, False): cg.add_define("USE_WIFI_RUNTIME_POWER_SAVE") - if CORE.data.get(WIFI_CALLBACKS_KEY, False): - cg.add_define("USE_WIFI_CALLBACKS") + if CORE.data.get(WIFI_LISTENERS_KEY, False): + cg.add_define("USE_WIFI_LISTENERS") @automation.register_action( diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index a9b03a8b8d..97cc3961fe 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -242,6 +242,37 @@ enum WifiMinAuthMode : uint8_t { struct IDFWiFiEvent; #endif +/** Listener interface for WiFi IP state changes. + * + * Components can implement this interface to receive IP address updates + * without the overhead of std::function callbacks. + */ +class WiFiIPStateListener { + public: + virtual void on_ip_state(const network::IPAddresses &ips, const network::IPAddress &dns1, + const network::IPAddress &dns2) = 0; +}; + +/** Listener interface for WiFi scan results. + * + * Components can implement this interface to receive scan results + * without the overhead of std::function callbacks. + */ +class WiFiScanResultsListener { + public: + virtual void on_wifi_scan_results(const wifi_scan_vector_t &results) = 0; +}; + +/** Listener interface for WiFi connection state changes. + * + * Components can implement this interface to receive connection updates + * without the overhead of std::function callbacks. + */ +class WiFiConnectStateListener { + public: + virtual void on_wifi_connect_state(const std::string &ssid, const bssid_t &bssid) = 0; +}; + /// This component is responsible for managing the ESP WiFi interface. class WiFiComponent : public Component { public: @@ -373,26 +404,22 @@ class WiFiComponent : public Component { int32_t get_wifi_channel(); -#ifdef USE_WIFI_CALLBACKS - /// Add a callback that will be called on configuration changes (IP change, SSID change, etc.) - /// @param callback The callback to be called; template arguments are: - /// - IP addresses - /// - DNS address 1 - /// - DNS address 2 - void add_on_ip_state_callback( - std::function &&callback) { - this->ip_state_callback_.add(std::move(callback)); +#ifdef USE_WIFI_LISTENERS + /** Add a listener for IP state changes. + * Listener receives: IP addresses, DNS address 1, DNS address 2 + */ + void add_ip_state_listener(WiFiIPStateListener *listener) { this->ip_state_listeners_.push_back(listener); } + /// Add a listener for WiFi scan results + void add_scan_results_listener(WiFiScanResultsListener *listener) { + this->scan_results_listeners_.push_back(listener); } - /// - Wi-Fi scan results - void add_on_wifi_scan_state_callback(std::function &)> &&callback) { - this->wifi_scan_state_callback_.add(std::move(callback)); + /** Add a listener for WiFi connection state changes. + * Listener receives: SSID, BSSID + */ + void add_connect_state_listener(WiFiConnectStateListener *listener) { + this->connect_state_listeners_.push_back(listener); } - /// - Wi-Fi SSID - /// - Wi-Fi BSSID - void add_on_wifi_connect_state_callback(std::function &&callback) { - this->wifi_connect_state_callback_.add(std::move(callback)); - } -#endif // USE_WIFI_CALLBACKS +#endif // USE_WIFI_LISTENERS #ifdef USE_WIFI_RUNTIME_POWER_SAVE /** Request high-performance mode (no power saving) for improved WiFi latency. @@ -550,11 +577,11 @@ class WiFiComponent : public Component { WiFiAP ap_; #endif optional output_power_; -#ifdef USE_WIFI_CALLBACKS - CallbackManager ip_state_callback_; - CallbackManager &)> wifi_scan_state_callback_; - CallbackManager wifi_connect_state_callback_; -#endif // USE_WIFI_CALLBACKS +#ifdef USE_WIFI_LISTENERS + std::vector ip_state_listeners_; + std::vector scan_results_listeners_; + std::vector connect_state_listeners_; +#endif // USE_WIFI_LISTENERS ESPPreferenceObject pref_; #ifdef USE_WIFI_FAST_CONNECT ESPPreferenceObject fast_connect_pref_; diff --git a/esphome/components/wifi/wifi_component_esp8266.cpp b/esphome/components/wifi/wifi_component_esp8266.cpp index 2d1f979c83..701cae5f7c 100644 --- a/esphome/components/wifi/wifi_component_esp8266.cpp +++ b/esphome/components/wifi/wifi_component_esp8266.cpp @@ -513,9 +513,10 @@ void WiFiComponent::wifi_event_callback(System_Event_t *event) { ESP_LOGV(TAG, "Connected ssid='%s' bssid=%s channel=%u", buf, format_mac_address_pretty(it.bssid).c_str(), it.channel); s_sta_connected = true; -#ifdef USE_WIFI_CALLBACKS - global_wifi_component->wifi_connect_state_callback_.call(global_wifi_component->wifi_ssid(), - global_wifi_component->wifi_bssid()); +#ifdef USE_WIFI_LISTENERS + for (auto *listener : global_wifi_component->connect_state_listeners_) { + listener->on_wifi_connect_state(global_wifi_component->wifi_ssid(), global_wifi_component->wifi_bssid()); + } #endif break; } @@ -536,8 +537,10 @@ void WiFiComponent::wifi_event_callback(System_Event_t *event) { } s_sta_connected = false; s_sta_connecting = false; -#ifdef USE_WIFI_CALLBACKS - global_wifi_component->wifi_connect_state_callback_.call("", bssid_t({0, 0, 0, 0, 0, 0})); +#ifdef USE_WIFI_LISTENERS + for (auto *listener : global_wifi_component->connect_state_listeners_) { + listener->on_wifi_connect_state("", bssid_t({0, 0, 0, 0, 0, 0})); + } #endif break; } @@ -561,10 +564,11 @@ void WiFiComponent::wifi_event_callback(System_Event_t *event) { ESP_LOGV(TAG, "static_ip=%s gateway=%s netmask=%s", format_ip_addr(it.ip).c_str(), format_ip_addr(it.gw).c_str(), format_ip_addr(it.mask).c_str()); s_sta_got_ip = true; -#ifdef USE_WIFI_CALLBACKS - global_wifi_component->ip_state_callback_.call(global_wifi_component->wifi_sta_ip_addresses(), - global_wifi_component->get_dns_address(0), - global_wifi_component->get_dns_address(1)); +#ifdef USE_WIFI_LISTENERS + for (auto *listener : global_wifi_component->ip_state_listeners_) { + listener->on_ip_state(global_wifi_component->wifi_sta_ip_addresses(), global_wifi_component->get_dns_address(0), + global_wifi_component->get_dns_address(1)); + } #endif break; } @@ -740,8 +744,10 @@ void WiFiComponent::wifi_scan_done_callback_(void *arg, STATUS status) { it->is_hidden != 0); } this->scan_done_ = true; -#ifdef USE_WIFI_CALLBACKS - global_wifi_component->wifi_scan_state_callback_.call(global_wifi_component->scan_result_); +#ifdef USE_WIFI_LISTENERS + for (auto *listener : global_wifi_component->scan_results_listeners_) { + listener->on_wifi_scan_results(global_wifi_component->scan_result_); + } #endif } diff --git a/esphome/components/wifi/wifi_component_esp_idf.cpp b/esphome/components/wifi/wifi_component_esp_idf.cpp index c20c96ced0..3d25d2890f 100644 --- a/esphome/components/wifi/wifi_component_esp_idf.cpp +++ b/esphome/components/wifi/wifi_component_esp_idf.cpp @@ -727,8 +727,10 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) { ESP_LOGV(TAG, "Connected ssid='%s' bssid=" LOG_SECRET("%s") " channel=%u, authmode=%s", buf, format_mac_address_pretty(it.bssid).c_str(), it.channel, get_auth_mode_str(it.authmode)); s_sta_connected = true; -#ifdef USE_WIFI_CALLBACKS - this->wifi_connect_state_callback_.call(this->wifi_ssid(), this->wifi_bssid()); +#ifdef USE_WIFI_LISTENERS + for (auto *listener : this->connect_state_listeners_) { + listener->on_wifi_connect_state(this->wifi_ssid(), this->wifi_bssid()); + } #endif } else if (data->event_base == WIFI_EVENT && data->event_id == WIFI_EVENT_STA_DISCONNECTED) { @@ -753,8 +755,10 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) { s_sta_connected = false; s_sta_connecting = false; error_from_callback_ = true; -#ifdef USE_WIFI_CALLBACKS - this->wifi_connect_state_callback_.call("", bssid_t({0, 0, 0, 0, 0, 0})); +#ifdef USE_WIFI_LISTENERS + for (auto *listener : this->connect_state_listeners_) { + listener->on_wifi_connect_state("", bssid_t({0, 0, 0, 0, 0, 0})); + } #endif } else if (data->event_base == IP_EVENT && data->event_id == IP_EVENT_STA_GOT_IP) { @@ -764,8 +768,10 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) { #endif /* USE_NETWORK_IPV6 */ ESP_LOGV(TAG, "static_ip=" IPSTR " gateway=" IPSTR, IP2STR(&it.ip_info.ip), IP2STR(&it.ip_info.gw)); this->got_ipv4_address_ = true; -#ifdef USE_WIFI_CALLBACKS - this->ip_state_callback_.call(this->wifi_sta_ip_addresses(), this->get_dns_address(0), this->get_dns_address(1)); +#ifdef USE_WIFI_LISTENERS + for (auto *listener : this->ip_state_listeners_) { + listener->on_ip_state(this->wifi_sta_ip_addresses(), this->get_dns_address(0), this->get_dns_address(1)); + } #endif #if USE_NETWORK_IPV6 @@ -773,8 +779,10 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) { const auto &it = data->data.ip_got_ip6; ESP_LOGV(TAG, "IPv6 address=" IPV6STR, IPV62STR(it.ip6_info.ip)); this->num_ipv6_addresses_++; -#ifdef USE_WIFI_CALLBACKS - this->ip_state_callback_.call(this->wifi_sta_ip_addresses(), this->get_dns_address(0), this->get_dns_address(1)); +#ifdef USE_WIFI_LISTENERS + for (auto *listener : this->ip_state_listeners_) { + listener->on_ip_state(this->wifi_sta_ip_addresses(), this->get_dns_address(0), this->get_dns_address(1)); + } #endif #endif /* USE_NETWORK_IPV6 */ @@ -815,8 +823,10 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) { scan_result_.emplace_back(bssid, ssid, record.primary, record.rssi, record.authmode != WIFI_AUTH_OPEN, ssid.empty()); } -#ifdef USE_WIFI_CALLBACKS - this->wifi_scan_state_callback_.call(this->scan_result_); +#ifdef USE_WIFI_LISTENERS + for (auto *listener : this->scan_results_listeners_) { + listener->on_wifi_scan_results(this->scan_result_); + } #endif } else if (data->event_base == WIFI_EVENT && data->event_id == WIFI_EVENT_AP_START) { diff --git a/esphome/components/wifi/wifi_component_libretiny.cpp b/esphome/components/wifi/wifi_component_libretiny.cpp index 04d0d4fa85..f1405d3bef 100644 --- a/esphome/components/wifi/wifi_component_libretiny.cpp +++ b/esphome/components/wifi/wifi_component_libretiny.cpp @@ -287,8 +287,10 @@ void WiFiComponent::wifi_event_callback_(esphome_wifi_event_id_t event, esphome_ buf[it.ssid_len] = '\0'; ESP_LOGV(TAG, "Connected ssid='%s' bssid=" LOG_SECRET("%s") " channel=%u, authmode=%s", buf, format_mac_address_pretty(it.bssid).c_str(), it.channel, get_auth_mode_str(it.authmode)); -#ifdef USE_WIFI_CALLBACKS - this->wifi_connect_state_callback_.call(this->wifi_ssid(), this->wifi_bssid()); +#ifdef USE_WIFI_LISTENERS + for (auto *listener : this->connect_state_listeners_) { + listener->on_wifi_connect_state(this->wifi_ssid(), this->wifi_bssid()); + } #endif break; } @@ -315,8 +317,10 @@ void WiFiComponent::wifi_event_callback_(esphome_wifi_event_id_t event, esphome_ } s_sta_connecting = false; -#ifdef USE_WIFI_CALLBACKS - this->wifi_connect_state_callback_.call("", bssid_t({0, 0, 0, 0, 0, 0})); +#ifdef USE_WIFI_LISTENERS + for (auto *listener : this->connect_state_listeners_) { + listener->on_wifi_connect_state("", bssid_t({0, 0, 0, 0, 0, 0})); + } #endif break; } @@ -339,16 +343,20 @@ void WiFiComponent::wifi_event_callback_(esphome_wifi_event_id_t event, esphome_ ESP_LOGV(TAG, "static_ip=%s gateway=%s", format_ip4_addr(WiFi.localIP()).c_str(), format_ip4_addr(WiFi.gatewayIP()).c_str()); s_sta_connecting = false; -#ifdef USE_WIFI_CALLBACKS - this->ip_state_callback_.call(this->wifi_sta_ip_addresses(), this->get_dns_address(0), this->get_dns_address(1)); +#ifdef USE_WIFI_LISTENERS + for (auto *listener : this->ip_state_listeners_) { + listener->on_ip_state(this->wifi_sta_ip_addresses(), this->get_dns_address(0), this->get_dns_address(1)); + } #endif break; } case ESPHOME_EVENT_ID_WIFI_STA_GOT_IP6: { // auto it = info.got_ip.ip_info; ESP_LOGV(TAG, "Got IPv6"); -#ifdef USE_WIFI_CALLBACKS - this->ip_state_callback_.call(this->wifi_sta_ip_addresses(), this->get_dns_address(0), this->get_dns_address(1)); +#ifdef USE_WIFI_LISTENERS + for (auto *listener : this->ip_state_listeners_) { + listener->on_ip_state(this->wifi_sta_ip_addresses(), this->get_dns_address(0), this->get_dns_address(1)); + } #endif break; } @@ -443,8 +451,10 @@ void WiFiComponent::wifi_scan_done_callback_() { } WiFi.scanDelete(); this->scan_done_ = true; -#ifdef USE_WIFI_CALLBACKS - this->wifi_scan_state_callback_.call(this->scan_result_); +#ifdef USE_WIFI_LISTENERS + for (auto *listener : this->scan_results_listeners_) { + listener->on_wifi_scan_results(this->scan_result_); + } #endif } diff --git a/esphome/components/wifi/wifi_component_pico_w.cpp b/esphome/components/wifi/wifi_component_pico_w.cpp index 326883c0c4..1a8b75213c 100644 --- a/esphome/components/wifi/wifi_component_pico_w.cpp +++ b/esphome/components/wifi/wifi_component_pico_w.cpp @@ -225,8 +225,10 @@ void WiFiComponent::wifi_loop_() { if (this->state_ == WIFI_COMPONENT_STATE_STA_SCANNING && !cyw43_wifi_scan_active(&cyw43_state)) { this->scan_done_ = true; ESP_LOGV(TAG, "Scan done"); -#ifdef USE_WIFI_CALLBACKS - this->wifi_scan_state_callback_.call(this->scan_result_); +#ifdef USE_WIFI_LISTENERS + for (auto *listener : this->scan_results_listeners_) { + listener->on_wifi_scan_results(this->scan_result_); + } #endif } @@ -241,16 +243,20 @@ void WiFiComponent::wifi_loop_() { // Just connected s_sta_was_connected = true; ESP_LOGV(TAG, "Connected"); -#ifdef USE_WIFI_CALLBACKS - this->wifi_connect_state_callback_.call(this->wifi_ssid(), this->wifi_bssid()); +#ifdef USE_WIFI_LISTENERS + for (auto *listener : this->connect_state_listeners_) { + listener->on_wifi_connect_state(this->wifi_ssid(), this->wifi_bssid()); + } #endif } else if (!is_connected && s_sta_was_connected) { // Just disconnected s_sta_was_connected = false; s_sta_had_ip = false; ESP_LOGV(TAG, "Disconnected"); -#ifdef USE_WIFI_CALLBACKS - this->wifi_connect_state_callback_.call("", bssid_t({0, 0, 0, 0, 0, 0})); +#ifdef USE_WIFI_LISTENERS + for (auto *listener : this->connect_state_listeners_) { + listener->on_wifi_connect_state("", bssid_t({0, 0, 0, 0, 0, 0})); + } #endif } @@ -267,8 +273,10 @@ void WiFiComponent::wifi_loop_() { // Just got IP address s_sta_had_ip = true; ESP_LOGV(TAG, "Got IP address"); -#ifdef USE_WIFI_CALLBACKS - this->ip_state_callback_.call(this->wifi_sta_ip_addresses(), this->get_dns_address(0), this->get_dns_address(1)); +#ifdef USE_WIFI_LISTENERS + for (auto *listener : this->ip_state_listeners_) { + listener->on_ip_state(this->wifi_sta_ip_addresses(), this->get_dns_address(0), this->get_dns_address(1)); + } #endif } } diff --git a/esphome/components/wifi_info/text_sensor.py b/esphome/components/wifi_info/text_sensor.py index 0feee3d4a9..bc0c038f80 100644 --- a/esphome/components/wifi_info/text_sensor.py +++ b/esphome/components/wifi_info/text_sensor.py @@ -61,7 +61,7 @@ CONFIG_SCHEMA = cv.Schema( } ) -# Keys that require WiFi callbacks +# Keys that require WiFi listeners _NETWORK_INFO_KEYS = { CONF_SSID, CONF_BSSID, @@ -79,9 +79,9 @@ async def setup_conf(config, key): async def to_code(config): - # Request WiFi callbacks for any sensor that needs them + # Request WiFi listeners for any sensor that needs them if _NETWORK_INFO_KEYS.intersection(config): - wifi.request_wifi_callbacks() + wifi.request_wifi_listeners() await setup_conf(config, CONF_SSID) await setup_conf(config, CONF_BSSID) diff --git a/esphome/components/wifi_info/wifi_info_text_sensor.cpp b/esphome/components/wifi_info/wifi_info_text_sensor.cpp index abd590b168..92d3ea29f5 100644 --- a/esphome/components/wifi_info/wifi_info_text_sensor.cpp +++ b/esphome/components/wifi_info/wifi_info_text_sensor.cpp @@ -12,16 +12,12 @@ static constexpr size_t MAX_STATE_LENGTH = 255; * IPAddressWiFiInfo *******************/ -void IPAddressWiFiInfo::setup() { - wifi::global_wifi_component->add_on_ip_state_callback( - [this](const network::IPAddresses &ips, const network::IPAddress &dns1_ip, const network::IPAddress &dns2_ip) { - this->state_callback_(ips); - }); -} +void IPAddressWiFiInfo::setup() { wifi::global_wifi_component->add_ip_state_listener(this); } void IPAddressWiFiInfo::dump_config() { LOG_TEXT_SENSOR("", "IP Address", this); } -void IPAddressWiFiInfo::state_callback_(const network::IPAddresses &ips) { +void IPAddressWiFiInfo::on_ip_state(const network::IPAddresses &ips, const network::IPAddress &dns1, + const network::IPAddress &dns2) { this->publish_state(ips[0].str()); uint8_t sensor = 0; for (const auto &ip : ips) { @@ -38,17 +34,13 @@ void IPAddressWiFiInfo::state_callback_(const network::IPAddresses &ips) { * DNSAddressWifiInfo ********************/ -void DNSAddressWifiInfo::setup() { - wifi::global_wifi_component->add_on_ip_state_callback( - [this](const network::IPAddresses &ips, const network::IPAddress &dns1_ip, const network::IPAddress &dns2_ip) { - this->state_callback_(dns1_ip, dns2_ip); - }); -} +void DNSAddressWifiInfo::setup() { wifi::global_wifi_component->add_ip_state_listener(this); } void DNSAddressWifiInfo::dump_config() { LOG_TEXT_SENSOR("", "DNS Address", this); } -void DNSAddressWifiInfo::state_callback_(const network::IPAddress &dns1_ip, const network::IPAddress &dns2_ip) { - std::string dns_results = dns1_ip.str() + " " + dns2_ip.str(); +void DNSAddressWifiInfo::on_ip_state(const network::IPAddresses &ips, const network::IPAddress &dns1, + const network::IPAddress &dns2) { + std::string dns_results = dns1.str() + " " + dns2.str(); this->publish_state(dns_results); } @@ -56,14 +48,11 @@ void DNSAddressWifiInfo::state_callback_(const network::IPAddress &dns1_ip, cons * ScanResultsWiFiInfo *********************/ -void ScanResultsWiFiInfo::setup() { - wifi::global_wifi_component->add_on_wifi_scan_state_callback( - [this](const wifi::wifi_scan_vector_t &results) { this->state_callback_(results); }); -} +void ScanResultsWiFiInfo::setup() { wifi::global_wifi_component->add_scan_results_listener(this); } void ScanResultsWiFiInfo::dump_config() { LOG_TEXT_SENSOR("", "Scan Results", this); } -void ScanResultsWiFiInfo::state_callback_(const wifi::wifi_scan_vector_t &results) { +void ScanResultsWiFiInfo::on_wifi_scan_results(const wifi::wifi_scan_vector_t &results) { std::string scan_results; for (const auto &scan : results) { if (scan.get_is_hidden()) @@ -85,33 +74,30 @@ void ScanResultsWiFiInfo::state_callback_(const wifi::wifi_scan_vector_tadd_on_wifi_connect_state_callback( - [this](const std::string &ssid, const wifi::bssid_t &bssid) { this->state_callback_(ssid); }); -} +void SSIDWiFiInfo::setup() { wifi::global_wifi_component->add_connect_state_listener(this); } void SSIDWiFiInfo::dump_config() { LOG_TEXT_SENSOR("", "SSID", this); } -void SSIDWiFiInfo::state_callback_(const std::string &ssid) { this->publish_state(ssid); } +void SSIDWiFiInfo::on_wifi_connect_state(const std::string &ssid, const wifi::bssid_t &bssid) { + this->publish_state(ssid); +} /**************** * BSSIDWiFiInfo ***************/ -void BSSIDWiFiInfo::setup() { - wifi::global_wifi_component->add_on_wifi_connect_state_callback( - [this](const std::string &ssid, const wifi::bssid_t &bssid) { this->state_callback_(bssid); }); -} +void BSSIDWiFiInfo::setup() { wifi::global_wifi_component->add_connect_state_listener(this); } void BSSIDWiFiInfo::dump_config() { LOG_TEXT_SENSOR("", "BSSID", this); } -void BSSIDWiFiInfo::state_callback_(const wifi::bssid_t &bssid) { +void BSSIDWiFiInfo::on_wifi_connect_state(const std::string &ssid, const wifi::bssid_t &bssid) { char buf[18] = "unknown"; if (mac_address_is_valid(bssid.data())) { format_mac_addr_upper(bssid.data(), buf); } this->publish_state(buf); } + /********************* * MacAddressWifiInfo ********************/ diff --git a/esphome/components/wifi_info/wifi_info_text_sensor.h b/esphome/components/wifi_info/wifi_info_text_sensor.h index 12666b4059..74d951f922 100644 --- a/esphome/components/wifi_info/wifi_info_text_sensor.h +++ b/esphome/components/wifi_info/wifi_info_text_sensor.h @@ -9,55 +9,61 @@ namespace esphome::wifi_info { -class IPAddressWiFiInfo : public Component, public text_sensor::TextSensor { +class IPAddressWiFiInfo final : public Component, public text_sensor::TextSensor, public wifi::WiFiIPStateListener { public: void setup() override; void dump_config() override; void add_ip_sensors(uint8_t index, text_sensor::TextSensor *s) { this->ip_sensors_[index] = s; } + // WiFiIPStateListener interface + void on_ip_state(const network::IPAddresses &ips, const network::IPAddress &dns1, + const network::IPAddress &dns2) override; + protected: - void state_callback_(const network::IPAddresses &ips); std::array ip_sensors_; }; -class DNSAddressWifiInfo : public Component, public text_sensor::TextSensor { +class DNSAddressWifiInfo final : public Component, public text_sensor::TextSensor, public wifi::WiFiIPStateListener { public: void setup() override; void dump_config() override; - protected: - void state_callback_(const network::IPAddress &dns1_ip, const network::IPAddress &dns2_ip); + // WiFiIPStateListener interface + void on_ip_state(const network::IPAddresses &ips, const network::IPAddress &dns1, + const network::IPAddress &dns2) override; }; -class ScanResultsWiFiInfo : public Component, public text_sensor::TextSensor { +class ScanResultsWiFiInfo final : public Component, + public text_sensor::TextSensor, + public wifi::WiFiScanResultsListener { public: void setup() override; float get_setup_priority() const override { return setup_priority::AFTER_WIFI; } void dump_config() override; - protected: - void state_callback_(const wifi::wifi_scan_vector_t &results); + // WiFiScanResultsListener interface + void on_wifi_scan_results(const wifi::wifi_scan_vector_t &results) override; }; -class SSIDWiFiInfo : public Component, public text_sensor::TextSensor { +class SSIDWiFiInfo final : public Component, public text_sensor::TextSensor, public wifi::WiFiConnectStateListener { public: void setup() override; void dump_config() override; - protected: - void state_callback_(const std::string &ssid); + // WiFiConnectStateListener interface + void on_wifi_connect_state(const std::string &ssid, const wifi::bssid_t &bssid) override; }; -class BSSIDWiFiInfo : public Component, public text_sensor::TextSensor { +class BSSIDWiFiInfo final : public Component, public text_sensor::TextSensor, public wifi::WiFiConnectStateListener { public: void setup() override; void dump_config() override; - protected: - void state_callback_(const wifi::bssid_t &bssid); + // WiFiConnectStateListener interface + void on_wifi_connect_state(const std::string &ssid, const wifi::bssid_t &bssid) override; }; -class MacAddressWifiInfo : public Component, public text_sensor::TextSensor { +class MacAddressWifiInfo final : public Component, public text_sensor::TextSensor { public: void setup() override { char mac_s[18]; diff --git a/esphome/core/defines.h b/esphome/core/defines.h index 1373ea6366..f4026aad96 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -210,7 +210,7 @@ #define USE_WEBSERVER_SORTING #define USE_WIFI_11KV_SUPPORT #define USE_WIFI_FAST_CONNECT -#define USE_WIFI_CALLBACKS +#define USE_WIFI_LISTENERS #define USE_WIFI_RUNTIME_POWER_SAVE #define USB_HOST_MAX_REQUESTS 16 From f1d8281489c8df9b173419ab24f71ed89c69b7c8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 28 Nov 2025 12:05:41 -0600 Subject: [PATCH 360/415] [esp32_camera] Replace std::function callbacks with CameraListener interface --- esphome/components/api/api_server.cpp | 16 ++++--- esphome/components/api/api_server.h | 10 +++++ esphome/components/camera/camera.h | 25 ++++++++--- .../components/esp32_camera/esp32_camera.cpp | 21 ++++----- .../components/esp32_camera/esp32_camera.h | 45 ++++++++----------- .../camera_web_server.cpp | 14 +++--- .../camera_web_server.h | 5 ++- 7 files changed, 80 insertions(+), 56 deletions(-) diff --git a/esphome/components/api/api_server.cpp b/esphome/components/api/api_server.cpp index de0c4b24c9..4168761c74 100644 --- a/esphome/components/api/api_server.cpp +++ b/esphome/components/api/api_server.cpp @@ -107,12 +107,7 @@ void APIServer::setup() { #ifdef USE_CAMERA if (camera::Camera::instance() != nullptr && !camera::Camera::instance()->is_internal()) { - camera::Camera::instance()->add_image_callback([this](const std::shared_ptr &image) { - for (auto &c : this->clients_) { - if (!c->flags_.remove) - c->set_camera_state(image); - } - }); + camera::Camera::instance()->add_listener(this); } #endif } @@ -544,6 +539,15 @@ void APIServer::on_log(uint8_t level, const char *tag, const char *message, size } #endif +#ifdef USE_CAMERA +void APIServer::on_camera_image(const std::shared_ptr &image) { + for (auto &c : this->clients_) { + if (!c->flags_.remove) + c->set_camera_state(image); + } +} +#endif + void APIServer::on_shutdown() { this->shutting_down_ = true; diff --git a/esphome/components/api/api_server.h b/esphome/components/api/api_server.h index 57aea6ad0e..3089bb1d35 100644 --- a/esphome/components/api/api_server.h +++ b/esphome/components/api/api_server.h @@ -18,6 +18,9 @@ #ifdef USE_LOGGER #include "esphome/components/logger/logger.h" #endif +#ifdef USE_CAMERA +#include "esphome/components/camera/camera.h" +#endif #include #include @@ -36,6 +39,10 @@ class APIServer : public Component, , public logger::LogListener #endif +#ifdef USE_CAMERA + , + public camera::CameraListener +#endif { public: APIServer(); @@ -49,6 +56,9 @@ class APIServer : public Component, #ifdef USE_LOGGER void on_log(uint8_t level, const char *tag, const char *message, size_t message_len) override; #endif +#ifdef USE_CAMERA + void on_camera_image(const std::shared_ptr &image) override; +#endif #ifdef USE_API_PASSWORD bool check_password(const uint8_t *password_data, size_t password_len) const; void set_password(const std::string &password); diff --git a/esphome/components/camera/camera.h b/esphome/components/camera/camera.h index c28a756a06..a4f33de00b 100644 --- a/esphome/components/camera/camera.h +++ b/esphome/components/camera/camera.h @@ -35,6 +35,21 @@ inline const char *to_string(PixelFormat format) { return "PIXEL_FORMAT_UNKNOWN"; } +// Forward declaration +class CameraImage; + +/** Listener interface for camera events. + * + * Components can implement this interface to receive camera notifications + * (new images, stream start/stop) without the overhead of std::function callbacks. + */ +class CameraListener { + public: + virtual void on_camera_image(const std::shared_ptr &image) = 0; + virtual void on_stream_start() {} + virtual void on_stream_stop() {} +}; + /** Abstract camera image base class. * Encapsulates the JPEG encoded data and it is shared among * all connected clients. @@ -87,12 +102,12 @@ struct CameraImageSpec { }; /** Abstract camera base class. Collaborates with API. - * 1) API server starts and installs callback (add_image_callback) - * which is called by the camera when a new image is available. + * 1) API server starts and registers as a listener (add_image_listener) + * to receive new images from the camera. * 2) New API client connects and creates a new image reader (create_image_reader). * 3) API connection receives protobuf CameraImageRequest and calls request_image. * 3.a) API connection receives protobuf CameraImageRequest and calls start_stream. - * 4) Camera implementation provides JPEG data in the CameraImage and calls callback. + * 4) Camera implementation provides JPEG data in the CameraImage and notifies listeners. * 5) API connection sets the image in the image reader. * 6) API connection consumes data from the image reader and returns the image when finished. * 7.a) Camera captures a new image and continues with 4) until start_stream is called. @@ -100,8 +115,8 @@ struct CameraImageSpec { class Camera : public EntityBase, public Component { public: Camera(); - // Camera implementation invokes callback to publish a new image. - virtual void add_image_callback(std::function)> &&callback) = 0; + /// Add a listener to receive camera events + virtual void add_listener(CameraListener *listener) = 0; /// Returns a new camera image reader that keeps track of the JPEG data in the camera image. virtual CameraImageReader *create_image_reader() = 0; // Connection, camera or web server requests one new JPEG image. diff --git a/esphome/components/esp32_camera/esp32_camera.cpp b/esphome/components/esp32_camera/esp32_camera.cpp index 38bd8d5822..5080a6f32d 100644 --- a/esphome/components/esp32_camera/esp32_camera.cpp +++ b/esphome/components/esp32_camera/esp32_camera.cpp @@ -205,7 +205,9 @@ void ESP32Camera::loop() { this->current_image_ = std::make_shared(fb, this->single_requesters_ | this->stream_requesters_); ESP_LOGD(TAG, "Got Image: len=%u", fb->len); - this->new_image_callback_.call(this->current_image_); + for (auto *listener : this->listeners_) { + listener->on_camera_image(this->current_image_); + } this->last_update_ = now; this->single_requesters_ = 0; } @@ -357,21 +359,16 @@ void ESP32Camera::set_frame_buffer_location(camera_fb_location_t fb_location) { } /* ---------------- public API (specific) ---------------- */ -void ESP32Camera::add_image_callback(std::function)> &&callback) { - this->new_image_callback_.add(std::move(callback)); -} -void ESP32Camera::add_stream_start_callback(std::function &&callback) { - this->stream_start_callback_.add(std::move(callback)); -} -void ESP32Camera::add_stream_stop_callback(std::function &&callback) { - this->stream_stop_callback_.add(std::move(callback)); -} void ESP32Camera::start_stream(camera::CameraRequester requester) { - this->stream_start_callback_.call(); + for (auto *listener : this->listeners_) { + listener->on_stream_start(); + } this->stream_requesters_ |= (1U << requester); } void ESP32Camera::stop_stream(camera::CameraRequester requester) { - this->stream_stop_callback_.call(); + for (auto *listener : this->listeners_) { + listener->on_stream_stop(); + } this->stream_requesters_ &= ~(1U << requester); } void ESP32Camera::request_image(camera::CameraRequester requester) { this->single_requesters_ |= (1U << requester); } diff --git a/esphome/components/esp32_camera/esp32_camera.h b/esphome/components/esp32_camera/esp32_camera.h index 0e7f7c0ea6..96b11db65c 100644 --- a/esphome/components/esp32_camera/esp32_camera.h +++ b/esphome/components/esp32_camera/esp32_camera.h @@ -165,9 +165,8 @@ class ESP32Camera : public camera::Camera { void request_image(camera::CameraRequester requester) override; void update_camera_parameters(); - void add_image_callback(std::function)> &&callback) override; - void add_stream_start_callback(std::function &&callback); - void add_stream_stop_callback(std::function &&callback); + /// Add a listener to receive camera events + void add_listener(camera::CameraListener *listener) override { this->listeners_.push_back(listener); } camera::CameraImageReader *create_image_reader() override; protected: @@ -210,9 +209,7 @@ class ESP32Camera : public camera::Camera { uint8_t stream_requesters_{0}; QueueHandle_t framebuffer_get_queue_; QueueHandle_t framebuffer_return_queue_; - CallbackManager)> new_image_callback_{}; - CallbackManager stream_start_callback_{}; - CallbackManager stream_stop_callback_{}; + std::vector listeners_; uint32_t last_idle_request_{0}; uint32_t last_update_{0}; @@ -221,33 +218,29 @@ class ESP32Camera : public camera::Camera { #endif // USE_I2C }; -class ESP32CameraImageTrigger : public Trigger { +class ESP32CameraImageTrigger : public Trigger, public camera::CameraListener { public: - explicit ESP32CameraImageTrigger(ESP32Camera *parent) { - parent->add_image_callback([this](const std::shared_ptr &image) { - CameraImageData camera_image_data{}; - camera_image_data.length = image->get_data_length(); - camera_image_data.data = image->get_data_buffer(); - this->trigger(camera_image_data); - }); + explicit ESP32CameraImageTrigger(ESP32Camera *parent) { parent->add_listener(this); } + void on_camera_image(const std::shared_ptr &image) override { + CameraImageData camera_image_data{}; + camera_image_data.length = image->get_data_length(); + camera_image_data.data = image->get_data_buffer(); + this->trigger(camera_image_data); } }; -class ESP32CameraStreamStartTrigger : public Trigger<> { +class ESP32CameraStreamStartTrigger : public Trigger<>, public camera::CameraListener { public: - explicit ESP32CameraStreamStartTrigger(ESP32Camera *parent) { - parent->add_stream_start_callback([this]() { this->trigger(); }); - } - - protected: + explicit ESP32CameraStreamStartTrigger(ESP32Camera *parent) { parent->add_listener(this); } + void on_camera_image(const std::shared_ptr &image) override {} + void on_stream_start() override { this->trigger(); } }; -class ESP32CameraStreamStopTrigger : public Trigger<> { - public: - explicit ESP32CameraStreamStopTrigger(ESP32Camera *parent) { - parent->add_stream_stop_callback([this]() { this->trigger(); }); - } - protected: +class ESP32CameraStreamStopTrigger : public Trigger<>, public camera::CameraListener { + public: + explicit ESP32CameraStreamStopTrigger(ESP32Camera *parent) { parent->add_listener(this); } + void on_camera_image(const std::shared_ptr &image) override {} + void on_stream_stop() override { this->trigger(); } }; } // namespace esp32_camera diff --git a/esphome/components/esp32_camera_web_server/camera_web_server.cpp b/esphome/components/esp32_camera_web_server/camera_web_server.cpp index 1b81989296..f49578c425 100644 --- a/esphome/components/esp32_camera_web_server/camera_web_server.cpp +++ b/esphome/components/esp32_camera_web_server/camera_web_server.cpp @@ -67,12 +67,14 @@ void CameraWebServer::setup() { httpd_register_uri_handler(this->httpd_, &uri); - camera::Camera::instance()->add_image_callback([this](std::shared_ptr image) { - if (this->running_ && image->was_requested_by(camera::WEB_REQUESTER)) { - this->image_ = std::move(image); - xSemaphoreGive(this->semaphore_); - } - }); + camera::Camera::instance()->add_listener(this); +} + +void CameraWebServer::on_camera_image(const std::shared_ptr &image) { + if (this->running_ && image->was_requested_by(camera::WEB_REQUESTER)) { + this->image_ = image; + xSemaphoreGive(this->semaphore_); + } } void CameraWebServer::on_shutdown() { diff --git a/esphome/components/esp32_camera_web_server/camera_web_server.h b/esphome/components/esp32_camera_web_server/camera_web_server.h index e70246745c..ad7b29fb11 100644 --- a/esphome/components/esp32_camera_web_server/camera_web_server.h +++ b/esphome/components/esp32_camera_web_server/camera_web_server.h @@ -18,7 +18,7 @@ namespace esp32_camera_web_server { enum Mode { STREAM, SNAPSHOT }; -class CameraWebServer : public Component { +class CameraWebServer : public Component, public camera::CameraListener { public: CameraWebServer(); ~CameraWebServer(); @@ -31,6 +31,9 @@ class CameraWebServer : public Component { void set_mode(Mode mode) { this->mode_ = mode; } void loop() override; + /// CameraListener interface + void on_camera_image(const std::shared_ptr &image) override; + protected: std::shared_ptr wait_for_image_(); esp_err_t handler_(struct httpd_req *req); From fb82362e9cbd97e09b5eb1cee5e4d5840e038ff0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 28 Nov 2025 12:13:29 -0600 Subject: [PATCH 361/415] [api] Eliminate rx_buf heap churn and release buffers after initial sync (#12133) --- esphome/components/api/api_connection.cpp | 6 ++++-- esphome/components/api/api_connection.h | 15 +++++++++---- esphome/components/api/api_frame_helper.h | 21 ++++++++++++++++--- .../components/api/api_frame_helper_noise.cpp | 3 +-- .../api/api_frame_helper_plaintext.cpp | 3 +-- esphome/components/api/api_pb2_service.cpp | 4 ++-- esphome/components/api/api_pb2_service.h | 4 ++-- esphome/components/api/proto.h | 2 +- script/api_protobuf/api_protobuf.py | 8 +++---- 9 files changed, 44 insertions(+), 22 deletions(-) diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 12cbbb991d..9ad45dc6b7 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -169,8 +169,7 @@ void APIConnection::loop() { } else { this->last_traffic_ = now; // read a packet - this->read_message(buffer.data_len, buffer.type, - buffer.data_len > 0 ? &buffer.container[buffer.data_offset] : nullptr); + this->read_message(buffer.data_len, buffer.type, buffer.data); if (this->flags_.remove) return; } @@ -195,6 +194,9 @@ void APIConnection::loop() { } // Now that everything is sent, enable immediate sending for future state changes this->flags_.should_try_send_immediately = true; + // Release excess memory from buffers that grew during initial sync + this->deferred_batch_.release_buffer(); + this->helper_->release_buffers(); } } diff --git a/esphome/components/api/api_connection.h b/esphome/components/api/api_connection.h index af3a19909f..05af0ccde7 100644 --- a/esphome/components/api/api_connection.h +++ b/esphome/components/api/api_connection.h @@ -554,10 +554,8 @@ class APIConnection final : public APIServerConnection { std::vector items; uint32_t batch_start_time{0}; - DeferredBatch() { - // Pre-allocate capacity for typical batch sizes to avoid reallocation - items.reserve(8); - } + // No pre-allocation - log connections never use batching, and for + // connections that do, buffers are released after initial sync anyway // Add item to the batch void add_item(EntityBase *entity, MessageCreator creator, uint8_t message_type, uint8_t estimated_size); @@ -576,6 +574,15 @@ class APIConnection final : public APIServerConnection { bool empty() const { return items.empty(); } size_t size() const { return items.size(); } const BatchItem &operator[](size_t index) const { return items[index]; } + // Release excess capacity - only releases if items already empty + void release_buffer() { + // Safe to call: batch is processed before release_buffer is called, + // and if any items remain (partial processing), we must not clear them. + // Use swap trick since shrink_to_fit() is non-binding and may be ignored. + if (items.empty()) { + std::vector().swap(items); + } + } }; // DeferredBatch here (16 bytes, 4-byte aligned) diff --git a/esphome/components/api/api_frame_helper.h b/esphome/components/api/api_frame_helper.h index d931a6e3a9..b582bcea9a 100644 --- a/esphome/components/api/api_frame_helper.h +++ b/esphome/components/api/api_frame_helper.h @@ -35,10 +35,9 @@ struct ClientInfo; class ProtoWriteBuffer; struct ReadPacketBuffer { - std::vector container; - uint16_t type; - uint16_t data_offset; + const uint8_t *data; // Points directly into frame helper's rx_buf_ (valid until next read_packet call) uint16_t data_len; + uint16_t type; }; // Packed packet info structure to minimize memory usage @@ -119,6 +118,22 @@ class APIFrameHelper { uint8_t frame_footer_size() const { return frame_footer_size_; } // Check if socket has data ready to read bool is_socket_ready() const { return socket_ != nullptr && socket_->ready(); } + // Release excess memory from internal buffers after initial sync + void release_buffers() { + // rx_buf_: Safe to clear only if no partial read in progress. + // rx_buf_len_ tracks bytes read so far; if non-zero, we're mid-frame + // and clearing would lose partially received data. + if (this->rx_buf_len_ == 0) { + // Use swap trick since shrink_to_fit() is non-binding and may be ignored + std::vector().swap(this->rx_buf_); + } + // reusable_iovs_: Safe to release unconditionally. + // Only used within write_protobuf_packets() calls - cleared at start, + // populated with pointers, used for writev(), then function returns. + // The iovecs contain stale pointers after the call (data was either sent + // or copied to tx_buf_), and are cleared on next write_protobuf_packets(). + std::vector().swap(this->reusable_iovs_); + } protected: // Buffer containing data to be sent diff --git a/esphome/components/api/api_frame_helper_noise.cpp b/esphome/components/api/api_frame_helper_noise.cpp index f1028fa299..ae69f0b673 100644 --- a/esphome/components/api/api_frame_helper_noise.cpp +++ b/esphome/components/api/api_frame_helper_noise.cpp @@ -407,8 +407,7 @@ APIError APINoiseFrameHelper::read_packet(ReadPacketBuffer *buffer) { return APIError::BAD_DATA_PACKET; } - buffer->container = std::move(this->rx_buf_); - buffer->data_offset = 4; + buffer->data = msg_data + 4; // Skip 4-byte header (type + length) buffer->data_len = data_len; buffer->type = type; return APIError::OK; diff --git a/esphome/components/api/api_frame_helper_plaintext.cpp b/esphome/components/api/api_frame_helper_plaintext.cpp index dcbd35aa32..b5d90b2429 100644 --- a/esphome/components/api/api_frame_helper_plaintext.cpp +++ b/esphome/components/api/api_frame_helper_plaintext.cpp @@ -210,8 +210,7 @@ APIError APIPlaintextFrameHelper::read_packet(ReadPacketBuffer *buffer) { return aerr; } - buffer->container = std::move(this->rx_buf_); - buffer->data_offset = 0; + buffer->data = this->rx_buf_.data(); buffer->data_len = this->rx_header_parsed_len_; buffer->type = this->rx_header_parsed_type_; return APIError::OK; diff --git a/esphome/components/api/api_pb2_service.cpp b/esphome/components/api/api_pb2_service.cpp index 3d28a137c8..45f6ecd30e 100644 --- a/esphome/components/api/api_pb2_service.cpp +++ b/esphome/components/api/api_pb2_service.cpp @@ -13,7 +13,7 @@ void APIServerConnectionBase::log_send_message_(const char *name, const std::str } #endif -void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) { +void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, const uint8_t *msg_data) { switch (msg_type) { case HelloRequest::MESSAGE_TYPE: { HelloRequest msg; @@ -827,7 +827,7 @@ void APIServerConnection::on_z_wave_proxy_frame(const ZWaveProxyFrame &msg) { th void APIServerConnection::on_z_wave_proxy_request(const ZWaveProxyRequest &msg) { this->zwave_proxy_request(msg); } #endif -void APIServerConnection::read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) { +void APIServerConnection::read_message(uint32_t msg_size, uint32_t msg_type, const uint8_t *msg_data) { // Check authentication/connection requirements for messages switch (msg_type) { case HelloRequest::MESSAGE_TYPE: // No setup required diff --git a/esphome/components/api/api_pb2_service.h b/esphome/components/api/api_pb2_service.h index 827b89e23c..6d94046a23 100644 --- a/esphome/components/api/api_pb2_service.h +++ b/esphome/components/api/api_pb2_service.h @@ -218,7 +218,7 @@ class APIServerConnectionBase : public ProtoService { virtual void on_z_wave_proxy_request(const ZWaveProxyRequest &value){}; #endif protected: - void read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) override; + void read_message(uint32_t msg_size, uint32_t msg_type, const uint8_t *msg_data) override; }; class APIServerConnection : public APIServerConnectionBase { @@ -480,7 +480,7 @@ class APIServerConnection : public APIServerConnectionBase { #ifdef USE_ZWAVE_PROXY void on_z_wave_proxy_request(const ZWaveProxyRequest &msg) override; #endif - void read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) override; + void read_message(uint32_t msg_size, uint32_t msg_type, const uint8_t *msg_data) override; }; } // namespace esphome::api diff --git a/esphome/components/api/proto.h b/esphome/components/api/proto.h index e7585924a5..83b6922be1 100644 --- a/esphome/components/api/proto.h +++ b/esphome/components/api/proto.h @@ -846,7 +846,7 @@ class ProtoService { */ virtual ProtoWriteBuffer create_buffer(uint32_t reserve_size) = 0; virtual bool send_buffer(ProtoWriteBuffer buffer, uint8_t message_type) = 0; - virtual void read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) = 0; + virtual void read_message(uint32_t msg_size, uint32_t msg_type, const uint8_t *msg_data) = 0; // Optimized method that pre-allocates buffer based on message size bool send_message_(const ProtoMessage &msg, uint8_t message_type) { diff --git a/script/api_protobuf/api_protobuf.py b/script/api_protobuf/api_protobuf.py index b07a249c8d..3412fac5db 100755 --- a/script/api_protobuf/api_protobuf.py +++ b/script/api_protobuf/api_protobuf.py @@ -2769,8 +2769,8 @@ static const char *const TAG = "api.service"; cases = list(RECEIVE_CASES.items()) cases.sort() hpp += " protected:\n" - hpp += " void read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) override;\n" - out = f"void {class_name}::read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) {{\n" + hpp += " void read_message(uint32_t msg_size, uint32_t msg_type, const uint8_t *msg_data) override;\n" + out = f"void {class_name}::read_message(uint32_t msg_size, uint32_t msg_type, const uint8_t *msg_data) {{\n" out += " switch (msg_type) {\n" for i, (case, ifdef, message_name) in cases: if ifdef is not None: @@ -2878,9 +2878,9 @@ static const char *const TAG = "api.service"; result += "#endif\n" return result - hpp_protected += " void read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) override;\n" + hpp_protected += " void read_message(uint32_t msg_size, uint32_t msg_type, const uint8_t *msg_data) override;\n" - cpp += f"\nvoid {class_name}::read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) {{\n" + cpp += f"\nvoid {class_name}::read_message(uint32_t msg_size, uint32_t msg_type, const uint8_t *msg_data) {{\n" cpp += " // Check authentication/connection requirements for messages\n" cpp += " switch (msg_type) {\n" From d43189cb076b74ce0085c084adf414ffef726df8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 28 Nov 2025 12:14:19 -0600 Subject: [PATCH 362/415] Update esphome/components/camera/camera.h Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- esphome/components/camera/camera.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/camera/camera.h b/esphome/components/camera/camera.h index a4f33de00b..9f46eb0c43 100644 --- a/esphome/components/camera/camera.h +++ b/esphome/components/camera/camera.h @@ -102,7 +102,7 @@ struct CameraImageSpec { }; /** Abstract camera base class. Collaborates with API. - * 1) API server starts and registers as a listener (add_image_listener) + * 1) API server starts and registers as a listener (add_listener) * to receive new images from the camera. * 2) New API client connects and creates a new image reader (create_image_reader). * 3) API connection receives protobuf CameraImageRequest and calls request_image. From e15f3a08aef0255b111db331c726128e4b68db8d Mon Sep 17 00:00:00 2001 From: Javier Peletier Date: Fri, 28 Nov 2025 19:15:55 +0100 Subject: [PATCH 363/415] [tests] Remote packages with substitutions (#12145) --- .../06-remote_packages.approved.yaml | 25 +++++++++++++ .../06-remote_packages.input.yaml | 37 +++++++++++++++++++ .../substitutions/remote_package_proxy.yaml | 6 +++ .../fixtures/substitutions/remotes/README.md | 3 ++ .../remotes/repo1/main/file1.yaml | 9 +++++ .../remotes/repo2/main/file2.yaml | 10 +++++ tests/unit_tests/test_substitutions.py | 33 ++++++++++++++++- 7 files changed, 122 insertions(+), 1 deletion(-) create mode 100644 tests/unit_tests/fixtures/substitutions/06-remote_packages.approved.yaml create mode 100644 tests/unit_tests/fixtures/substitutions/06-remote_packages.input.yaml create mode 100644 tests/unit_tests/fixtures/substitutions/remote_package_proxy.yaml create mode 100644 tests/unit_tests/fixtures/substitutions/remotes/README.md create mode 100644 tests/unit_tests/fixtures/substitutions/remotes/repo1/main/file1.yaml create mode 100644 tests/unit_tests/fixtures/substitutions/remotes/repo2/main/file2.yaml diff --git a/tests/unit_tests/fixtures/substitutions/06-remote_packages.approved.yaml b/tests/unit_tests/fixtures/substitutions/06-remote_packages.approved.yaml new file mode 100644 index 0000000000..4b5315013c --- /dev/null +++ b/tests/unit_tests/fixtures/substitutions/06-remote_packages.approved.yaml @@ -0,0 +1,25 @@ +substitutions: + x: 10 + y: 20 + z: 30 +values_from_repo1_main: + - package_name: package1 + x: 3 + y: 4 + z: 5 + volume: 60 + - package_name: package2 + x: 6 + y: 7 + z: 8 + volume: 336 + - package_name: default + x: 10 + y: 20 + z: 5 + volume: 1000 + - package_name: package4_from_repo2 + x: 9 + y: 10 + z: 11 + volume: 990 diff --git a/tests/unit_tests/fixtures/substitutions/06-remote_packages.input.yaml b/tests/unit_tests/fixtures/substitutions/06-remote_packages.input.yaml new file mode 100644 index 0000000000..a8128a7a07 --- /dev/null +++ b/tests/unit_tests/fixtures/substitutions/06-remote_packages.input.yaml @@ -0,0 +1,37 @@ +substitutions: + x: 10 + y: 20 + z: 30 +packages: + package1: + url: https://github.com/esphome/repo1 + files: + - path: file1.yaml + vars: + package_name: package1 + x: 3 + y: 4 + ref: main + package2: !include # a package that just includes the given remote package + file: remote_package_proxy.yaml + vars: + url: https://github.com/esphome/repo1 + ref: main + files: + - path: file1.yaml + vars: + package_name: package2 + x: 6 + y: 7 + z: 8 + package3: github://esphome/repo1/file1.yaml@main # a package that uses the shorthand syntax + package4: # include repo2, which itself includes repo1 + url: https://github.com/esphome/repo2 + files: + - path: file2.yaml + vars: + package_name: package4 + a: 9 + b: 10 + c: 11 + ref: main diff --git a/tests/unit_tests/fixtures/substitutions/remote_package_proxy.yaml b/tests/unit_tests/fixtures/substitutions/remote_package_proxy.yaml new file mode 100644 index 0000000000..05da30acb4 --- /dev/null +++ b/tests/unit_tests/fixtures/substitutions/remote_package_proxy.yaml @@ -0,0 +1,6 @@ +# acts as a proxy to be able to include a remote package +# in which the url/ref/files come from a substitution +packages: + - url: ${url} + ref: ${ref} + files: ${files} diff --git a/tests/unit_tests/fixtures/substitutions/remotes/README.md b/tests/unit_tests/fixtures/substitutions/remotes/README.md new file mode 100644 index 0000000000..09d9f38699 --- /dev/null +++ b/tests/unit_tests/fixtures/substitutions/remotes/README.md @@ -0,0 +1,3 @@ +This folder contains fake repos for remote packages testing +These are used by `test_substitutions.py`. +To add repos, create a folder and add its path to the `REMOTES` constant in `test_substitutions.py`. diff --git a/tests/unit_tests/fixtures/substitutions/remotes/repo1/main/file1.yaml b/tests/unit_tests/fixtures/substitutions/remotes/repo1/main/file1.yaml new file mode 100644 index 0000000000..3830b1650f --- /dev/null +++ b/tests/unit_tests/fixtures/substitutions/remotes/repo1/main/file1.yaml @@ -0,0 +1,9 @@ +defaults: + z: 5 + package_name: default +values_from_repo1_main: + - package_name: ${package_name} + x: ${x} + y: ${y} + z: ${z} + volume: ${x*y*z} diff --git a/tests/unit_tests/fixtures/substitutions/remotes/repo2/main/file2.yaml b/tests/unit_tests/fixtures/substitutions/remotes/repo2/main/file2.yaml new file mode 100644 index 0000000000..7f62ab8926 --- /dev/null +++ b/tests/unit_tests/fixtures/substitutions/remotes/repo2/main/file2.yaml @@ -0,0 +1,10 @@ +packages: + - url: https://github.com/esphome/repo1 + ref: main + files: + - path: file1.yaml + vars: + package_name: ${package_name}_from_repo2 + x: ${a} + y: ${b} + z: ${c} diff --git a/tests/unit_tests/test_substitutions.py b/tests/unit_tests/test_substitutions.py index 7d50b44506..c5e6618ea6 100644 --- a/tests/unit_tests/test_substitutions.py +++ b/tests/unit_tests/test_substitutions.py @@ -2,6 +2,7 @@ import glob import logging from pathlib import Path from typing import Any +from unittest.mock import patch from esphome import config as config_module, yaml_util from esphome.components import substitutions @@ -84,11 +85,41 @@ def verify_database(value: Any, path: str = "") -> str | None: return None -def test_substitutions_fixtures(fixture_path): +# Mapping of (url, ref) to local test repository path under fixtures/substitutions +REMOTES = { + ("https://github.com/esphome/repo1", "main"): "remotes/repo1/main", + ("https://github.com/esphome/repo2", "main"): "remotes/repo2/main", +} + + +@patch("esphome.git.clone_or_update") +def test_substitutions_fixtures(mock_clone_or_update, fixture_path): base_dir = fixture_path / "substitutions" sources = sorted(glob.glob(str(base_dir / "*.input.yaml"))) assert sources, f"No input YAML files found in {base_dir}" + def fake_clone_or_update( + *, + url: str, + ref: str | None = None, + refresh=None, + domain: str, + username: str | None = None, + password: str | None = None, + submodules: list[str] | None = None, + _recover_broken: bool = True, + ) -> tuple[Path, None]: + path = REMOTES.get((url, ref)) + if path is None: + path = REMOTES.get((url.rstrip(".git"), ref)) + if path is None: + raise RuntimeError( + f"Cannot find test repository for {url} @ {ref}. Check the REMOTES mapping in test_substitutions.py" + ) + return base_dir / path, None + + mock_clone_or_update.side_effect = fake_clone_or_update + failures = [] for source_path in sources: source_path = Path(source_path) From d6ca01775e5c19a48c58f7147c3dfc8e6cc00489 Mon Sep 17 00:00:00 2001 From: Javier Peletier Date: Fri, 28 Nov 2025 19:24:09 +0100 Subject: [PATCH 364/415] [packages] Restore remote shorthand vars and !remove in early package contents validation (#12158) Co-authored-by: J. Nick Koston --- esphome/components/packages/__init__.py | 12 +++++++++--- .../substitutions/06-remote_packages.approved.yaml | 5 +++++ .../substitutions/06-remote_packages.input.yaml | 6 ++++++ .../substitutions/remote_package_shorthand.yaml | 4 ++++ 4 files changed, 24 insertions(+), 3 deletions(-) create mode 100644 tests/unit_tests/fixtures/substitutions/remote_package_shorthand.yaml diff --git a/esphome/components/packages/__init__.py b/esphome/components/packages/__init__.py index 41cde0391b..67fd2770e9 100644 --- a/esphome/components/packages/__init__.py +++ b/esphome/components/packages/__init__.py @@ -2,7 +2,8 @@ import logging from pathlib import Path from esphome import git, yaml_util -from esphome.config_helpers import merge_config +from esphome.components.substitutions.jinja import has_jinja +from esphome.config_helpers import Remove, merge_config import esphome.config_validation as cv from esphome.const import ( CONF_ESPHOME, @@ -39,10 +40,15 @@ def valid_package_contents(package_config: dict): for k, v in package_config.items(): if not isinstance(k, str): raise cv.Invalid("Package content keys must be strings") - if isinstance(v, (dict, list)): - continue # e.g. script: [] or logger: {level: debug} + if isinstance(v, (dict, list, Remove)): + continue # e.g. script: [], psram: !remove, logger: {level: debug} if v is None: continue # e.g. web_server: + if isinstance(v, str) and has_jinja(v): + # e.g: remote package shorthand: + # package_name: github://esphome/repo/file.yaml@${ branch } + continue + raise cv.Invalid("Invalid component content in package definition") return package_config diff --git a/tests/unit_tests/fixtures/substitutions/06-remote_packages.approved.yaml b/tests/unit_tests/fixtures/substitutions/06-remote_packages.approved.yaml index 4b5315013c..0fffbfb7cb 100644 --- a/tests/unit_tests/fixtures/substitutions/06-remote_packages.approved.yaml +++ b/tests/unit_tests/fixtures/substitutions/06-remote_packages.approved.yaml @@ -23,3 +23,8 @@ values_from_repo1_main: y: 10 z: 11 volume: 990 + - package_name: default + x: 10 + y: 20 + z: 5 + volume: 1000 diff --git a/tests/unit_tests/fixtures/substitutions/06-remote_packages.input.yaml b/tests/unit_tests/fixtures/substitutions/06-remote_packages.input.yaml index a8128a7a07..772860bf19 100644 --- a/tests/unit_tests/fixtures/substitutions/06-remote_packages.input.yaml +++ b/tests/unit_tests/fixtures/substitutions/06-remote_packages.input.yaml @@ -35,3 +35,9 @@ packages: b: 10 c: 11 ref: main + package5: !include + file: remote_package_shorthand.yaml + vars: + repo: repo1 + file: file1.yaml + ref: main diff --git a/tests/unit_tests/fixtures/substitutions/remote_package_shorthand.yaml b/tests/unit_tests/fixtures/substitutions/remote_package_shorthand.yaml new file mode 100644 index 0000000000..f49e85e038 --- /dev/null +++ b/tests/unit_tests/fixtures/substitutions/remote_package_shorthand.yaml @@ -0,0 +1,4 @@ +# acts as a proxy to be able to include a remote package +# in which the shorthand comes from a substitution +packages: + - github://esphome/${repo}/${file}@${ref} From 149f5e59ec8beabda89f7238fa2e23bb43af02ff Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 28 Nov 2025 12:36:40 -0600 Subject: [PATCH 365/415] [light] Use listener pattern for state callbacks with lazy allocation --- esphome/components/light/automation.h | 62 +++++++++++++----------- esphome/components/light/light_call.cpp | 6 ++- esphome/components/light/light_state.cpp | 26 +++++++--- esphome/components/light/light_state.h | 58 +++++++++++++++------- esphome/components/mqtt/mqtt_light.cpp | 7 ++- esphome/components/mqtt/mqtt_light.h | 5 +- 6 files changed, 109 insertions(+), 55 deletions(-) diff --git a/esphome/components/light/automation.h b/esphome/components/light/automation.h index 9893c15e0c..c90d71c5df 100644 --- a/esphome/components/light/automation.h +++ b/esphome/components/light/automation.h @@ -120,46 +120,54 @@ template class LightIsOffCondition : public Condition { LightState *state_; }; -class LightTurnOnTrigger : public Trigger<> { +class LightTurnOnTrigger : public Trigger<>, public LightRemoteValuesListener { public: - LightTurnOnTrigger(LightState *a_light) { - a_light->add_new_remote_values_callback([this, a_light]() { - // using the remote value because of transitions we need to trigger as early as possible - auto is_on = a_light->remote_values.is_on(); - // only trigger when going from off to on - auto should_trigger = is_on && !this->last_on_; - // Set new state immediately so that trigger() doesn't devolve - // into infinite loop - this->last_on_ = is_on; - if (should_trigger) { - this->trigger(); - } - }); + explicit LightTurnOnTrigger(LightState *a_light) : light_(a_light) { + a_light->add_remote_values_listener(this); this->last_on_ = a_light->current_values.is_on(); } + void on_light_remote_values_update() override { + // using the remote value because of transitions we need to trigger as early as possible + auto is_on = this->light_->remote_values.is_on(); + // only trigger when going from off to on + auto should_trigger = is_on && !this->last_on_; + // Set new state immediately so that trigger() doesn't devolve + // into infinite loop + this->last_on_ = is_on; + if (should_trigger) { + this->trigger(); + } + } + protected: + LightState *light_; bool last_on_; }; -class LightTurnOffTrigger : public Trigger<> { +class LightTurnOffTrigger : public Trigger<>, public LightTargetStateReachedListener { public: - LightTurnOffTrigger(LightState *a_light) { - a_light->add_new_target_state_reached_callback([this, a_light]() { - auto is_on = a_light->current_values.is_on(); - // only trigger when going from on to off - if (!is_on) { - this->trigger(); - } - }); + explicit LightTurnOffTrigger(LightState *a_light) : light_(a_light) { + a_light->add_target_state_reached_listener(this); } + + void on_light_target_state_reached() override { + auto is_on = this->light_->current_values.is_on(); + // only trigger when going from on to off + if (!is_on) { + this->trigger(); + } + } + + protected: + LightState *light_; }; -class LightStateTrigger : public Trigger<> { +class LightStateTrigger : public Trigger<>, public LightRemoteValuesListener { public: - LightStateTrigger(LightState *a_light) { - a_light->add_new_remote_values_callback([this]() { this->trigger(); }); - } + explicit LightStateTrigger(LightState *a_light) { a_light->add_remote_values_listener(this); } + + void on_light_remote_values_update() override { this->trigger(); } }; // This is slightly ugly, but we can't log in headers, and can't make this a static method on AddressableSet diff --git a/esphome/components/light/light_call.cpp b/esphome/components/light/light_call.cpp index b3bdb16c73..f1d4d459e9 100644 --- a/esphome/components/light/light_call.cpp +++ b/esphome/components/light/light_call.cpp @@ -174,8 +174,10 @@ void LightCall::perform() { this->parent_->set_immediately_(v, publish); } - if (!this->has_transition_()) { - this->parent_->target_state_reached_callback_.call(); + if (!this->has_transition_() && this->parent_->target_state_reached_listeners_) { + for (auto *listener : *this->parent_->target_state_reached_listeners_) { + listener->on_light_target_state_reached(); + } } if (publish) { this->parent_->publish_state(); diff --git a/esphome/components/light/light_state.cpp b/esphome/components/light/light_state.cpp index 9cde9077da..af619a426a 100644 --- a/esphome/components/light/light_state.cpp +++ b/esphome/components/light/light_state.cpp @@ -127,7 +127,11 @@ void LightState::loop() { this->transformer_->stop(); this->is_transformer_active_ = false; this->transformer_ = nullptr; - this->target_state_reached_callback_.call(); + if (this->target_state_reached_listeners_) { + for (auto *listener : *this->target_state_reached_listeners_) { + listener->on_light_target_state_reached(); + } + } // Disable loop if idle (no transformer and no effect) this->disable_loop_if_idle_(); @@ -146,7 +150,11 @@ void LightState::loop() { float LightState::get_setup_priority() const { return setup_priority::HARDWARE - 1.0f; } void LightState::publish_state() { - this->remote_values_callback_.call(); + if (this->remote_values_listeners_) { + for (auto *listener : *this->remote_values_listeners_) { + listener->on_light_remote_values_update(); + } + } #if defined(USE_LIGHT) && defined(USE_CONTROLLER_REGISTRY) ControllerRegistry::notify_light_update(this); #endif @@ -171,11 +179,17 @@ StringRef LightState::get_effect_name_ref() { return EFFECT_NONE_REF; } -void LightState::add_new_remote_values_callback(std::function &&send_callback) { - this->remote_values_callback_.add(std::move(send_callback)); +void LightState::add_remote_values_listener(LightRemoteValuesListener *listener) { + if (!this->remote_values_listeners_) { + this->remote_values_listeners_ = make_unique>(); + } + this->remote_values_listeners_->push_back(listener); } -void LightState::add_new_target_state_reached_callback(std::function &&send_callback) { - this->target_state_reached_callback_.add(std::move(send_callback)); +void LightState::add_target_state_reached_listener(LightTargetStateReachedListener *listener) { + if (!this->target_state_reached_listeners_) { + this->target_state_reached_listeners_ = make_unique>(); + } + this->target_state_reached_listeners_->push_back(listener); } void LightState::set_default_transition_length(uint32_t default_transition_length) { diff --git a/esphome/components/light/light_state.h b/esphome/components/light/light_state.h index ad8922b46f..7ea72306f9 100644 --- a/esphome/components/light/light_state.h +++ b/esphome/components/light/light_state.h @@ -18,6 +18,29 @@ namespace esphome::light { class LightOutput; +class LightState; + +/** Listener interface for light remote value changes. + * + * Components can implement this interface to receive notifications + * when the light's remote values change (state, brightness, color, etc.) + * without the overhead of std::function callbacks. + */ +class LightRemoteValuesListener { + public: + virtual void on_light_remote_values_update() = 0; +}; + +/** Listener interface for light target state reached. + * + * Components can implement this interface to receive notifications + * when the light finishes a transition and reaches its target state + * without the overhead of std::function callbacks. + */ +class LightTargetStateReachedListener { + public: + virtual void on_light_target_state_reached() = 0; +}; enum LightRestoreMode : uint8_t { LIGHT_RESTORE_DEFAULT_OFF, @@ -121,21 +144,17 @@ class LightState : public EntityBase, public Component { /// Return the name of the current effect as StringRef (for API usage) StringRef get_effect_name_ref(); - /** - * This lets front-end components subscribe to light change events. This callback is called once - * when the remote color values are changed. - * - * @param send_callback The callback. + /** Add a listener for remote values changes. + * Listener is notified when the light's remote values change (state, brightness, color, etc.) + * Lazily allocates the listener vector on first registration. */ - void add_new_remote_values_callback(std::function &&send_callback); + void add_remote_values_listener(LightRemoteValuesListener *listener); - /** - * The callback is called once the state of current_values and remote_values are equal (when the - * transition is finished). - * - * @param send_callback + /** Add a listener for target state reached. + * Listener is notified when the light finishes a transition and reaches its target state. + * Lazily allocates the listener vector on first registration. */ - void add_new_target_state_reached_callback(std::function &&send_callback); + void add_target_state_reached_listener(LightTargetStateReachedListener *listener); /// Set the default transition length, i.e. the transition length when no transition is provided. void set_default_transition_length(uint32_t default_transition_length); @@ -279,19 +298,24 @@ class LightState : public EntityBase, public Component { // for effects, true if a transformer (transition) is active. bool is_transformer_active_ = false; - /** Callback to call when new values for the frontend are available. + /** Listeners for remote values changes. * * "Remote values" are light color values that are reported to the frontend and have a lower * publish frequency than the "real" color values. For example, during transitions the current * color value may change continuously, but the remote values will be reported as the target values * starting with the beginning of the transition. + * + * Lazily allocated - only created when a listener is actually registered. */ - CallbackManager remote_values_callback_{}; + std::unique_ptr> remote_values_listeners_; - /** Callback to call when the state of current_values and remote_values are equal - * This should be called once the state of current_values changed and equals the state of remote_values + /** Listeners for target state reached. + * Notified when the state of current_values and remote_values are equal + * (when the transition is finished). + * + * Lazily allocated - only created when a listener is actually registered. */ - CallbackManager target_state_reached_callback_{}; + std::unique_ptr> target_state_reached_listeners_; /// Initial state of the light. optional initial_state_{}; diff --git a/esphome/components/mqtt/mqtt_light.cpp b/esphome/components/mqtt/mqtt_light.cpp index 883b67ffc6..fe911bfba2 100644 --- a/esphome/components/mqtt/mqtt_light.cpp +++ b/esphome/components/mqtt/mqtt_light.cpp @@ -25,8 +25,11 @@ void MQTTJSONLightComponent::setup() { call.perform(); }); - auto f = std::bind(&MQTTJSONLightComponent::publish_state_, this); - this->state_->add_new_remote_values_callback([this, f]() { this->defer("send", f); }); + this->state_->add_remote_values_listener(this); +} + +void MQTTJSONLightComponent::on_light_remote_values_update() { + this->defer("send", [this]() { this->publish_state_(); }); } MQTTJSONLightComponent::MQTTJSONLightComponent(LightState *state) : state_(state) {} diff --git a/esphome/components/mqtt/mqtt_light.h b/esphome/components/mqtt/mqtt_light.h index 3d1e770d4d..a105f3d7b8 100644 --- a/esphome/components/mqtt/mqtt_light.h +++ b/esphome/components/mqtt/mqtt_light.h @@ -11,7 +11,7 @@ namespace esphome { namespace mqtt { -class MQTTJSONLightComponent : public mqtt::MQTTComponent { +class MQTTJSONLightComponent : public mqtt::MQTTComponent, public light::LightRemoteValuesListener { public: explicit MQTTJSONLightComponent(light::LightState *state); @@ -25,6 +25,9 @@ class MQTTJSONLightComponent : public mqtt::MQTTComponent { bool send_initial_state() override; + // LightRemoteValuesListener interface + void on_light_remote_values_update() override; + protected: std::string component_type() const override; const EntityBase *get_entity() const override; From 087ed48dba17b5ef1874bc440c8ab46402a6b8cd Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 28 Nov 2025 12:56:13 -0600 Subject: [PATCH 366/415] cleanups per review --- esphome/components/sensor/__init__.py | 15 +++++++-------- esphome/components/sensor/filter.cpp | 4 ++-- esphome/components/sensor/filter.h | 4 ++-- 3 files changed, 11 insertions(+), 12 deletions(-) diff --git a/esphome/components/sensor/__init__.py b/esphome/components/sensor/__init__.py index f7d7513b7b..ca6cd49090 100644 --- a/esphome/components/sensor/__init__.py +++ b/esphome/components/sensor/__init__.py @@ -270,7 +270,7 @@ ThrottleFilter = sensor_ns.class_("ThrottleFilter", Filter) ThrottleWithPriorityFilter = sensor_ns.class_( "ThrottleWithPriorityFilter", ValueListFilter ) -TimeoutFilter = sensor_ns.class_("TimeoutFilter", Filter, cg.Component) +TimeoutFilterLast = sensor_ns.class_("TimeoutFilterLast", Filter, cg.Component) TimeoutFilterConfigured = sensor_ns.class_( "TimeoutFilterConfigured", Filter, cg.Component ) @@ -684,18 +684,17 @@ TIMEOUT_SCHEMA = cv.maybe_simple_value( ) -@FILTER_REGISTRY.register("timeout", TimeoutFilter, TIMEOUT_SCHEMA) +@FILTER_REGISTRY.register("timeout", Filter, TIMEOUT_SCHEMA) async def timeout_filter_to_code(config, filter_id): if config[CONF_VALUE] == "last": - # Use TimeoutFilter for "last" mode (smaller, more common - LD2450, LD2412, etc.) - var = cg.new_Pvariable(filter_id, config[CONF_TIMEOUT]) + # Use TimeoutFilterLast for "last" mode (smaller, more common - LD2450, LD2412, etc.) + rhs = TimeoutFilterLast.new(config[CONF_TIMEOUT]) + var = cg.Pvariable(filter_id, rhs, TimeoutFilterLast) else: # Use TimeoutFilterConfigured for configured value mode - # Change the type to TimeoutFilterConfigured (similar to stateless lambda pattern) - filter_id = filter_id.copy() - filter_id.type = TimeoutFilterConfigured template_ = await cg.templatable(config[CONF_VALUE], [], float) - var = cg.new_Pvariable(filter_id, config[CONF_TIMEOUT], template_) + rhs = TimeoutFilterConfigured.new(config[CONF_TIMEOUT], template_) + var = cg.Pvariable(filter_id, rhs, TimeoutFilterConfigured) await cg.register_component(var, {}) return var diff --git a/esphome/components/sensor/filter.cpp b/esphome/components/sensor/filter.cpp index 6825dfebd0..c8c6540112 100644 --- a/esphome/components/sensor/filter.cpp +++ b/esphome/components/sensor/filter.cpp @@ -355,8 +355,8 @@ void TimeoutFilterBase::loop() { float TimeoutFilterBase::get_setup_priority() const { return setup_priority::HARDWARE; } -// TimeoutFilter - "last" mode implementation -optional TimeoutFilter::new_value(float value) { +// TimeoutFilterLast - "last" mode implementation +optional TimeoutFilterLast::new_value(float value) { // Store the value to output when timeout fires this->pending_value_ = value; diff --git a/esphome/components/sensor/filter.h b/esphome/components/sensor/filter.h index 102ae2fea6..92a9184c18 100644 --- a/esphome/components/sensor/filter.h +++ b/esphome/components/sensor/filter.h @@ -396,9 +396,9 @@ class TimeoutFilterBase : public Filter, public Component { }; // Timeout filter for "last" mode - outputs the last received value after timeout -class TimeoutFilter : public TimeoutFilterBase { +class TimeoutFilterLast : public TimeoutFilterBase { public: - explicit TimeoutFilter(uint32_t time_period) : TimeoutFilterBase(time_period) {} + explicit TimeoutFilterLast(uint32_t time_period) : TimeoutFilterBase(time_period) {} optional new_value(float value) override; From cc40f0857411d8764acb9ac74cb095d76c701538 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 28 Nov 2025 13:02:37 -0600 Subject: [PATCH 367/415] use TimeoutFilterBase --- esphome/components/sensor/__init__.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/esphome/components/sensor/__init__.py b/esphome/components/sensor/__init__.py index ca6cd49090..f83226d10f 100644 --- a/esphome/components/sensor/__init__.py +++ b/esphome/components/sensor/__init__.py @@ -270,10 +270,9 @@ ThrottleFilter = sensor_ns.class_("ThrottleFilter", Filter) ThrottleWithPriorityFilter = sensor_ns.class_( "ThrottleWithPriorityFilter", ValueListFilter ) -TimeoutFilterLast = sensor_ns.class_("TimeoutFilterLast", Filter, cg.Component) -TimeoutFilterConfigured = sensor_ns.class_( - "TimeoutFilterConfigured", Filter, cg.Component -) +TimeoutFilterBase = sensor_ns.class_("TimeoutFilterBase", Filter, cg.Component) +TimeoutFilterLast = sensor_ns.class_("TimeoutFilterLast", TimeoutFilterBase) +TimeoutFilterConfigured = sensor_ns.class_("TimeoutFilterConfigured", TimeoutFilterBase) DebounceFilter = sensor_ns.class_("DebounceFilter", Filter, cg.Component) HeartbeatFilter = sensor_ns.class_("HeartbeatFilter", Filter, cg.Component) DeltaFilter = sensor_ns.class_("DeltaFilter", Filter) @@ -684,17 +683,18 @@ TIMEOUT_SCHEMA = cv.maybe_simple_value( ) -@FILTER_REGISTRY.register("timeout", Filter, TIMEOUT_SCHEMA) +@FILTER_REGISTRY.register("timeout", TimeoutFilterBase, TIMEOUT_SCHEMA) async def timeout_filter_to_code(config, filter_id): + filter_id = filter_id.copy() if config[CONF_VALUE] == "last": # Use TimeoutFilterLast for "last" mode (smaller, more common - LD2450, LD2412, etc.) - rhs = TimeoutFilterLast.new(config[CONF_TIMEOUT]) - var = cg.Pvariable(filter_id, rhs, TimeoutFilterLast) + filter_id.type = TimeoutFilterLast + var = cg.new_Pvariable(filter_id, config[CONF_TIMEOUT]) else: # Use TimeoutFilterConfigured for configured value mode + filter_id.type = TimeoutFilterConfigured template_ = await cg.templatable(config[CONF_VALUE], [], float) - rhs = TimeoutFilterConfigured.new(config[CONF_TIMEOUT], template_) - var = cg.Pvariable(filter_id, rhs, TimeoutFilterConfigured) + var = cg.new_Pvariable(filter_id, config[CONF_TIMEOUT], template_) await cg.register_component(var, {}) return var From 8fe981b9f1c9265daa09d0565bbc9daa7b13269f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 28 Nov 2025 13:46:00 -0600 Subject: [PATCH 368/415] [ota] Replace std::function callbacks with listener interface --- .../components/esp32_ble_tracker/__init__.py | 4 +- .../components/esphome/ota/ota_esphome.cpp | 18 +-- .../http_request/ota/ota_http_request.cpp | 18 +-- .../http_request/update/__init__.py | 4 +- .../update/http_request_update.cpp | 30 +++-- .../http_request/update/http_request_update.h | 11 +- .../components/micro_wake_word/__init__.py | 4 +- esphome/components/ota/__init__.py | 22 +++- esphome/components/ota/automation.h | 88 +++++++------- esphome/components/ota/ota_backend.cpp | 6 +- esphome/components/ota/ota_backend.h | 108 ++++++++++++------ .../speaker/media_player/__init__.py | 4 +- .../web_server/ota/ota_web_server.cpp | 38 +++--- esphome/core/defines.h | 2 +- 14 files changed, 213 insertions(+), 144 deletions(-) diff --git a/esphome/components/esp32_ble_tracker/__init__.py b/esphome/components/esp32_ble_tracker/__init__.py index 4e25434aad..37e74672ed 100644 --- a/esphome/components/esp32_ble_tracker/__init__.py +++ b/esphome/components/esp32_ble_tracker/__init__.py @@ -5,7 +5,7 @@ import logging from esphome import automation import esphome.codegen as cg -from esphome.components import esp32_ble +from esphome.components import esp32_ble, ota from esphome.components.esp32 import add_idf_sdkconfig_option from esphome.components.esp32_ble import ( IDF_MAX_CONNECTIONS, @@ -328,7 +328,7 @@ async def to_code(config): # Note: CONFIG_BT_ACL_CONNECTIONS and CONFIG_BTDM_CTRL_BLE_MAX_CONN are now # configured in esp32_ble component based on max_connections setting - cg.add_define("USE_OTA_STATE_CALLBACK") # To be notified when an OTA update starts + ota.request_ota_state_listeners() # To be notified when an OTA update starts cg.add_define("USE_ESP32_BLE_CLIENT") CORE.add_job(_add_ble_features) diff --git a/esphome/components/esphome/ota/ota_esphome.cpp b/esphome/components/esphome/ota/ota_esphome.cpp index eb6c61a69b..469c57211c 100644 --- a/esphome/components/esphome/ota/ota_esphome.cpp +++ b/esphome/components/esphome/ota/ota_esphome.cpp @@ -42,7 +42,7 @@ static constexpr size_t SHA256_HEX_SIZE = 64; // SHA256 hash as hex string (32 #endif // USE_OTA_PASSWORD void ESPHomeOTAComponent::setup() { -#ifdef USE_OTA_STATE_CALLBACK +#ifdef USE_OTA_STATE_LISTENER ota::register_ota_platform(this); #endif @@ -298,8 +298,8 @@ void ESPHomeOTAComponent::handle_data_() { // accidentally trigger the update process. this->log_start_(LOG_STR("update")); this->status_set_warning(); -#ifdef USE_OTA_STATE_CALLBACK - this->state_callback_.call(ota::OTA_STARTED, 0.0f, 0); +#ifdef USE_OTA_STATE_LISTENER + this->notify_state_(ota::OTA_STARTED, 0.0f, 0); #endif // This will block for a few seconds as it locks flash @@ -358,8 +358,8 @@ void ESPHomeOTAComponent::handle_data_() { last_progress = now; float percentage = (total * 100.0f) / ota_size; ESP_LOGD(TAG, "Progress: %0.1f%%", percentage); -#ifdef USE_OTA_STATE_CALLBACK - this->state_callback_.call(ota::OTA_IN_PROGRESS, percentage, 0); +#ifdef USE_OTA_STATE_LISTENER + this->notify_state_(ota::OTA_IN_PROGRESS, percentage, 0); #endif // feed watchdog and give other tasks a chance to run this->yield_and_feed_watchdog_(); @@ -388,8 +388,8 @@ void ESPHomeOTAComponent::handle_data_() { delay(10); ESP_LOGI(TAG, "Update complete"); this->status_clear_warning(); -#ifdef USE_OTA_STATE_CALLBACK - this->state_callback_.call(ota::OTA_COMPLETED, 100.0f, 0); +#ifdef USE_OTA_STATE_LISTENER + this->notify_state_(ota::OTA_COMPLETED, 100.0f, 0); #endif delay(100); // NOLINT App.safe_reboot(); @@ -403,8 +403,8 @@ error: } this->status_momentary_error("onerror", 5000); -#ifdef USE_OTA_STATE_CALLBACK - this->state_callback_.call(ota::OTA_ERROR, 0.0f, static_cast(error_code)); +#ifdef USE_OTA_STATE_LISTENER + this->notify_state_(ota::OTA_ERROR, 0.0f, static_cast(error_code)); #endif } diff --git a/esphome/components/http_request/ota/ota_http_request.cpp b/esphome/components/http_request/ota/ota_http_request.cpp index 4d9e868c74..2a52a0e264 100644 --- a/esphome/components/http_request/ota/ota_http_request.cpp +++ b/esphome/components/http_request/ota/ota_http_request.cpp @@ -18,7 +18,7 @@ namespace http_request { static const char *const TAG = "http_request.ota"; void OtaHttpRequestComponent::setup() { -#ifdef USE_OTA_STATE_CALLBACK +#ifdef USE_OTA_STATE_LISTENER ota::register_ota_platform(this); #endif } @@ -49,24 +49,24 @@ void OtaHttpRequestComponent::flash() { } ESP_LOGI(TAG, "Starting update"); -#ifdef USE_OTA_STATE_CALLBACK - this->state_callback_.call(ota::OTA_STARTED, 0.0f, 0); +#ifdef USE_OTA_STATE_LISTENER + this->notify_state_(ota::OTA_STARTED, 0.0f, 0); #endif auto ota_status = this->do_ota_(); switch (ota_status) { case ota::OTA_RESPONSE_OK: -#ifdef USE_OTA_STATE_CALLBACK - this->state_callback_.call(ota::OTA_COMPLETED, 100.0f, ota_status); +#ifdef USE_OTA_STATE_LISTENER + this->notify_state_(ota::OTA_COMPLETED, 100.0f, ota_status); #endif delay(10); App.safe_reboot(); break; default: -#ifdef USE_OTA_STATE_CALLBACK - this->state_callback_.call(ota::OTA_ERROR, 0.0f, ota_status); +#ifdef USE_OTA_STATE_LISTENER + this->notify_state_(ota::OTA_ERROR, 0.0f, ota_status); #endif this->md5_computed_.clear(); // will be reset at next attempt this->md5_expected_.clear(); // will be reset at next attempt @@ -159,8 +159,8 @@ uint8_t OtaHttpRequestComponent::do_ota_() { last_progress = now; float percentage = container->get_bytes_read() * 100.0f / container->content_length; ESP_LOGD(TAG, "Progress: %0.1f%%", percentage); -#ifdef USE_OTA_STATE_CALLBACK - this->state_callback_.call(ota::OTA_IN_PROGRESS, percentage, 0); +#ifdef USE_OTA_STATE_LISTENER + this->notify_state_(ota::OTA_IN_PROGRESS, percentage, 0); #endif } } // while diff --git a/esphome/components/http_request/update/__init__.py b/esphome/components/http_request/update/__init__.py index abb4b2a430..d84d80109a 100644 --- a/esphome/components/http_request/update/__init__.py +++ b/esphome/components/http_request/update/__init__.py @@ -1,5 +1,5 @@ import esphome.codegen as cg -from esphome.components import update +from esphome.components import ota, update import esphome.config_validation as cv from esphome.const import CONF_SOURCE @@ -38,6 +38,6 @@ async def to_code(config): cg.add(var.set_source_url(config[CONF_SOURCE])) - cg.add_define("USE_OTA_STATE_CALLBACK") + ota.request_ota_state_listeners() await cg.register_component(var, config) diff --git a/esphome/components/http_request/update/http_request_update.cpp b/esphome/components/http_request/update/http_request_update.cpp index c91b0eba73..ca4afa67d4 100644 --- a/esphome/components/http_request/update/http_request_update.cpp +++ b/esphome/components/http_request/update/http_request_update.cpp @@ -21,20 +21,26 @@ static const char *const TAG = "http_request.update"; static const size_t MAX_READ_SIZE = 256; void HttpRequestUpdate::setup() { - this->ota_parent_->add_on_state_callback([this](ota::OTAState state, float progress, uint8_t err) { - if (state == ota::OTAState::OTA_IN_PROGRESS) { - this->state_ = update::UPDATE_STATE_INSTALLING; - this->update_info_.has_progress = true; - this->update_info_.progress = progress; - this->publish_state(); - } else if (state == ota::OTAState::OTA_ABORT || state == ota::OTAState::OTA_ERROR) { - this->state_ = update::UPDATE_STATE_AVAILABLE; - this->status_set_error(LOG_STR("Failed to install firmware")); - this->publish_state(); - } - }); +#ifdef USE_OTA_STATE_LISTENER + this->ota_parent_->add_state_listener(this); +#endif } +#ifdef USE_OTA_STATE_LISTENER +void HttpRequestUpdate::on_ota_state(ota::OTAState state, float progress, uint8_t error) { + if (state == ota::OTAState::OTA_IN_PROGRESS) { + this->state_ = update::UPDATE_STATE_INSTALLING; + this->update_info_.has_progress = true; + this->update_info_.progress = progress; + this->publish_state(); + } else if (state == ota::OTAState::OTA_ABORT || state == ota::OTAState::OTA_ERROR) { + this->state_ = update::UPDATE_STATE_AVAILABLE; + this->status_set_error(LOG_STR("Failed to install firmware")); + this->publish_state(); + } +} +#endif + void HttpRequestUpdate::update() { #ifdef USE_ESP32 xTaskCreate(HttpRequestUpdate::update_task, "update_task", 8192, (void *) this, 1, &this->update_task_handle_); diff --git a/esphome/components/http_request/update/http_request_update.h b/esphome/components/http_request/update/http_request_update.h index e05fdb0cc2..937f6dbeb9 100644 --- a/esphome/components/http_request/update/http_request_update.h +++ b/esphome/components/http_request/update/http_request_update.h @@ -1,6 +1,7 @@ #pragma once #include "esphome/core/component.h" +#include "esphome/core/defines.h" #include "esphome/core/helpers.h" #include "esphome/components/http_request/http_request.h" @@ -14,7 +15,11 @@ namespace esphome { namespace http_request { -class HttpRequestUpdate : public update::UpdateEntity, public PollingComponent { +#ifdef USE_OTA_STATE_LISTENER +class HttpRequestUpdate final : public update::UpdateEntity, public PollingComponent, public ota::OTAStateListener { +#else +class HttpRequestUpdate final : public update::UpdateEntity, public PollingComponent { +#endif public: void setup() override; void update() override; @@ -29,6 +34,10 @@ class HttpRequestUpdate : public update::UpdateEntity, public PollingComponent { float get_setup_priority() const override { return setup_priority::AFTER_WIFI; } +#ifdef USE_OTA_STATE_LISTENER + void on_ota_state(ota::OTAState state, float progress, uint8_t error) override; +#endif + protected: HttpRequestComponent *request_parent_; OtaHttpRequestComponent *ota_parent_; diff --git a/esphome/components/micro_wake_word/__init__.py b/esphome/components/micro_wake_word/__init__.py index 575fb97799..0d478f749b 100644 --- a/esphome/components/micro_wake_word/__init__.py +++ b/esphome/components/micro_wake_word/__init__.py @@ -7,7 +7,7 @@ from urllib.parse import urljoin from esphome import automation, external_files, git from esphome.automation import register_action, register_condition import esphome.codegen as cg -from esphome.components import esp32, microphone, socket +from esphome.components import esp32, microphone, ota, socket import esphome.config_validation as cv from esphome.const import ( CONF_FILE, @@ -452,7 +452,7 @@ async def to_code(config): cg.add(var.set_microphone_source(mic_source)) cg.add_define("USE_MICRO_WAKE_WORD") - cg.add_define("USE_OTA_STATE_CALLBACK") + ota.request_ota_state_listeners() esp32.add_idf_component(name="espressif/esp-tflite-micro", ref="1.3.3~1") diff --git a/esphome/components/ota/__init__.py b/esphome/components/ota/__init__.py index eec39668db..387a307ab7 100644 --- a/esphome/components/ota/__init__.py +++ b/esphome/components/ota/__init__.py @@ -13,6 +13,8 @@ from esphome.const import ( from esphome.core import CORE, coroutine_with_priority from esphome.coroutine import CoroPriority +OTA_STATE_LISTENER_KEY = "ota_state_listener" + CODEOWNERS = ["@esphome/core"] AUTO_LOAD = ["md5", "safe_mode"] @@ -86,6 +88,7 @@ BASE_OTA_SCHEMA = cv.Schema( @coroutine_with_priority(CoroPriority.OTA_UPDATES) async def to_code(config): cg.add_define("USE_OTA") + CORE.add_job(final_step) if CORE.is_esp32 and CORE.using_arduino: cg.add_library("Update", None) @@ -122,7 +125,24 @@ async def ota_to_code(var, config): await automation.build_automation(trigger, [(cg.uint8, "x")], conf) use_state_callback = True if use_state_callback: - cg.add_define("USE_OTA_STATE_CALLBACK") + request_ota_state_listeners() + + +def request_ota_state_listeners() -> None: + """Request that OTA state listeners be compiled in. + + Components that need to be notified about OTA state changes (start, progress, + complete, error) should call this function during their code generation. + This enables the add_state_listener() API on OTAComponent. + """ + CORE.data[OTA_STATE_LISTENER_KEY] = True + + +@coroutine_with_priority(CoroPriority.FINAL) +async def final_step(): + """Final code generation step to configure optional OTA features.""" + if CORE.data.get(OTA_STATE_LISTENER_KEY, False): + cg.add_define("USE_OTA_STATE_LISTENER") FILTER_SOURCE_FILES = filter_source_files_from_platform( diff --git a/esphome/components/ota/automation.h b/esphome/components/ota/automation.h index 7e1a60f3ce..520cb293f0 100644 --- a/esphome/components/ota/automation.h +++ b/esphome/components/ota/automation.h @@ -1,5 +1,5 @@ #pragma once -#ifdef USE_OTA_STATE_CALLBACK +#ifdef USE_OTA_STATE_LISTENER #include "ota_backend.h" #include "esphome/core/automation.h" @@ -7,69 +7,65 @@ namespace esphome { namespace ota { -class OTAStateChangeTrigger : public Trigger { +class OTAStateChangeTrigger final : public Trigger, public OTAStateListener { public: - explicit OTAStateChangeTrigger(OTAComponent *parent) { - parent->add_on_state_callback([this, parent](OTAState state, float progress, uint8_t error) { - if (!parent->is_failed()) { - trigger(state); - } - }); + explicit OTAStateChangeTrigger(OTAComponent *parent) { parent->add_state_listener(this); } + + void on_ota_state(OTAState state, float progress, uint8_t error) override { this->trigger(state); } +}; + +class OTAStartTrigger final : public Trigger<>, public OTAStateListener { + public: + explicit OTAStartTrigger(OTAComponent *parent) { parent->add_state_listener(this); } + + void on_ota_state(OTAState state, float progress, uint8_t error) override { + if (state == OTA_STARTED) { + this->trigger(); + } } }; -class OTAStartTrigger : public Trigger<> { +class OTAProgressTrigger final : public Trigger, public OTAStateListener { public: - explicit OTAStartTrigger(OTAComponent *parent) { - parent->add_on_state_callback([this, parent](OTAState state, float progress, uint8_t error) { - if (state == OTA_STARTED && !parent->is_failed()) { - trigger(); - } - }); + explicit OTAProgressTrigger(OTAComponent *parent) { parent->add_state_listener(this); } + + void on_ota_state(OTAState state, float progress, uint8_t error) override { + if (state == OTA_IN_PROGRESS) { + this->trigger(progress); + } } }; -class OTAProgressTrigger : public Trigger { +class OTAEndTrigger final : public Trigger<>, public OTAStateListener { public: - explicit OTAProgressTrigger(OTAComponent *parent) { - parent->add_on_state_callback([this, parent](OTAState state, float progress, uint8_t error) { - if (state == OTA_IN_PROGRESS && !parent->is_failed()) { - trigger(progress); - } - }); + explicit OTAEndTrigger(OTAComponent *parent) { parent->add_state_listener(this); } + + void on_ota_state(OTAState state, float progress, uint8_t error) override { + if (state == OTA_COMPLETED) { + this->trigger(); + } } }; -class OTAEndTrigger : public Trigger<> { +class OTAAbortTrigger final : public Trigger<>, public OTAStateListener { public: - explicit OTAEndTrigger(OTAComponent *parent) { - parent->add_on_state_callback([this, parent](OTAState state, float progress, uint8_t error) { - if (state == OTA_COMPLETED && !parent->is_failed()) { - trigger(); - } - }); + explicit OTAAbortTrigger(OTAComponent *parent) { parent->add_state_listener(this); } + + void on_ota_state(OTAState state, float progress, uint8_t error) override { + if (state == OTA_ABORT) { + this->trigger(); + } } }; -class OTAAbortTrigger : public Trigger<> { +class OTAErrorTrigger final : public Trigger, public OTAStateListener { public: - explicit OTAAbortTrigger(OTAComponent *parent) { - parent->add_on_state_callback([this, parent](OTAState state, float progress, uint8_t error) { - if (state == OTA_ABORT && !parent->is_failed()) { - trigger(); - } - }); - } -}; + explicit OTAErrorTrigger(OTAComponent *parent) { parent->add_state_listener(this); } -class OTAErrorTrigger : public Trigger { - public: - explicit OTAErrorTrigger(OTAComponent *parent) { - parent->add_on_state_callback([this, parent](OTAState state, float progress, uint8_t error) { - if (state == OTA_ERROR && !parent->is_failed()) { - trigger(error); - } - }); + void on_ota_state(OTAState state, float progress, uint8_t error) override { + if (state == OTA_ERROR) { + this->trigger(error); + } } }; diff --git a/esphome/components/ota/ota_backend.cpp b/esphome/components/ota/ota_backend.cpp index 30de4ec4b3..5f510b4f8b 100644 --- a/esphome/components/ota/ota_backend.cpp +++ b/esphome/components/ota/ota_backend.cpp @@ -3,7 +3,7 @@ namespace esphome { namespace ota { -#ifdef USE_OTA_STATE_CALLBACK +#ifdef USE_OTA_STATE_LISTENER OTAGlobalCallback *global_ota_callback{nullptr}; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) OTAGlobalCallback *get_global_ota_callback() { @@ -14,6 +14,10 @@ OTAGlobalCallback *get_global_ota_callback() { } void register_ota_platform(OTAComponent *ota_caller) { get_global_ota_callback()->register_ota(ota_caller); } + +void OTAComponentBridge::on_ota_state(OTAState state, float progress, uint8_t error) { + this->global_callback_->notify_global_listeners(state, progress, error, this->component_); +} #endif } // namespace ota diff --git a/esphome/components/ota/ota_backend.h b/esphome/components/ota/ota_backend.h index 64ee0b9f7c..e474316bb5 100644 --- a/esphome/components/ota/ota_backend.h +++ b/esphome/components/ota/ota_backend.h @@ -4,9 +4,7 @@ #include "esphome/core/defines.h" #include "esphome/core/helpers.h" -#ifdef USE_OTA_STATE_CALLBACK -#include "esphome/core/automation.h" -#endif +#include namespace esphome { namespace ota { @@ -60,62 +58,98 @@ class OTABackend { virtual bool supports_compression() = 0; }; -class OTAComponent : public Component { -#ifdef USE_OTA_STATE_CALLBACK +/** Listener interface for OTA state changes. + * + * Components can implement this interface to receive OTA state updates + * without the overhead of std::function callbacks. + */ +class OTAStateListener { public: - void add_on_state_callback(std::function &&callback) { - this->state_callback_.add(std::move(callback)); - } + virtual void on_ota_state(OTAState state, float progress, uint8_t error) = 0; +}; + +class OTAComponent : public Component { +#ifdef USE_OTA_STATE_LISTENER + public: + void add_state_listener(OTAStateListener *listener) { this->state_listeners_.push_back(listener); } protected: - /** Extended callback manager with deferred call support. - * - * This adds a call_deferred() method for thread-safe execution from other tasks. - */ - class StateCallbackManager : public CallbackManager { - public: - StateCallbackManager(OTAComponent *component) : component_(component) {} - - /** Call callbacks with deferral to main loop (for thread safety). - * - * This should be used by OTA implementations that run in separate tasks - * (like web_server OTA) to ensure callbacks execute in the main loop. - */ - void call_deferred(ota::OTAState state, float progress, uint8_t error) { - component_->defer([this, state, progress, error]() { this->call(state, progress, error); }); + void notify_state_(OTAState state, float progress, uint8_t error) { + for (auto *listener : this->state_listeners_) { + listener->on_ota_state(state, progress, error); } + } - private: - OTAComponent *component_; - }; + /** Notify state with deferral to main loop (for thread safety). + * + * This should be used by OTA implementations that run in separate tasks + * (like web_server OTA) to ensure listeners execute in the main loop. + */ + void notify_state_deferred_(OTAState state, float progress, uint8_t error) { + this->defer([this, state, progress, error]() { this->notify_state_(state, progress, error); }); + } - StateCallbackManager state_callback_{this}; + std::vector state_listeners_; #endif }; -#ifdef USE_OTA_STATE_CALLBACK +#ifdef USE_OTA_STATE_LISTENER +class OTAGlobalCallback; + +/** Listener interface for global OTA state changes (includes OTA component pointer). + * + * Used by OTAGlobalCallback to aggregate state from multiple OTA components. + */ +class OTAGlobalStateListener { + public: + virtual void on_ota_global_state(OTAState state, float progress, uint8_t error, OTAComponent *component) = 0; +}; + +/** Helper class to bridge per-component OTA state to global listeners. + * + * Each OTA component gets one of these registered as a listener. When that + * component fires state events, this bridge forwards them to all global listeners + * along with the component pointer. + */ +class OTAComponentBridge : public OTAStateListener { + public: + OTAComponentBridge(OTAGlobalCallback *global_callback, OTAComponent *component) + : global_callback_(global_callback), component_(component) {} + + void on_ota_state(OTAState state, float progress, uint8_t error) override; + + private: + OTAGlobalCallback *global_callback_; + OTAComponent *component_; +}; + class OTAGlobalCallback { public: void register_ota(OTAComponent *ota_caller) { - ota_caller->add_on_state_callback([this, ota_caller](OTAState state, float progress, uint8_t error) { - this->state_callback_.call(state, progress, error, ota_caller); - }); + // Create a bridge that forwards this component's events to global listeners + auto *bridge = new OTAComponentBridge(this, ota_caller); // NOLINT(cppcoreguidelines-owning-memory) + ota_caller->add_state_listener(bridge); } - void add_on_state_callback(std::function &&callback) { - this->state_callback_.add(std::move(callback)); + + void add_global_state_listener(OTAGlobalStateListener *listener) { this->global_listeners_.push_back(listener); } + + void notify_global_listeners(OTAState state, float progress, uint8_t error, OTAComponent *component) { + for (auto *listener : this->global_listeners_) { + listener->on_ota_global_state(state, progress, error, component); + } } protected: - CallbackManager state_callback_{}; + std::vector global_listeners_; }; OTAGlobalCallback *get_global_ota_callback(); void register_ota_platform(OTAComponent *ota_caller); // OTA implementations should use: -// - state_callback_.call() when already in main loop (e.g., esphome OTA) -// - state_callback_.call_deferred() when in separate task (e.g., web_server OTA) -// This ensures proper callback execution in all contexts. +// - notify_state_() when already in main loop (e.g., esphome OTA) +// - notify_state_deferred_() when in separate task (e.g., web_server OTA) +// This ensures proper listener execution in all contexts. #endif std::unique_ptr make_ota_backend(); diff --git a/esphome/components/speaker/media_player/__init__.py b/esphome/components/speaker/media_player/__init__.py index 062bff92f8..4ca57f2c4a 100644 --- a/esphome/components/speaker/media_player/__init__.py +++ b/esphome/components/speaker/media_player/__init__.py @@ -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, network, psram, speaker +from esphome.components import audio, esp32, media_player, network, ota, psram, speaker import esphome.config_validation as cv from esphome.const import ( CONF_BUFFER_SIZE, @@ -342,7 +342,7 @@ async def to_code(config): var = await media_player.new_media_player(config) await cg.register_component(var, config) - cg.add_define("USE_OTA_STATE_CALLBACK") + ota.request_ota_state_listeners() cg.add(var.set_buffer_size(config[CONF_BUFFER_SIZE])) diff --git a/esphome/components/web_server/ota/ota_web_server.cpp b/esphome/components/web_server/ota/ota_web_server.cpp index 7929f3647f..30c4a59b8b 100644 --- a/esphome/components/web_server/ota/ota_web_server.cpp +++ b/esphome/components/web_server/ota/ota_web_server.cpp @@ -84,9 +84,9 @@ void OTARequestHandler::report_ota_progress_(AsyncWebServerRequest *request) { } else { ESP_LOGD(TAG, "OTA in progress: %" PRIu32 " bytes read", this->ota_read_length_); } -#ifdef USE_OTA_STATE_CALLBACK - // Report progress - use call_deferred since we're in web server task - this->parent_->state_callback_.call_deferred(ota::OTA_IN_PROGRESS, percentage, 0); +#ifdef USE_OTA_STATE_LISTENER + // Report progress - use notify_state_deferred_ since we're in web server task + this->parent_->notify_state_deferred_(ota::OTA_IN_PROGRESS, percentage, 0); #endif this->last_ota_progress_ = now; } @@ -114,9 +114,9 @@ void OTARequestHandler::handleUpload(AsyncWebServerRequest *request, const Platf // Initialize OTA on first call this->ota_init_(filename.c_str()); -#ifdef USE_OTA_STATE_CALLBACK - // Notify OTA started - use call_deferred since we're in web server task - this->parent_->state_callback_.call_deferred(ota::OTA_STARTED, 0.0f, 0); +#ifdef USE_OTA_STATE_LISTENER + // Notify OTA started - use notify_state_deferred_ since we're in web server task + this->parent_->notify_state_deferred_(ota::OTA_STARTED, 0.0f, 0); #endif // Platform-specific pre-initialization @@ -134,9 +134,9 @@ void OTARequestHandler::handleUpload(AsyncWebServerRequest *request, const Platf this->ota_backend_ = ota::make_ota_backend(); if (!this->ota_backend_) { ESP_LOGE(TAG, "Failed to create OTA backend"); -#ifdef USE_OTA_STATE_CALLBACK - this->parent_->state_callback_.call_deferred(ota::OTA_ERROR, 0.0f, - static_cast(ota::OTA_RESPONSE_ERROR_UNKNOWN)); +#ifdef USE_OTA_STATE_LISTENER + this->parent_->notify_state_deferred_(ota::OTA_ERROR, 0.0f, + static_cast(ota::OTA_RESPONSE_ERROR_UNKNOWN)); #endif return; } @@ -148,8 +148,8 @@ void OTARequestHandler::handleUpload(AsyncWebServerRequest *request, const Platf if (error_code != ota::OTA_RESPONSE_OK) { ESP_LOGE(TAG, "OTA begin failed: %d", error_code); this->ota_backend_.reset(); -#ifdef USE_OTA_STATE_CALLBACK - this->parent_->state_callback_.call_deferred(ota::OTA_ERROR, 0.0f, static_cast(error_code)); +#ifdef USE_OTA_STATE_LISTENER + this->parent_->notify_state_deferred_(ota::OTA_ERROR, 0.0f, static_cast(error_code)); #endif return; } @@ -166,8 +166,8 @@ void OTARequestHandler::handleUpload(AsyncWebServerRequest *request, const Platf ESP_LOGE(TAG, "OTA write failed: %d", error_code); this->ota_backend_->abort(); this->ota_backend_.reset(); -#ifdef USE_OTA_STATE_CALLBACK - this->parent_->state_callback_.call_deferred(ota::OTA_ERROR, 0.0f, static_cast(error_code)); +#ifdef USE_OTA_STATE_LISTENER + this->parent_->notify_state_deferred_(ota::OTA_ERROR, 0.0f, static_cast(error_code)); #endif return; } @@ -186,15 +186,15 @@ void OTARequestHandler::handleUpload(AsyncWebServerRequest *request, const Platf error_code = this->ota_backend_->end(); if (error_code == ota::OTA_RESPONSE_OK) { this->ota_success_ = true; -#ifdef USE_OTA_STATE_CALLBACK - // Report completion before reboot - use call_deferred since we're in web server task - this->parent_->state_callback_.call_deferred(ota::OTA_COMPLETED, 100.0f, 0); +#ifdef USE_OTA_STATE_LISTENER + // Report completion before reboot - use notify_state_deferred_ since we're in web server task + this->parent_->notify_state_deferred_(ota::OTA_COMPLETED, 100.0f, 0); #endif this->schedule_ota_reboot_(); } else { ESP_LOGE(TAG, "OTA end failed: %d", error_code); -#ifdef USE_OTA_STATE_CALLBACK - this->parent_->state_callback_.call_deferred(ota::OTA_ERROR, 0.0f, static_cast(error_code)); +#ifdef USE_OTA_STATE_LISTENER + this->parent_->notify_state_deferred_(ota::OTA_ERROR, 0.0f, static_cast(error_code)); #endif } this->ota_backend_.reset(); @@ -232,7 +232,7 @@ void WebServerOTAComponent::setup() { // AsyncWebServer takes ownership of the handler and will delete it when the server is destroyed base->add_handler(new OTARequestHandler(this)); // NOLINT -#ifdef USE_OTA_STATE_CALLBACK +#ifdef USE_OTA_STATE_LISTENER // Register with global OTA callback system ota::register_ota_platform(this); #endif diff --git a/esphome/core/defines.h b/esphome/core/defines.h index f4026aad96..82963c767d 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -139,7 +139,7 @@ #define USE_OTA_PASSWORD #define USE_OTA_SHA256 #define ALLOW_OTA_DOWNGRADE_MD5 -#define USE_OTA_STATE_CALLBACK +#define USE_OTA_STATE_LISTENER #define USE_OTA_VERSION 2 #define USE_TIME_TIMEZONE #define USE_WIFI From 515cdf9b9fadaa8bf8e13f9af633fe386323572c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 28 Nov 2025 13:48:19 -0600 Subject: [PATCH 369/415] its always on --- .../http_request/update/http_request_update.cpp | 8 +------- .../components/http_request/update/http_request_update.h | 6 ------ 2 files changed, 1 insertion(+), 13 deletions(-) diff --git a/esphome/components/http_request/update/http_request_update.cpp b/esphome/components/http_request/update/http_request_update.cpp index ca4afa67d4..cf0fca06e7 100644 --- a/esphome/components/http_request/update/http_request_update.cpp +++ b/esphome/components/http_request/update/http_request_update.cpp @@ -20,13 +20,8 @@ static const char *const TAG = "http_request.update"; static const size_t MAX_READ_SIZE = 256; -void HttpRequestUpdate::setup() { -#ifdef USE_OTA_STATE_LISTENER - this->ota_parent_->add_state_listener(this); -#endif -} +void HttpRequestUpdate::setup() { this->ota_parent_->add_state_listener(this); } -#ifdef USE_OTA_STATE_LISTENER void HttpRequestUpdate::on_ota_state(ota::OTAState state, float progress, uint8_t error) { if (state == ota::OTAState::OTA_IN_PROGRESS) { this->state_ = update::UPDATE_STATE_INSTALLING; @@ -39,7 +34,6 @@ void HttpRequestUpdate::on_ota_state(ota::OTAState state, float progress, uint8_ this->publish_state(); } } -#endif void HttpRequestUpdate::update() { #ifdef USE_ESP32 diff --git a/esphome/components/http_request/update/http_request_update.h b/esphome/components/http_request/update/http_request_update.h index 937f6dbeb9..197a1b5e1c 100644 --- a/esphome/components/http_request/update/http_request_update.h +++ b/esphome/components/http_request/update/http_request_update.h @@ -15,11 +15,7 @@ namespace esphome { namespace http_request { -#ifdef USE_OTA_STATE_LISTENER class HttpRequestUpdate final : public update::UpdateEntity, public PollingComponent, public ota::OTAStateListener { -#else -class HttpRequestUpdate final : public update::UpdateEntity, public PollingComponent { -#endif public: void setup() override; void update() override; @@ -34,9 +30,7 @@ class HttpRequestUpdate final : public update::UpdateEntity, public PollingCompo float get_setup_priority() const override { return setup_priority::AFTER_WIFI; } -#ifdef USE_OTA_STATE_LISTENER void on_ota_state(ota::OTAState state, float progress, uint8_t error) override; -#endif protected: HttpRequestComponent *request_parent_; From a224d0acbdcf92733e16130905bbac5a8a0a3e0f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 28 Nov 2025 13:50:20 -0600 Subject: [PATCH 370/415] dry --- .../http_request/update/http_request_update.h | 1 - esphome/components/ota/automation.h | 32 ++++--------------- 2 files changed, 7 insertions(+), 26 deletions(-) diff --git a/esphome/components/http_request/update/http_request_update.h b/esphome/components/http_request/update/http_request_update.h index 197a1b5e1c..cf34ace18e 100644 --- a/esphome/components/http_request/update/http_request_update.h +++ b/esphome/components/http_request/update/http_request_update.h @@ -1,7 +1,6 @@ #pragma once #include "esphome/core/component.h" -#include "esphome/core/defines.h" #include "esphome/core/helpers.h" #include "esphome/components/http_request/http_request.h" diff --git a/esphome/components/ota/automation.h b/esphome/components/ota/automation.h index 520cb293f0..ded8378ff2 100644 --- a/esphome/components/ota/automation.h +++ b/esphome/components/ota/automation.h @@ -14,17 +14,21 @@ class OTAStateChangeTrigger final : public Trigger, public OTAStateLis void on_ota_state(OTAState state, float progress, uint8_t error) override { this->trigger(state); } }; -class OTAStartTrigger final : public Trigger<>, public OTAStateListener { +template class OTAStateTrigger final : public Trigger<>, public OTAStateListener { public: - explicit OTAStartTrigger(OTAComponent *parent) { parent->add_state_listener(this); } + explicit OTAStateTrigger(OTAComponent *parent) { parent->add_state_listener(this); } void on_ota_state(OTAState state, float progress, uint8_t error) override { - if (state == OTA_STARTED) { + if (state == State) { this->trigger(); } } }; +using OTAStartTrigger = OTAStateTrigger; +using OTAEndTrigger = OTAStateTrigger; +using OTAAbortTrigger = OTAStateTrigger; + class OTAProgressTrigger final : public Trigger, public OTAStateListener { public: explicit OTAProgressTrigger(OTAComponent *parent) { parent->add_state_listener(this); } @@ -36,28 +40,6 @@ class OTAProgressTrigger final : public Trigger, public OTAStateListener } }; -class OTAEndTrigger final : public Trigger<>, public OTAStateListener { - public: - explicit OTAEndTrigger(OTAComponent *parent) { parent->add_state_listener(this); } - - void on_ota_state(OTAState state, float progress, uint8_t error) override { - if (state == OTA_COMPLETED) { - this->trigger(); - } - } -}; - -class OTAAbortTrigger final : public Trigger<>, public OTAStateListener { - public: - explicit OTAAbortTrigger(OTAComponent *parent) { parent->add_state_listener(this); } - - void on_ota_state(OTAState state, float progress, uint8_t error) override { - if (state == OTA_ABORT) { - this->trigger(); - } - } -}; - class OTAErrorTrigger final : public Trigger, public OTAStateListener { public: explicit OTAErrorTrigger(OTAComponent *parent) { parent->add_state_listener(this); } From d9701af9c11e3992ce3ebade327cc723ef039701 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 28 Nov 2025 13:51:21 -0600 Subject: [PATCH 371/415] dry --- esphome/components/ota/ota_backend.h | 2 ++ 1 file changed, 2 insertions(+) diff --git a/esphome/components/ota/ota_backend.h b/esphome/components/ota/ota_backend.h index e474316bb5..5316411ba0 100644 --- a/esphome/components/ota/ota_backend.h +++ b/esphome/components/ota/ota_backend.h @@ -4,7 +4,9 @@ #include "esphome/core/defines.h" #include "esphome/core/helpers.h" +#ifdef USE_OTA_STATE_LISTENER #include +#endif namespace esphome { namespace ota { From ab6b4c77d2409be180140cb4639f854adc086081 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 28 Nov 2025 13:52:58 -0600 Subject: [PATCH 372/415] dry --- esphome/components/ota/automation.h | 32 +++++++++++++++++++++-------- 1 file changed, 24 insertions(+), 8 deletions(-) diff --git a/esphome/components/ota/automation.h b/esphome/components/ota/automation.h index ded8378ff2..92c0050ba0 100644 --- a/esphome/components/ota/automation.h +++ b/esphome/components/ota/automation.h @@ -9,20 +9,30 @@ namespace ota { class OTAStateChangeTrigger final : public Trigger, public OTAStateListener { public: - explicit OTAStateChangeTrigger(OTAComponent *parent) { parent->add_state_listener(this); } + explicit OTAStateChangeTrigger(OTAComponent *parent) : parent_(parent) { parent->add_state_listener(this); } - void on_ota_state(OTAState state, float progress, uint8_t error) override { this->trigger(state); } + void on_ota_state(OTAState state, float progress, uint8_t error) override { + if (!this->parent_->is_failed()) { + this->trigger(state); + } + } + + protected: + OTAComponent *parent_; }; template class OTAStateTrigger final : public Trigger<>, public OTAStateListener { public: - explicit OTAStateTrigger(OTAComponent *parent) { parent->add_state_listener(this); } + explicit OTAStateTrigger(OTAComponent *parent) : parent_(parent) { parent->add_state_listener(this); } void on_ota_state(OTAState state, float progress, uint8_t error) override { - if (state == State) { + if (state == State && !this->parent_->is_failed()) { this->trigger(); } } + + protected: + OTAComponent *parent_; }; using OTAStartTrigger = OTAStateTrigger; @@ -31,24 +41,30 @@ using OTAAbortTrigger = OTAStateTrigger; class OTAProgressTrigger final : public Trigger, public OTAStateListener { public: - explicit OTAProgressTrigger(OTAComponent *parent) { parent->add_state_listener(this); } + explicit OTAProgressTrigger(OTAComponent *parent) : parent_(parent) { parent->add_state_listener(this); } void on_ota_state(OTAState state, float progress, uint8_t error) override { - if (state == OTA_IN_PROGRESS) { + if (state == OTA_IN_PROGRESS && !this->parent_->is_failed()) { this->trigger(progress); } } + + protected: + OTAComponent *parent_; }; class OTAErrorTrigger final : public Trigger, public OTAStateListener { public: - explicit OTAErrorTrigger(OTAComponent *parent) { parent->add_state_listener(this); } + explicit OTAErrorTrigger(OTAComponent *parent) : parent_(parent) { parent->add_state_listener(this); } void on_ota_state(OTAState state, float progress, uint8_t error) override { - if (state == OTA_ERROR) { + if (state == OTA_ERROR && !this->parent_->is_failed()) { this->trigger(error); } } + + protected: + OTAComponent *parent_; }; } // namespace ota From ee91bb2405663606eb2fcd1d810c3dca6a41b95d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 28 Nov 2025 13:54:46 -0600 Subject: [PATCH 373/415] dry --- esphome/components/ota/ota_backend.h | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/esphome/components/ota/ota_backend.h b/esphome/components/ota/ota_backend.h index 5316411ba0..64fbbcfda7 100644 --- a/esphome/components/ota/ota_backend.h +++ b/esphome/components/ota/ota_backend.h @@ -128,7 +128,8 @@ class OTAComponentBridge : public OTAStateListener { class OTAGlobalCallback { public: void register_ota(OTAComponent *ota_caller) { - // Create a bridge that forwards this component's events to global listeners + // Create a bridge that forwards this component's events to global listeners. + // Intentionally never deleted - these objects live for the lifetime of the device. auto *bridge = new OTAComponentBridge(this, ota_caller); // NOLINT(cppcoreguidelines-owning-memory) ota_caller->add_state_listener(bridge); } From a45a2e8f5fc86da63035d6e7aa0b7ebd0a8dc852 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 28 Nov 2025 14:01:37 -0600 Subject: [PATCH 374/415] guards --- .../esp32_ble_tracker/esp32_ble_tracker.cpp | 27 ++++++------ .../esp32_ble_tracker/esp32_ble_tracker.h | 11 +++++ .../micro_wake_word/micro_wake_word.cpp | 21 ++++++---- .../micro_wake_word/micro_wake_word.h | 16 ++++++- .../media_player/speaker_media_player.cpp | 42 ++++++++++--------- .../media_player/speaker_media_player.h | 18 +++++++- 6 files changed, 92 insertions(+), 43 deletions(-) diff --git a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp index d3c5edfb94..542b076f40 100644 --- a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp +++ b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp @@ -71,21 +71,24 @@ void ESP32BLETracker::setup() { global_esp32_ble_tracker = this; -#ifdef USE_OTA - ota::get_global_ota_callback()->add_on_state_callback( - [this](ota::OTAState state, float progress, uint8_t error, ota::OTAComponent *comp) { - if (state == ota::OTA_STARTED) { - this->stop_scan(); -#ifdef ESPHOME_ESP32_BLE_TRACKER_CLIENT_COUNT - for (auto *client : this->clients_) { - client->disconnect(); - } -#endif - } - }); +#ifdef USE_OTA_STATE_LISTENER + ota::get_global_ota_callback()->add_global_state_listener(this); #endif } +#ifdef USE_OTA_STATE_LISTENER +void ESP32BLETracker::on_ota_global_state(ota::OTAState state, float progress, uint8_t error, ota::OTAComponent *comp) { + if (state == ota::OTA_STARTED) { + this->stop_scan(); +#ifdef ESPHOME_ESP32_BLE_TRACKER_CLIENT_COUNT + for (auto *client : this->clients_) { + client->disconnect(); + } +#endif + } +} +#endif + void ESP32BLETracker::loop() { if (!this->parent_->is_active()) { this->ble_was_disabled_ = true; diff --git a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h index 92d13a62ad..b64e36279c 100644 --- a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h +++ b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h @@ -22,6 +22,10 @@ #include "esphome/components/esp32_ble/ble_uuid.h" #include "esphome/components/esp32_ble/ble_scan_result.h" +#ifdef USE_OTA_STATE_LISTENER +#include "esphome/components/ota/ota_backend.h" +#endif + namespace esphome::esp32_ble_tracker { using namespace esp32_ble; @@ -241,6 +245,9 @@ class ESP32BLETracker : public Component, public GAPScanEventHandler, public GATTcEventHandler, public BLEStatusEventHandler, +#ifdef USE_OTA_STATE_LISTENER + public ota::OTAGlobalStateListener, +#endif public Parented { public: void set_scan_duration(uint32_t scan_duration) { scan_duration_ = scan_duration; } @@ -274,6 +281,10 @@ class ESP32BLETracker : public Component, void gap_scan_event_handler(const BLEScanResult &scan_result) override; void ble_before_disabled_event_handler() override; +#ifdef USE_OTA_STATE_LISTENER + void on_ota_global_state(ota::OTAState state, float progress, uint8_t error, ota::OTAComponent *comp) override; +#endif + /// Add a listener for scanner state changes void add_scanner_state_listener(BLEScannerStateListener *listener) { this->scanner_state_listeners_.push_back(listener); diff --git a/esphome/components/micro_wake_word/micro_wake_word.cpp b/esphome/components/micro_wake_word/micro_wake_word.cpp index a0547b158e..0f72f188bc 100644 --- a/esphome/components/micro_wake_word/micro_wake_word.cpp +++ b/esphome/components/micro_wake_word/micro_wake_word.cpp @@ -119,18 +119,21 @@ void MicroWakeWord::setup() { } }); -#ifdef USE_OTA - ota::get_global_ota_callback()->add_on_state_callback( - [this](ota::OTAState state, float progress, uint8_t error, ota::OTAComponent *comp) { - if (state == ota::OTA_STARTED) { - this->suspend_task_(); - } else if (state == ota::OTA_ERROR) { - this->resume_task_(); - } - }); +#ifdef USE_OTA_STATE_LISTENER + ota::get_global_ota_callback()->add_global_state_listener(this); #endif } +#ifdef USE_OTA_STATE_LISTENER +void MicroWakeWord::on_ota_global_state(ota::OTAState state, float progress, uint8_t error, ota::OTAComponent *comp) { + if (state == ota::OTA_STARTED) { + this->suspend_task_(); + } else if (state == ota::OTA_ERROR) { + this->resume_task_(); + } +} +#endif + void MicroWakeWord::inference_task(void *params) { MicroWakeWord *this_mww = (MicroWakeWord *) params; diff --git a/esphome/components/micro_wake_word/micro_wake_word.h b/esphome/components/micro_wake_word/micro_wake_word.h index d46c40e48b..84261eaa5b 100644 --- a/esphome/components/micro_wake_word/micro_wake_word.h +++ b/esphome/components/micro_wake_word/micro_wake_word.h @@ -9,8 +9,13 @@ #include "esphome/core/automation.h" #include "esphome/core/component.h" +#include "esphome/core/defines.h" #include "esphome/core/ring_buffer.h" +#ifdef USE_OTA_STATE_LISTENER +#include "esphome/components/ota/ota_backend.h" +#endif + #include #include @@ -26,13 +31,22 @@ enum State { STOPPED, }; -class MicroWakeWord : public Component { +class MicroWakeWord : public Component +#ifdef USE_OTA_STATE_LISTENER + , + public ota::OTAGlobalStateListener +#endif +{ public: void setup() override; void loop() override; float get_setup_priority() const override; void dump_config() override; +#ifdef USE_OTA_STATE_LISTENER + void on_ota_global_state(ota::OTAState state, float progress, uint8_t error, ota::OTAComponent *comp) override; +#endif + void start(); void stop(); diff --git a/esphome/components/speaker/media_player/speaker_media_player.cpp b/esphome/components/speaker/media_player/speaker_media_player.cpp index b45a78010a..5722aab195 100644 --- a/esphome/components/speaker/media_player/speaker_media_player.cpp +++ b/esphome/components/speaker/media_player/speaker_media_player.cpp @@ -66,25 +66,8 @@ void SpeakerMediaPlayer::setup() { this->set_mute_state_(false); } -#ifdef USE_OTA - ota::get_global_ota_callback()->add_on_state_callback( - [this](ota::OTAState state, float progress, uint8_t error, ota::OTAComponent *comp) { - if (state == ota::OTA_STARTED) { - if (this->media_pipeline_ != nullptr) { - this->media_pipeline_->suspend_tasks(); - } - if (this->announcement_pipeline_ != nullptr) { - this->announcement_pipeline_->suspend_tasks(); - } - } else if (state == ota::OTA_ERROR) { - if (this->media_pipeline_ != nullptr) { - this->media_pipeline_->resume_tasks(); - } - if (this->announcement_pipeline_ != nullptr) { - this->announcement_pipeline_->resume_tasks(); - } - } - }); +#ifdef USE_OTA_STATE_LISTENER + ota::get_global_ota_callback()->add_global_state_listener(this); #endif this->announcement_pipeline_ = @@ -300,6 +283,27 @@ void SpeakerMediaPlayer::watch_media_commands_() { } } +#ifdef USE_OTA_STATE_LISTENER +void SpeakerMediaPlayer::on_ota_global_state(ota::OTAState state, float progress, uint8_t error, + ota::OTAComponent *comp) { + if (state == ota::OTA_STARTED) { + if (this->media_pipeline_ != nullptr) { + this->media_pipeline_->suspend_tasks(); + } + if (this->announcement_pipeline_ != nullptr) { + this->announcement_pipeline_->suspend_tasks(); + } + } else if (state == ota::OTA_ERROR) { + if (this->media_pipeline_ != nullptr) { + this->media_pipeline_->resume_tasks(); + } + if (this->announcement_pipeline_ != nullptr) { + this->announcement_pipeline_->resume_tasks(); + } + } +} +#endif + void SpeakerMediaPlayer::loop() { this->watch_media_commands_(); diff --git a/esphome/components/speaker/media_player/speaker_media_player.h b/esphome/components/speaker/media_player/speaker_media_player.h index 967772d1a5..f1c564b63d 100644 --- a/esphome/components/speaker/media_player/speaker_media_player.h +++ b/esphome/components/speaker/media_player/speaker_media_player.h @@ -5,14 +5,18 @@ #include "audio_pipeline.h" #include "esphome/components/audio/audio.h" - #include "esphome/components/media_player/media_player.h" #include "esphome/components/speaker/speaker.h" #include "esphome/core/automation.h" #include "esphome/core/component.h" +#include "esphome/core/defines.h" #include "esphome/core/preferences.h" +#ifdef USE_OTA_STATE_LISTENER +#include "esphome/components/ota/ota_backend.h" +#endif + #include #include #include @@ -39,12 +43,22 @@ struct VolumeRestoreState { bool is_muted; }; -class SpeakerMediaPlayer : public Component, public media_player::MediaPlayer { +class SpeakerMediaPlayer : public Component, + public media_player::MediaPlayer +#ifdef USE_OTA_STATE_LISTENER + , + public ota::OTAGlobalStateListener +#endif +{ public: float get_setup_priority() const override { return esphome::setup_priority::PROCESSOR; } void setup() override; void loop() override; +#ifdef USE_OTA_STATE_LISTENER + void on_ota_global_state(ota::OTAState state, float progress, uint8_t error, ota::OTAComponent *comp) override; +#endif + // MediaPlayer implementations media_player::MediaPlayerTraits get_traits() override; bool is_muted() const override { return this->is_muted_; } From b1a318c0d75682c2a5f7ad910868bdfae1f23e07 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 28 Nov 2025 14:07:55 -0600 Subject: [PATCH 375/415] simplify, no more register needed --- .../components/esphome/ota/ota_esphome.cpp | 4 --- .../http_request/ota/ota_http_request.cpp | 6 +--- esphome/components/ota/ota_backend.cpp | 9 ++--- esphome/components/ota/ota_backend.h | 36 +++---------------- .../web_server/ota/ota_web_server.cpp | 4 --- 5 files changed, 11 insertions(+), 48 deletions(-) diff --git a/esphome/components/esphome/ota/ota_esphome.cpp b/esphome/components/esphome/ota/ota_esphome.cpp index 469c57211c..521b3de15a 100644 --- a/esphome/components/esphome/ota/ota_esphome.cpp +++ b/esphome/components/esphome/ota/ota_esphome.cpp @@ -42,10 +42,6 @@ static constexpr size_t SHA256_HEX_SIZE = 64; // SHA256 hash as hex string (32 #endif // USE_OTA_PASSWORD void ESPHomeOTAComponent::setup() { -#ifdef USE_OTA_STATE_LISTENER - ota::register_ota_platform(this); -#endif - this->server_ = socket::socket_ip_loop_monitored(SOCK_STREAM, 0); // monitored for incoming connections if (this->server_ == nullptr) { this->log_socket_error_(LOG_STR("creation")); diff --git a/esphome/components/http_request/ota/ota_http_request.cpp b/esphome/components/http_request/ota/ota_http_request.cpp index 2a52a0e264..59bdeb9ceb 100644 --- a/esphome/components/http_request/ota/ota_http_request.cpp +++ b/esphome/components/http_request/ota/ota_http_request.cpp @@ -17,11 +17,7 @@ namespace http_request { static const char *const TAG = "http_request.ota"; -void OtaHttpRequestComponent::setup() { -#ifdef USE_OTA_STATE_LISTENER - ota::register_ota_platform(this); -#endif -} +void OtaHttpRequestComponent::setup() {} void OtaHttpRequestComponent::dump_config() { ESP_LOGCONFIG(TAG, "Over-The-Air updates via HTTP request"); }; diff --git a/esphome/components/ota/ota_backend.cpp b/esphome/components/ota/ota_backend.cpp index 5f510b4f8b..8fb9f67214 100644 --- a/esphome/components/ota/ota_backend.cpp +++ b/esphome/components/ota/ota_backend.cpp @@ -13,10 +13,11 @@ OTAGlobalCallback *get_global_ota_callback() { return global_ota_callback; } -void register_ota_platform(OTAComponent *ota_caller) { get_global_ota_callback()->register_ota(ota_caller); } - -void OTAComponentBridge::on_ota_state(OTAState state, float progress, uint8_t error) { - this->global_callback_->notify_global_listeners(state, progress, error, this->component_); +void OTAComponent::notify_state_(OTAState state, float progress, uint8_t error) { + for (auto *listener : this->state_listeners_) { + listener->on_ota_state(state, progress, error); + } + get_global_ota_callback()->notify_ota_state(state, progress, error, this); } #endif diff --git a/esphome/components/ota/ota_backend.h b/esphome/components/ota/ota_backend.h index 64fbbcfda7..c00ecba9e6 100644 --- a/esphome/components/ota/ota_backend.h +++ b/esphome/components/ota/ota_backend.h @@ -76,11 +76,7 @@ class OTAComponent : public Component { void add_state_listener(OTAStateListener *listener) { this->state_listeners_.push_back(listener); } protected: - void notify_state_(OTAState state, float progress, uint8_t error) { - for (auto *listener : this->state_listeners_) { - listener->on_ota_state(state, progress, error); - } - } + void notify_state_(OTAState state, float progress, uint8_t error); /** Notify state with deferral to main loop (for thread safety). * @@ -96,7 +92,6 @@ class OTAComponent : public Component { }; #ifdef USE_OTA_STATE_LISTENER -class OTAGlobalCallback; /** Listener interface for global OTA state changes (includes OTA component pointer). * @@ -107,36 +102,16 @@ class OTAGlobalStateListener { virtual void on_ota_global_state(OTAState state, float progress, uint8_t error, OTAComponent *component) = 0; }; -/** Helper class to bridge per-component OTA state to global listeners. +/** Global callback that aggregates OTA state from all OTA components. * - * Each OTA component gets one of these registered as a listener. When that - * component fires state events, this bridge forwards them to all global listeners - * along with the component pointer. + * OTA components call notify_ota_state() directly with their pointer, + * which forwards the event to all registered global listeners. */ -class OTAComponentBridge : public OTAStateListener { - public: - OTAComponentBridge(OTAGlobalCallback *global_callback, OTAComponent *component) - : global_callback_(global_callback), component_(component) {} - - void on_ota_state(OTAState state, float progress, uint8_t error) override; - - private: - OTAGlobalCallback *global_callback_; - OTAComponent *component_; -}; - class OTAGlobalCallback { public: - void register_ota(OTAComponent *ota_caller) { - // Create a bridge that forwards this component's events to global listeners. - // Intentionally never deleted - these objects live for the lifetime of the device. - auto *bridge = new OTAComponentBridge(this, ota_caller); // NOLINT(cppcoreguidelines-owning-memory) - ota_caller->add_state_listener(bridge); - } - void add_global_state_listener(OTAGlobalStateListener *listener) { this->global_listeners_.push_back(listener); } - void notify_global_listeners(OTAState state, float progress, uint8_t error, OTAComponent *component) { + void notify_ota_state(OTAState state, float progress, uint8_t error, OTAComponent *component) { for (auto *listener : this->global_listeners_) { listener->on_ota_global_state(state, progress, error, component); } @@ -147,7 +122,6 @@ class OTAGlobalCallback { }; OTAGlobalCallback *get_global_ota_callback(); -void register_ota_platform(OTAComponent *ota_caller); // OTA implementations should use: // - notify_state_() when already in main loop (e.g., esphome OTA) diff --git a/esphome/components/web_server/ota/ota_web_server.cpp b/esphome/components/web_server/ota/ota_web_server.cpp index 30c4a59b8b..f612aa056c 100644 --- a/esphome/components/web_server/ota/ota_web_server.cpp +++ b/esphome/components/web_server/ota/ota_web_server.cpp @@ -232,10 +232,6 @@ void WebServerOTAComponent::setup() { // AsyncWebServer takes ownership of the handler and will delete it when the server is destroyed base->add_handler(new OTARequestHandler(this)); // NOLINT -#ifdef USE_OTA_STATE_LISTENER - // Register with global OTA callback system - ota::register_ota_platform(this); -#endif } void WebServerOTAComponent::dump_config() { ESP_LOGCONFIG(TAG, "Web Server OTA"); } From e3dc9a715fc39a27c10989728a83223077f0f2fa Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 28 Nov 2025 14:14:03 -0600 Subject: [PATCH 376/415] tweak --- esphome/components/ota/ota_backend.h | 2 ++ 1 file changed, 2 insertions(+) diff --git a/esphome/components/ota/ota_backend.h b/esphome/components/ota/ota_backend.h index c00ecba9e6..e03afd4fc6 100644 --- a/esphome/components/ota/ota_backend.h +++ b/esphome/components/ota/ota_backend.h @@ -67,6 +67,7 @@ class OTABackend { */ class OTAStateListener { public: + virtual ~OTAStateListener() = default; virtual void on_ota_state(OTAState state, float progress, uint8_t error) = 0; }; @@ -99,6 +100,7 @@ class OTAComponent : public Component { */ class OTAGlobalStateListener { public: + virtual ~OTAGlobalStateListener() = default; virtual void on_ota_global_state(OTAState state, float progress, uint8_t error, OTAComponent *component) = 0; }; From ed0751246ad4468bd345110f1b23c936808e8815 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 28 Nov 2025 14:28:02 -0600 Subject: [PATCH 377/415] [logger] Conditionally compile log level change listener --- esphome/components/logger/__init__.py | 22 +++++++++++++++++++ esphome/components/logger/logger.cpp | 5 ++++- esphome/components/logger/logger.h | 22 ++++++++++++++++--- esphome/components/logger/select/__init__.py | 9 +++++++- .../logger/select/logger_level_select.cpp | 6 ++--- .../logger/select/logger_level_select.h | 9 ++++++-- esphome/core/defines.h | 1 + 7 files changed, 64 insertions(+), 10 deletions(-) diff --git a/esphome/components/logger/__init__.py b/esphome/components/logger/__init__.py index 39877030e9..bd9885facf 100644 --- a/esphome/components/logger/__init__.py +++ b/esphome/components/logger/__init__.py @@ -406,6 +406,8 @@ async def to_code(config): conf, ) + CORE.add_job(final_step) + def validate_printf(value): # https://stackoverflow.com/questions/30011379/how-can-i-parse-a-c-format-string-in-python @@ -506,3 +508,23 @@ FILTER_SOURCE_FILES = filter_source_files_from_platform( }, } ) + +# Key for CORE.data to track if level listeners are requested +LOGGER_LEVEL_LISTENERS_KEY = "logger_level_listeners" + + +def request_logger_level_listeners() -> None: + """Request that logger level listeners be compiled in. + + Components that need to be notified about log level changes should call this + function during their code generation. This enables the add_level_listener() + method and compiles in the listener vector. + """ + CORE.data[LOGGER_LEVEL_LISTENERS_KEY] = True + + +@coroutine_with_priority(CoroPriority.FINAL) +async def final_step(): + """Final code generation step to configure optional logger features.""" + if CORE.data.get(LOGGER_LEVEL_LISTENERS_KEY, False): + cg.add_define("USE_LOGGER_LEVEL_LISTENERS") diff --git a/esphome/components/logger/logger.cpp b/esphome/components/logger/logger.cpp index f925e85e11..21e2b44808 100644 --- a/esphome/components/logger/logger.cpp +++ b/esphome/components/logger/logger.cpp @@ -288,7 +288,10 @@ void Logger::set_log_level(uint8_t level) { ESP_LOGW(TAG, "Cannot set log level higher than pre-compiled %s", LOG_STR_ARG(LOG_LEVELS[ESPHOME_LOG_LEVEL])); } this->current_level_ = level; - this->level_callback_.call(level); +#ifdef USE_LOGGER_LEVEL_LISTENERS + for (auto *listener : this->level_listeners_) + listener->on_log_level_change(level); +#endif } Logger *global_logger = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) diff --git a/esphome/components/logger/logger.h b/esphome/components/logger/logger.h index a0024411d7..4cf3d1f423 100644 --- a/esphome/components/logger/logger.h +++ b/esphome/components/logger/logger.h @@ -58,6 +58,18 @@ class LogListener { virtual void on_log(uint8_t level, const char *tag, const char *message, size_t message_len) = 0; }; +#ifdef USE_LOGGER_LEVEL_LISTENERS +/** Interface for receiving log level changes without std::function overhead. + * + * Components can implement this interface instead of using lambdas with std::function + * to reduce flash usage from std::function type erasure machinery. + */ +class LoggerLevelListener { + public: + virtual void on_log_level_change(uint8_t level) = 0; +}; +#endif + #ifdef USE_LOGGER_RUNTIME_TAG_LEVELS // Comparison function for const char* keys in log_levels_ map struct CStrCompare { @@ -193,8 +205,10 @@ class Logger : public Component { /// Register a log listener to receive log messages void add_log_listener(LogListener *listener) { this->log_listeners_.push_back(listener); } - // add a listener for log level changes - void add_listener(std::function &&callback) { this->level_callback_.add(std::move(callback)); } +#ifdef USE_LOGGER_LEVEL_LISTENERS + /// Register a listener for log level changes + void add_level_listener(LoggerLevelListener *listener) { this->level_listeners_.push_back(listener); } +#endif float get_setup_priority() const override; @@ -325,7 +339,9 @@ class Logger : public Component { std::map log_levels_{}; #endif std::vector log_listeners_; // Log message listeners (API, MQTT, syslog, etc.) - CallbackManager level_callback_{}; +#ifdef USE_LOGGER_LEVEL_LISTENERS + std::vector level_listeners_; // Log level change listeners +#endif #ifdef USE_ESPHOME_TASK_LOG_BUFFER std::unique_ptr log_buffer_; // Will be initialized with init_log_buffer #endif diff --git a/esphome/components/logger/select/__init__.py b/esphome/components/logger/select/__init__.py index 2e83599eb4..6ce663978e 100644 --- a/esphome/components/logger/select/__init__.py +++ b/esphome/components/logger/select/__init__.py @@ -5,7 +5,13 @@ from esphome.const import CONF_LEVEL, CONF_LOGGER, ENTITY_CATEGORY_CONFIG, ICON_ from esphome.core import CORE from esphome.cpp_helpers import register_component, register_parented -from .. import CONF_LOGGER_ID, LOG_LEVELS, Logger, logger_ns +from .. import ( + CONF_LOGGER_ID, + LOG_LEVELS, + Logger, + logger_ns, + request_logger_level_listeners, +) CODEOWNERS = ["@clydebarrow"] @@ -21,6 +27,7 @@ CONFIG_SCHEMA = select.select_schema( async def to_code(config): + request_logger_level_listeners() parent = await cg.get_variable(config[CONF_LOGGER_ID]) levels = list(LOG_LEVELS) index = levels.index(CORE.data[CONF_LOGGER][CONF_LEVEL]) diff --git a/esphome/components/logger/select/logger_level_select.cpp b/esphome/components/logger/select/logger_level_select.cpp index e2ec28a390..3091ca1851 100644 --- a/esphome/components/logger/select/logger_level_select.cpp +++ b/esphome/components/logger/select/logger_level_select.cpp @@ -2,7 +2,7 @@ namespace esphome::logger { -void LoggerLevelSelect::publish_state(int level) { +void LoggerLevelSelect::on_log_level_change(uint8_t level) { auto index = level_to_index(level); if (!this->has_index(index)) return; @@ -10,8 +10,8 @@ void LoggerLevelSelect::publish_state(int level) { } void LoggerLevelSelect::setup() { - this->parent_->add_listener([this](int level) { this->publish_state(level); }); - this->publish_state(this->parent_->get_log_level()); + this->parent_->add_level_listener(this); + this->on_log_level_change(this->parent_->get_log_level()); } void LoggerLevelSelect::control(size_t index) { this->parent_->set_log_level(index_to_level(index)); } diff --git a/esphome/components/logger/select/logger_level_select.h b/esphome/components/logger/select/logger_level_select.h index 950edd29ac..6482114943 100644 --- a/esphome/components/logger/select/logger_level_select.h +++ b/esphome/components/logger/select/logger_level_select.h @@ -5,12 +5,17 @@ #include "esphome/components/logger/logger.h" namespace esphome::logger { -class LoggerLevelSelect : public Component, public select::Select, public Parented { +class LoggerLevelSelect final : public Component, + public select::Select, + public Parented, + public LoggerLevelListener { public: - void publish_state(int level); void setup() override; void control(size_t index) override; + // LoggerLevelListener interface + void on_log_level_change(uint8_t level) override; + protected: // Convert log level to option index (skip CONFIG at level 4) static uint8_t level_to_index(uint8_t level) { return (level > ESPHOME_LOG_LEVEL_CONFIG) ? level - 1 : level; } diff --git a/esphome/core/defines.h b/esphome/core/defines.h index f4026aad96..538d4e3d6e 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -51,6 +51,7 @@ #define USE_LIGHT #define USE_LOCK #define USE_LOGGER +#define USE_LOGGER_LEVEL_LISTENERS #define USE_LOGGER_RUNTIME_TAG_LEVELS #define USE_LVGL #define USE_LVGL_ANIMIMG From 2e5529664006c585692ba47310229e2fe3f3e52c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 28 Nov 2025 14:43:11 -0600 Subject: [PATCH 378/415] [sensor] Replace timeout filter scheduler with loop-based implementation (#11922) --- esphome/components/sensor/__init__.py | 11 +++++-- esphome/components/sensor/filter.cpp | 43 ++++++++++++++++++++------ esphome/components/sensor/filter.h | 44 ++++++++++++++++++++++----- 3 files changed, 78 insertions(+), 20 deletions(-) diff --git a/esphome/components/sensor/__init__.py b/esphome/components/sensor/__init__.py index e8fec222a1..f83226d10f 100644 --- a/esphome/components/sensor/__init__.py +++ b/esphome/components/sensor/__init__.py @@ -270,7 +270,9 @@ ThrottleFilter = sensor_ns.class_("ThrottleFilter", Filter) ThrottleWithPriorityFilter = sensor_ns.class_( "ThrottleWithPriorityFilter", ValueListFilter ) -TimeoutFilter = sensor_ns.class_("TimeoutFilter", Filter, cg.Component) +TimeoutFilterBase = sensor_ns.class_("TimeoutFilterBase", Filter, cg.Component) +TimeoutFilterLast = sensor_ns.class_("TimeoutFilterLast", TimeoutFilterBase) +TimeoutFilterConfigured = sensor_ns.class_("TimeoutFilterConfigured", TimeoutFilterBase) DebounceFilter = sensor_ns.class_("DebounceFilter", Filter, cg.Component) HeartbeatFilter = sensor_ns.class_("HeartbeatFilter", Filter, cg.Component) DeltaFilter = sensor_ns.class_("DeltaFilter", Filter) @@ -681,11 +683,16 @@ TIMEOUT_SCHEMA = cv.maybe_simple_value( ) -@FILTER_REGISTRY.register("timeout", TimeoutFilter, TIMEOUT_SCHEMA) +@FILTER_REGISTRY.register("timeout", TimeoutFilterBase, TIMEOUT_SCHEMA) async def timeout_filter_to_code(config, filter_id): + filter_id = filter_id.copy() if config[CONF_VALUE] == "last": + # Use TimeoutFilterLast for "last" mode (smaller, more common - LD2450, LD2412, etc.) + filter_id.type = TimeoutFilterLast var = cg.new_Pvariable(filter_id, config[CONF_TIMEOUT]) else: + # Use TimeoutFilterConfigured for configured value mode + filter_id.type = TimeoutFilterConfigured template_ = await cg.templatable(config[CONF_VALUE], [], float) var = cg.new_Pvariable(filter_id, config[CONF_TIMEOUT], template_) await cg.register_component(var, {}) diff --git a/esphome/components/sensor/filter.cpp b/esphome/components/sensor/filter.cpp index 65d8dea31c..c8c6540112 100644 --- a/esphome/components/sensor/filter.cpp +++ b/esphome/components/sensor/filter.cpp @@ -339,20 +339,43 @@ void OrFilter::initialize(Sensor *parent, Filter *next) { this->phi_.initialize(parent, nullptr); } -// TimeoutFilter -optional TimeoutFilter::new_value(float value) { - if (this->value_.has_value()) { - this->set_timeout("timeout", this->time_period_, [this]() { this->output(this->value_.value().value()); }); - } else { - this->set_timeout("timeout", this->time_period_, [this, value]() { this->output(value); }); +// TimeoutFilterBase - shared loop logic +void TimeoutFilterBase::loop() { + // Check if timeout period has elapsed + // Use cached loop start time to avoid repeated millis() calls + const uint32_t now = App.get_loop_component_start_time(); + if (now - this->timeout_start_time_ >= this->time_period_) { + // Timeout fired - get output value from derived class and output it + this->output(this->get_output_value()); + + // Disable loop until next value arrives + this->disable_loop(); } +} + +float TimeoutFilterBase::get_setup_priority() const { return setup_priority::HARDWARE; } + +// TimeoutFilterLast - "last" mode implementation +optional TimeoutFilterLast::new_value(float value) { + // Store the value to output when timeout fires + this->pending_value_ = value; + + // Record when timeout started and enable loop + this->timeout_start_time_ = millis(); + this->enable_loop(); + return value; } -TimeoutFilter::TimeoutFilter(uint32_t time_period) : time_period_(time_period) {} -TimeoutFilter::TimeoutFilter(uint32_t time_period, const TemplatableValue &new_value) - : time_period_(time_period), value_(new_value) {} -float TimeoutFilter::get_setup_priority() const { return setup_priority::HARDWARE; } +// TimeoutFilterConfigured - configured value mode implementation +optional TimeoutFilterConfigured::new_value(float value) { + // Record when timeout started and enable loop + // Note: we don't store the incoming value since we have a configured value + this->timeout_start_time_ = millis(); + this->enable_loop(); + + return value; +} // DebounceFilter optional DebounceFilter::new_value(float value) { diff --git a/esphome/components/sensor/filter.h b/esphome/components/sensor/filter.h index 75e28a1efe..92a9184c18 100644 --- a/esphome/components/sensor/filter.h +++ b/esphome/components/sensor/filter.h @@ -380,18 +380,46 @@ class ThrottleWithPriorityFilter : public ValueListFilter { uint32_t min_time_between_inputs_; }; -class TimeoutFilter : public Filter, public Component { +// Base class for timeout filters - contains common loop logic +class TimeoutFilterBase : public Filter, public Component { public: - explicit TimeoutFilter(uint32_t time_period); - explicit TimeoutFilter(uint32_t time_period, const TemplatableValue &new_value); - - optional new_value(float value) override; - + void loop() override; float get_setup_priority() const override; protected: - uint32_t time_period_; - optional> value_; + explicit TimeoutFilterBase(uint32_t time_period) : time_period_(time_period) { this->disable_loop(); } + virtual float get_output_value() = 0; + + uint32_t time_period_; // 4 bytes (timeout duration in ms) + uint32_t timeout_start_time_{0}; // 4 bytes (when the timeout was started) + // Total base: 8 bytes +}; + +// Timeout filter for "last" mode - outputs the last received value after timeout +class TimeoutFilterLast : public TimeoutFilterBase { + public: + explicit TimeoutFilterLast(uint32_t time_period) : TimeoutFilterBase(time_period) {} + + optional new_value(float value) override; + + protected: + float get_output_value() override { return this->pending_value_; } + float pending_value_{0}; // 4 bytes (value to output when timeout fires) + // Total: 8 (base) + 4 = 12 bytes + vtable ptr + Component overhead +}; + +// Timeout filter with configured value - evaluates TemplatableValue after timeout +class TimeoutFilterConfigured : public TimeoutFilterBase { + public: + explicit TimeoutFilterConfigured(uint32_t time_period, const TemplatableValue &new_value) + : TimeoutFilterBase(time_period), value_(new_value) {} + + optional new_value(float value) override; + + protected: + float get_output_value() override { return this->value_.value(); } + TemplatableValue value_; // 16 bytes (configured output value, can be lambda) + // Total: 8 (base) + 16 = 24 bytes + vtable ptr + Component overhead }; class DebounceFilter : public Filter, public Component { From 200c0c77c77106b5c705903a8621b324d550074e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 28 Nov 2025 14:48:52 -0600 Subject: [PATCH 379/415] Update esphome/components/logger/logger.h Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- esphome/components/logger/logger.h | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/esphome/components/logger/logger.h b/esphome/components/logger/logger.h index 4cf3d1f423..8abc1196e1 100644 --- a/esphome/components/logger/logger.h +++ b/esphome/components/logger/logger.h @@ -63,6 +63,18 @@ class LogListener { * * Components can implement this interface instead of using lambdas with std::function * to reduce flash usage from std::function type erasure machinery. + * + * Usage: + * class MyComponent : public Component, public LoggerLevelListener { + * public: + * void setup() override { + * if (logger::global_logger != nullptr) + * logger::global_logger->add_logger_level_listener(this); + * } + * void on_log_level_change(uint8_t level) override { + * // Handle log level change + * } + * }; */ class LoggerLevelListener { public: From ca599b25c2a817a963eb5f76de2a0fcd8cc73738 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 28 Nov 2025 15:33:28 -0600 Subject: [PATCH 380/415] [espnow] Initialize LwIP stack when running without WiFi component (#12169) --- esphome/components/espnow/espnow_component.cpp | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/esphome/components/espnow/espnow_component.cpp b/esphome/components/espnow/espnow_component.cpp index d2f136d1c7..bc05833709 100644 --- a/esphome/components/espnow/espnow_component.cpp +++ b/esphome/components/espnow/espnow_component.cpp @@ -10,6 +10,7 @@ #include #include +#include #include #include #include @@ -157,6 +158,12 @@ bool ESPNowComponent::is_wifi_enabled() { } void ESPNowComponent::setup() { +#ifndef USE_WIFI + // Initialize LwIP stack for wake_loop_threadsafe() socket support + // When WiFi component is present, it handles esp_netif_init() + ESP_ERROR_CHECK(esp_netif_init()); +#endif + if (this->enable_on_boot_) { this->enable_(); } else { From e8bc19a07d32124aafe2d36ebce0855e8ac046b9 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 28 Nov 2025 16:11:23 -0600 Subject: [PATCH 381/415] [alarm_control_panel] Replace callbacks with listener interface --- .../alarm_control_panel.cpp | 70 ++--------- .../alarm_control_panel/alarm_control_panel.h | 109 +++++------------- .../alarm_control_panel/automation.h | 91 ++++++++------- .../mqtt/mqtt_alarm_control_panel.cpp | 2 +- .../mqtt/mqtt_alarm_control_panel.h | 9 +- .../template_alarm_control_panel.cpp | 4 +- 6 files changed, 100 insertions(+), 185 deletions(-) diff --git a/esphome/components/alarm_control_panel/alarm_control_panel.cpp b/esphome/components/alarm_control_panel/alarm_control_panel.cpp index c29e02c8ef..585401028a 100644 --- a/esphome/components/alarm_control_panel/alarm_control_panel.cpp +++ b/esphome/components/alarm_control_panel/alarm_control_panel.cpp @@ -35,29 +35,15 @@ void AlarmControlPanel::publish_state(AlarmControlPanelState state) { ESP_LOGD(TAG, "Set state to: %s, previous: %s", LOG_STR_ARG(alarm_control_panel_state_to_string(state)), LOG_STR_ARG(alarm_control_panel_state_to_string(prev_state))); this->current_state_ = state; - this->state_callback_.call(); + + for (auto *listener : this->listeners_) { + listener->on_state(state, prev_state); + } + #if defined(USE_ALARM_CONTROL_PANEL) && defined(USE_CONTROLLER_REGISTRY) ControllerRegistry::notify_alarm_control_panel_update(this); #endif - if (state == ACP_STATE_TRIGGERED) { - this->triggered_callback_.call(); - } else if (state == ACP_STATE_ARMING) { - this->arming_callback_.call(); - } else if (state == ACP_STATE_PENDING) { - this->pending_callback_.call(); - } else if (state == ACP_STATE_ARMED_HOME) { - this->armed_home_callback_.call(); - } else if (state == ACP_STATE_ARMED_NIGHT) { - this->armed_night_callback_.call(); - } else if (state == ACP_STATE_ARMED_AWAY) { - this->armed_away_callback_.call(); - } else if (state == ACP_STATE_DISARMED) { - this->disarmed_callback_.call(); - } - if (prev_state == ACP_STATE_TRIGGERED) { - this->cleared_callback_.call(); - } if (state == this->desired_state_) { // only store when in the desired state this->pref_.save(&state); @@ -65,48 +51,14 @@ void AlarmControlPanel::publish_state(AlarmControlPanelState state) { } } -void AlarmControlPanel::add_on_state_callback(std::function &&callback) { - this->state_callback_.add(std::move(callback)); +void AlarmControlPanel::notify_chime() { + for (auto *listener : this->listeners_) + listener->on_chime(); } -void AlarmControlPanel::add_on_triggered_callback(std::function &&callback) { - this->triggered_callback_.add(std::move(callback)); -} - -void AlarmControlPanel::add_on_arming_callback(std::function &&callback) { - this->arming_callback_.add(std::move(callback)); -} - -void AlarmControlPanel::add_on_armed_home_callback(std::function &&callback) { - this->armed_home_callback_.add(std::move(callback)); -} - -void AlarmControlPanel::add_on_armed_night_callback(std::function &&callback) { - this->armed_night_callback_.add(std::move(callback)); -} - -void AlarmControlPanel::add_on_armed_away_callback(std::function &&callback) { - this->armed_away_callback_.add(std::move(callback)); -} - -void AlarmControlPanel::add_on_pending_callback(std::function &&callback) { - this->pending_callback_.add(std::move(callback)); -} - -void AlarmControlPanel::add_on_disarmed_callback(std::function &&callback) { - this->disarmed_callback_.add(std::move(callback)); -} - -void AlarmControlPanel::add_on_cleared_callback(std::function &&callback) { - this->cleared_callback_.add(std::move(callback)); -} - -void AlarmControlPanel::add_on_chime_callback(std::function &&callback) { - this->chime_callback_.add(std::move(callback)); -} - -void AlarmControlPanel::add_on_ready_callback(std::function &&callback) { - this->ready_callback_.add(std::move(callback)); +void AlarmControlPanel::notify_ready() { + for (auto *listener : this->listeners_) + listener->on_ready(); } void AlarmControlPanel::arm_away(optional code) { diff --git a/esphome/components/alarm_control_panel/alarm_control_panel.h b/esphome/components/alarm_control_panel/alarm_control_panel.h index 85c2b2148e..d9090e3c19 100644 --- a/esphome/components/alarm_control_panel/alarm_control_panel.h +++ b/esphome/components/alarm_control_panel/alarm_control_panel.h @@ -1,17 +1,32 @@ #pragma once -#include +#include #include "alarm_control_panel_call.h" #include "alarm_control_panel_state.h" -#include "esphome/core/automation.h" +#include "esphome/core/component.h" #include "esphome/core/entity_base.h" +#include "esphome/core/helpers.h" #include "esphome/core/log.h" +#include "esphome/core/preferences.h" namespace esphome { namespace alarm_control_panel { +/// Listener interface for alarm control panel events. +/// Implement this interface and register with add_listener() to receive notifications. +class AlarmControlPanelListener { + public: + virtual ~AlarmControlPanelListener() = default; + /// Called when state changes. Check new_state to filter specific states. + virtual void on_state(AlarmControlPanelState new_state, AlarmControlPanelState prev_state) {} + /// Called when a chime zone opens while disarmed. + virtual void on_chime() {} + /// Called when ready state changes. + virtual void on_ready() {} +}; + enum AlarmControlPanelFeature : uint8_t { // Matches Home Assistant values ACP_FEAT_ARM_HOME = 1 << 0, @@ -35,71 +50,19 @@ class AlarmControlPanel : public EntityBase { */ void publish_state(AlarmControlPanelState state); - /** Add a callback for when the state of the alarm_control_panel changes + /** Register a listener for alarm control panel events. * - * @param callback The callback function + * @param listener The listener to add (must remain valid for lifetime of panel) */ - void add_on_state_callback(std::function &&callback); + void add_listener(AlarmControlPanelListener *listener) { this->listeners_.push_back(listener); } - /** Add a callback for when the state of the alarm_control_panel chanes to triggered - * - * @param callback The callback function + /** Notify listeners of a chime event (zone opened while disarmed). */ - void add_on_triggered_callback(std::function &&callback); + void notify_chime(); - /** Add a callback for when the state of the alarm_control_panel chanes to arming - * - * @param callback The callback function + /** Notify listeners of a ready state change. */ - void add_on_arming_callback(std::function &&callback); - - /** Add a callback for when the state of the alarm_control_panel changes to pending - * - * @param callback The callback function - */ - void add_on_pending_callback(std::function &&callback); - - /** Add a callback for when the state of the alarm_control_panel changes to armed_home - * - * @param callback The callback function - */ - void add_on_armed_home_callback(std::function &&callback); - - /** Add a callback for when the state of the alarm_control_panel changes to armed_night - * - * @param callback The callback function - */ - void add_on_armed_night_callback(std::function &&callback); - - /** Add a callback for when the state of the alarm_control_panel changes to armed_away - * - * @param callback The callback function - */ - void add_on_armed_away_callback(std::function &&callback); - - /** Add a callback for when the state of the alarm_control_panel changes to disarmed - * - * @param callback The callback function - */ - void add_on_disarmed_callback(std::function &&callback); - - /** Add a callback for when the state of the alarm_control_panel clears from triggered - * - * @param callback The callback function - */ - void add_on_cleared_callback(std::function &&callback); - - /** Add a callback for when a chime zone goes from closed to open - * - * @param callback The callback function - */ - void add_on_chime_callback(std::function &&callback); - - /** Add a callback for when a ready state changes - * - * @param callback The callback function - */ - void add_on_ready_callback(std::function &&callback); + void notify_ready(); /** A numeric representation of the supported features as per HomeAssistant * @@ -172,28 +135,8 @@ class AlarmControlPanel : public EntityBase { uint32_t last_update_; // the call control function virtual void control(const AlarmControlPanelCall &call) = 0; - // state callback - CallbackManager state_callback_{}; - // trigger callback - CallbackManager triggered_callback_{}; - // arming callback - CallbackManager arming_callback_{}; - // pending callback - CallbackManager pending_callback_{}; - // armed_home callback - CallbackManager armed_home_callback_{}; - // armed_night callback - CallbackManager armed_night_callback_{}; - // armed_away callback - CallbackManager armed_away_callback_{}; - // disarmed callback - CallbackManager disarmed_callback_{}; - // clear callback - CallbackManager cleared_callback_{}; - // chime callback - CallbackManager chime_callback_{}; - // ready callback - CallbackManager ready_callback_{}; + // registered listeners + std::vector listeners_; }; } // namespace alarm_control_panel diff --git a/esphome/components/alarm_control_panel/automation.h b/esphome/components/alarm_control_panel/automation.h index db2ef78158..533f4892d1 100644 --- a/esphome/components/alarm_control_panel/automation.h +++ b/esphome/components/alarm_control_panel/automation.h @@ -6,81 +6,94 @@ namespace esphome { namespace alarm_control_panel { -class StateTrigger : public Trigger<> { +class StateTrigger final : public Trigger<>, public AlarmControlPanelListener { public: - explicit StateTrigger(AlarmControlPanel *alarm_control_panel) { - alarm_control_panel->add_on_state_callback([this]() { this->trigger(); }); + explicit StateTrigger(AlarmControlPanel *alarm_control_panel) { alarm_control_panel->add_listener(this); } + void on_state(AlarmControlPanelState new_state, AlarmControlPanelState prev_state) override { this->trigger(); } +}; + +class TriggeredTrigger final : public Trigger<>, public AlarmControlPanelListener { + public: + explicit TriggeredTrigger(AlarmControlPanel *alarm_control_panel) { alarm_control_panel->add_listener(this); } + void on_state(AlarmControlPanelState new_state, AlarmControlPanelState prev_state) override { + if (new_state == ACP_STATE_TRIGGERED) + this->trigger(); } }; -class TriggeredTrigger : public Trigger<> { +class ArmingTrigger final : public Trigger<>, public AlarmControlPanelListener { public: - explicit TriggeredTrigger(AlarmControlPanel *alarm_control_panel) { - alarm_control_panel->add_on_triggered_callback([this]() { this->trigger(); }); + explicit ArmingTrigger(AlarmControlPanel *alarm_control_panel) { alarm_control_panel->add_listener(this); } + void on_state(AlarmControlPanelState new_state, AlarmControlPanelState prev_state) override { + if (new_state == ACP_STATE_ARMING) + this->trigger(); } }; -class ArmingTrigger : public Trigger<> { +class PendingTrigger final : public Trigger<>, public AlarmControlPanelListener { public: - explicit ArmingTrigger(AlarmControlPanel *alarm_control_panel) { - alarm_control_panel->add_on_arming_callback([this]() { this->trigger(); }); + explicit PendingTrigger(AlarmControlPanel *alarm_control_panel) { alarm_control_panel->add_listener(this); } + void on_state(AlarmControlPanelState new_state, AlarmControlPanelState prev_state) override { + if (new_state == ACP_STATE_PENDING) + this->trigger(); } }; -class PendingTrigger : public Trigger<> { +class ArmedHomeTrigger final : public Trigger<>, public AlarmControlPanelListener { public: - explicit PendingTrigger(AlarmControlPanel *alarm_control_panel) { - alarm_control_panel->add_on_pending_callback([this]() { this->trigger(); }); + explicit ArmedHomeTrigger(AlarmControlPanel *alarm_control_panel) { alarm_control_panel->add_listener(this); } + void on_state(AlarmControlPanelState new_state, AlarmControlPanelState prev_state) override { + if (new_state == ACP_STATE_ARMED_HOME) + this->trigger(); } }; -class ArmedHomeTrigger : public Trigger<> { +class ArmedNightTrigger final : public Trigger<>, public AlarmControlPanelListener { public: - explicit ArmedHomeTrigger(AlarmControlPanel *alarm_control_panel) { - alarm_control_panel->add_on_armed_home_callback([this]() { this->trigger(); }); + explicit ArmedNightTrigger(AlarmControlPanel *alarm_control_panel) { alarm_control_panel->add_listener(this); } + void on_state(AlarmControlPanelState new_state, AlarmControlPanelState prev_state) override { + if (new_state == ACP_STATE_ARMED_NIGHT) + this->trigger(); } }; -class ArmedNightTrigger : public Trigger<> { +class ArmedAwayTrigger final : public Trigger<>, public AlarmControlPanelListener { public: - explicit ArmedNightTrigger(AlarmControlPanel *alarm_control_panel) { - alarm_control_panel->add_on_armed_night_callback([this]() { this->trigger(); }); + explicit ArmedAwayTrigger(AlarmControlPanel *alarm_control_panel) { alarm_control_panel->add_listener(this); } + void on_state(AlarmControlPanelState new_state, AlarmControlPanelState prev_state) override { + if (new_state == ACP_STATE_ARMED_AWAY) + this->trigger(); } }; -class ArmedAwayTrigger : public Trigger<> { +class DisarmedTrigger final : public Trigger<>, public AlarmControlPanelListener { public: - explicit ArmedAwayTrigger(AlarmControlPanel *alarm_control_panel) { - alarm_control_panel->add_on_armed_away_callback([this]() { this->trigger(); }); + explicit DisarmedTrigger(AlarmControlPanel *alarm_control_panel) { alarm_control_panel->add_listener(this); } + void on_state(AlarmControlPanelState new_state, AlarmControlPanelState prev_state) override { + if (new_state == ACP_STATE_DISARMED) + this->trigger(); } }; -class DisarmedTrigger : public Trigger<> { +class ClearedTrigger final : public Trigger<>, public AlarmControlPanelListener { public: - explicit DisarmedTrigger(AlarmControlPanel *alarm_control_panel) { - alarm_control_panel->add_on_disarmed_callback([this]() { this->trigger(); }); + explicit ClearedTrigger(AlarmControlPanel *alarm_control_panel) { alarm_control_panel->add_listener(this); } + void on_state(AlarmControlPanelState new_state, AlarmControlPanelState prev_state) override { + if (prev_state == ACP_STATE_TRIGGERED) + this->trigger(); } }; -class ClearedTrigger : public Trigger<> { +class ChimeTrigger final : public Trigger<>, public AlarmControlPanelListener { public: - explicit ClearedTrigger(AlarmControlPanel *alarm_control_panel) { - alarm_control_panel->add_on_cleared_callback([this]() { this->trigger(); }); - } + explicit ChimeTrigger(AlarmControlPanel *alarm_control_panel) { alarm_control_panel->add_listener(this); } + void on_chime() override { this->trigger(); } }; -class ChimeTrigger : public Trigger<> { +class ReadyTrigger final : public Trigger<>, public AlarmControlPanelListener { public: - explicit ChimeTrigger(AlarmControlPanel *alarm_control_panel) { - alarm_control_panel->add_on_chime_callback([this]() { this->trigger(); }); - } -}; - -class ReadyTrigger : public Trigger<> { - public: - explicit ReadyTrigger(AlarmControlPanel *alarm_control_panel) { - alarm_control_panel->add_on_ready_callback([this]() { this->trigger(); }); - } + explicit ReadyTrigger(AlarmControlPanel *alarm_control_panel) { alarm_control_panel->add_listener(this); } + void on_ready() override { this->trigger(); } }; template class ArmAwayAction : public Action { diff --git a/esphome/components/mqtt/mqtt_alarm_control_panel.cpp b/esphome/components/mqtt/mqtt_alarm_control_panel.cpp index dd3df5f8aa..c96d696862 100644 --- a/esphome/components/mqtt/mqtt_alarm_control_panel.cpp +++ b/esphome/components/mqtt/mqtt_alarm_control_panel.cpp @@ -16,7 +16,7 @@ using namespace esphome::alarm_control_panel; MQTTAlarmControlPanelComponent::MQTTAlarmControlPanelComponent(AlarmControlPanel *alarm_control_panel) : alarm_control_panel_(alarm_control_panel) {} void MQTTAlarmControlPanelComponent::setup() { - this->alarm_control_panel_->add_on_state_callback([this]() { this->publish_state(); }); + this->alarm_control_panel_->add_listener(this); this->subscribe(this->get_command_topic_(), [this](const std::string &topic, const std::string &payload) { auto call = this->alarm_control_panel_->make_call(); if (strcasecmp(payload.c_str(), "ARM_AWAY") == 0) { diff --git a/esphome/components/mqtt/mqtt_alarm_control_panel.h b/esphome/components/mqtt/mqtt_alarm_control_panel.h index 4ad37b7314..47d8610d0b 100644 --- a/esphome/components/mqtt/mqtt_alarm_control_panel.h +++ b/esphome/components/mqtt/mqtt_alarm_control_panel.h @@ -11,7 +11,8 @@ namespace esphome { namespace mqtt { -class MQTTAlarmControlPanelComponent : public mqtt::MQTTComponent { +class MQTTAlarmControlPanelComponent final : public mqtt::MQTTComponent, + public alarm_control_panel::AlarmControlPanelListener { public: explicit MQTTAlarmControlPanelComponent(alarm_control_panel::AlarmControlPanel *alarm_control_panel); @@ -25,6 +26,12 @@ class MQTTAlarmControlPanelComponent : public mqtt::MQTTComponent { void dump_config() override; + // AlarmControlPanelListener interface + void on_state(alarm_control_panel::AlarmControlPanelState new_state, + alarm_control_panel::AlarmControlPanelState prev_state) override { + this->publish_state(); + } + protected: std::string component_type() const override; const EntityBase *get_entity() const override; diff --git a/esphome/components/template/alarm_control_panel/template_alarm_control_panel.cpp b/esphome/components/template/alarm_control_panel/template_alarm_control_panel.cpp index f025435261..003acb06ff 100644 --- a/esphome/components/template/alarm_control_panel/template_alarm_control_panel.cpp +++ b/esphome/components/template/alarm_control_panel/template_alarm_control_panel.cpp @@ -133,7 +133,7 @@ void TemplateAlarmControlPanel::loop() { if ((!this->sensor_data_[info.store_index].last_chime_state) && (sensor->state)) { // Must be disarmed to chime if (this->current_state_ == ACP_STATE_DISARMED) { - this->chime_callback_.call(); + this->notify_chime(); } } // Record the sensor state change @@ -182,7 +182,7 @@ void TemplateAlarmControlPanel::loop() { // Call the ready state change callback if there was a change if (this->sensors_ready_ != sensors_ready) { this->sensors_ready_ = sensors_ready; - this->ready_callback_.call(); + this->notify_ready(); } #endif From bc50be6053493aa453ab68602be89c2c4c55ecc6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 28 Nov 2025 16:14:00 -0600 Subject: [PATCH 382/415] [logger] Conditionally compile log level change listener (#12168) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- esphome/components/logger/__init__.py | 23 +++++++++++++ esphome/components/logger/logger.cpp | 5 ++- esphome/components/logger/logger.h | 34 +++++++++++++++++-- esphome/components/logger/select/__init__.py | 9 ++++- .../logger/select/logger_level_select.cpp | 6 ++-- .../logger/select/logger_level_select.h | 9 +++-- esphome/core/defines.h | 1 + 7 files changed, 77 insertions(+), 10 deletions(-) diff --git a/esphome/components/logger/__init__.py b/esphome/components/logger/__init__.py index 39877030e9..d9ca44d3c9 100644 --- a/esphome/components/logger/__init__.py +++ b/esphome/components/logger/__init__.py @@ -406,6 +406,8 @@ async def to_code(config): conf, ) + CORE.add_job(final_step) + def validate_printf(value): # https://stackoverflow.com/questions/30011379/how-can-i-parse-a-c-format-string-in-python @@ -506,3 +508,24 @@ FILTER_SOURCE_FILES = filter_source_files_from_platform( }, } ) + +# Keys for CORE.data storage +DOMAIN = "logger" +KEY_LEVEL_LISTENERS = "level_listeners" + + +def request_logger_level_listeners() -> None: + """Request that logger level listeners be compiled in. + + Components that need to be notified about log level changes should call this + function during their code generation. This enables the add_level_listener() + method and compiles in the listener vector. + """ + CORE.data.setdefault(DOMAIN, {})[KEY_LEVEL_LISTENERS] = True + + +@coroutine_with_priority(CoroPriority.FINAL) +async def final_step(): + """Final code generation step to configure optional logger features.""" + if CORE.data.get(DOMAIN, {}).get(KEY_LEVEL_LISTENERS, False): + cg.add_define("USE_LOGGER_LEVEL_LISTENERS") diff --git a/esphome/components/logger/logger.cpp b/esphome/components/logger/logger.cpp index f925e85e11..21e2b44808 100644 --- a/esphome/components/logger/logger.cpp +++ b/esphome/components/logger/logger.cpp @@ -288,7 +288,10 @@ void Logger::set_log_level(uint8_t level) { ESP_LOGW(TAG, "Cannot set log level higher than pre-compiled %s", LOG_STR_ARG(LOG_LEVELS[ESPHOME_LOG_LEVEL])); } this->current_level_ = level; - this->level_callback_.call(level); +#ifdef USE_LOGGER_LEVEL_LISTENERS + for (auto *listener : this->level_listeners_) + listener->on_log_level_change(level); +#endif } Logger *global_logger = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) diff --git a/esphome/components/logger/logger.h b/esphome/components/logger/logger.h index a0024411d7..8abc1196e1 100644 --- a/esphome/components/logger/logger.h +++ b/esphome/components/logger/logger.h @@ -58,6 +58,30 @@ class LogListener { virtual void on_log(uint8_t level, const char *tag, const char *message, size_t message_len) = 0; }; +#ifdef USE_LOGGER_LEVEL_LISTENERS +/** Interface for receiving log level changes without std::function overhead. + * + * Components can implement this interface instead of using lambdas with std::function + * to reduce flash usage from std::function type erasure machinery. + * + * Usage: + * class MyComponent : public Component, public LoggerLevelListener { + * public: + * void setup() override { + * if (logger::global_logger != nullptr) + * logger::global_logger->add_logger_level_listener(this); + * } + * void on_log_level_change(uint8_t level) override { + * // Handle log level change + * } + * }; + */ +class LoggerLevelListener { + public: + virtual void on_log_level_change(uint8_t level) = 0; +}; +#endif + #ifdef USE_LOGGER_RUNTIME_TAG_LEVELS // Comparison function for const char* keys in log_levels_ map struct CStrCompare { @@ -193,8 +217,10 @@ class Logger : public Component { /// Register a log listener to receive log messages void add_log_listener(LogListener *listener) { this->log_listeners_.push_back(listener); } - // add a listener for log level changes - void add_listener(std::function &&callback) { this->level_callback_.add(std::move(callback)); } +#ifdef USE_LOGGER_LEVEL_LISTENERS + /// Register a listener for log level changes + void add_level_listener(LoggerLevelListener *listener) { this->level_listeners_.push_back(listener); } +#endif float get_setup_priority() const override; @@ -325,7 +351,9 @@ class Logger : public Component { std::map log_levels_{}; #endif std::vector log_listeners_; // Log message listeners (API, MQTT, syslog, etc.) - CallbackManager level_callback_{}; +#ifdef USE_LOGGER_LEVEL_LISTENERS + std::vector level_listeners_; // Log level change listeners +#endif #ifdef USE_ESPHOME_TASK_LOG_BUFFER std::unique_ptr log_buffer_; // Will be initialized with init_log_buffer #endif diff --git a/esphome/components/logger/select/__init__.py b/esphome/components/logger/select/__init__.py index 2e83599eb4..6ce663978e 100644 --- a/esphome/components/logger/select/__init__.py +++ b/esphome/components/logger/select/__init__.py @@ -5,7 +5,13 @@ from esphome.const import CONF_LEVEL, CONF_LOGGER, ENTITY_CATEGORY_CONFIG, ICON_ from esphome.core import CORE from esphome.cpp_helpers import register_component, register_parented -from .. import CONF_LOGGER_ID, LOG_LEVELS, Logger, logger_ns +from .. import ( + CONF_LOGGER_ID, + LOG_LEVELS, + Logger, + logger_ns, + request_logger_level_listeners, +) CODEOWNERS = ["@clydebarrow"] @@ -21,6 +27,7 @@ CONFIG_SCHEMA = select.select_schema( async def to_code(config): + request_logger_level_listeners() parent = await cg.get_variable(config[CONF_LOGGER_ID]) levels = list(LOG_LEVELS) index = levels.index(CORE.data[CONF_LOGGER][CONF_LEVEL]) diff --git a/esphome/components/logger/select/logger_level_select.cpp b/esphome/components/logger/select/logger_level_select.cpp index e2ec28a390..3091ca1851 100644 --- a/esphome/components/logger/select/logger_level_select.cpp +++ b/esphome/components/logger/select/logger_level_select.cpp @@ -2,7 +2,7 @@ namespace esphome::logger { -void LoggerLevelSelect::publish_state(int level) { +void LoggerLevelSelect::on_log_level_change(uint8_t level) { auto index = level_to_index(level); if (!this->has_index(index)) return; @@ -10,8 +10,8 @@ void LoggerLevelSelect::publish_state(int level) { } void LoggerLevelSelect::setup() { - this->parent_->add_listener([this](int level) { this->publish_state(level); }); - this->publish_state(this->parent_->get_log_level()); + this->parent_->add_level_listener(this); + this->on_log_level_change(this->parent_->get_log_level()); } void LoggerLevelSelect::control(size_t index) { this->parent_->set_log_level(index_to_level(index)); } diff --git a/esphome/components/logger/select/logger_level_select.h b/esphome/components/logger/select/logger_level_select.h index 950edd29ac..6482114943 100644 --- a/esphome/components/logger/select/logger_level_select.h +++ b/esphome/components/logger/select/logger_level_select.h @@ -5,12 +5,17 @@ #include "esphome/components/logger/logger.h" namespace esphome::logger { -class LoggerLevelSelect : public Component, public select::Select, public Parented { +class LoggerLevelSelect final : public Component, + public select::Select, + public Parented, + public LoggerLevelListener { public: - void publish_state(int level); void setup() override; void control(size_t index) override; + // LoggerLevelListener interface + void on_log_level_change(uint8_t level) override; + protected: // Convert log level to option index (skip CONFIG at level 4) static uint8_t level_to_index(uint8_t level) { return (level > ESPHOME_LOG_LEVEL_CONFIG) ? level - 1 : level; } diff --git a/esphome/core/defines.h b/esphome/core/defines.h index f4026aad96..538d4e3d6e 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -51,6 +51,7 @@ #define USE_LIGHT #define USE_LOCK #define USE_LOGGER +#define USE_LOGGER_LEVEL_LISTENERS #define USE_LOGGER_RUNTIME_TAG_LEVELS #define USE_LVGL #define USE_LVGL_ANIMIMG From 3c1c19da1c572f3ad60efc272513045cdc99c732 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 28 Nov 2025 16:23:31 -0600 Subject: [PATCH 383/415] tweaks --- .../alarm_control_panel.cpp | 6 +- .../alarm_control_panel/alarm_control_panel.h | 35 +++++--- .../alarm_control_panel/automation.h | 81 +++++-------------- .../mqtt/mqtt_alarm_control_panel.h | 4 +- 4 files changed, 50 insertions(+), 76 deletions(-) diff --git a/esphome/components/alarm_control_panel/alarm_control_panel.cpp b/esphome/components/alarm_control_panel/alarm_control_panel.cpp index 585401028a..733d255158 100644 --- a/esphome/components/alarm_control_panel/alarm_control_panel.cpp +++ b/esphome/components/alarm_control_panel/alarm_control_panel.cpp @@ -36,7 +36,7 @@ void AlarmControlPanel::publish_state(AlarmControlPanelState state) { LOG_STR_ARG(alarm_control_panel_state_to_string(prev_state))); this->current_state_ = state; - for (auto *listener : this->listeners_) { + for (auto *listener : this->state_listeners_) { listener->on_state(state, prev_state); } @@ -52,12 +52,12 @@ void AlarmControlPanel::publish_state(AlarmControlPanelState state) { } void AlarmControlPanel::notify_chime() { - for (auto *listener : this->listeners_) + for (auto *listener : this->event_listeners_) listener->on_chime(); } void AlarmControlPanel::notify_ready() { - for (auto *listener : this->listeners_) + for (auto *listener : this->event_listeners_) listener->on_ready(); } diff --git a/esphome/components/alarm_control_panel/alarm_control_panel.h b/esphome/components/alarm_control_panel/alarm_control_panel.h index d9090e3c19..80d3906aa3 100644 --- a/esphome/components/alarm_control_panel/alarm_control_panel.h +++ b/esphome/components/alarm_control_panel/alarm_control_panel.h @@ -14,16 +14,21 @@ namespace esphome { namespace alarm_control_panel { -/// Listener interface for alarm control panel events. -/// Implement this interface and register with add_listener() to receive notifications. -class AlarmControlPanelListener { +/// Listener interface for alarm control panel state changes. +class AlarmControlPanelStateListener { public: - virtual ~AlarmControlPanelListener() = default; - /// Called when state changes. Check new_state to filter specific states. - virtual void on_state(AlarmControlPanelState new_state, AlarmControlPanelState prev_state) {} + virtual ~AlarmControlPanelStateListener() = default; + /// Called when state changes. Check new_state/prev_state to filter specific states. + virtual void on_state(AlarmControlPanelState new_state, AlarmControlPanelState prev_state) = 0; +}; + +/// Listener interface for alarm events (chime, ready, etc). +class AlarmControlPanelEventListener { + public: + virtual ~AlarmControlPanelEventListener() = default; /// Called when a chime zone opens while disarmed. virtual void on_chime() {} - /// Called when ready state changes. + /// Called when zones ready state changes. virtual void on_ready() {} }; @@ -50,11 +55,17 @@ class AlarmControlPanel : public EntityBase { */ void publish_state(AlarmControlPanelState state); - /** Register a listener for alarm control panel events. + /** Register a listener for state changes. * * @param listener The listener to add (must remain valid for lifetime of panel) */ - void add_listener(AlarmControlPanelListener *listener) { this->listeners_.push_back(listener); } + void add_listener(AlarmControlPanelStateListener *listener) { this->state_listeners_.push_back(listener); } + + /** Register a listener for alarm events (chime/ready/etc). + * + * @param listener The listener to add (must remain valid for lifetime of panel) + */ + void add_listener(AlarmControlPanelEventListener *listener) { this->event_listeners_.push_back(listener); } /** Notify listeners of a chime event (zone opened while disarmed). */ @@ -135,8 +146,10 @@ class AlarmControlPanel : public EntityBase { uint32_t last_update_; // the call control function virtual void control(const AlarmControlPanelCall &call) = 0; - // registered listeners - std::vector listeners_; + // registered state listeners + std::vector state_listeners_; + // registered event listeners (chime/ready/etc) + std::vector event_listeners_; }; } // namespace alarm_control_panel diff --git a/esphome/components/alarm_control_panel/automation.h b/esphome/components/alarm_control_panel/automation.h index 533f4892d1..19a3662e83 100644 --- a/esphome/components/alarm_control_panel/automation.h +++ b/esphome/components/alarm_control_panel/automation.h @@ -6,76 +6,35 @@ namespace esphome { namespace alarm_control_panel { -class StateTrigger final : public Trigger<>, public AlarmControlPanelListener { +/// Trigger on any state change +class StateTrigger final : public Trigger<>, public AlarmControlPanelStateListener { public: explicit StateTrigger(AlarmControlPanel *alarm_control_panel) { alarm_control_panel->add_listener(this); } void on_state(AlarmControlPanelState new_state, AlarmControlPanelState prev_state) override { this->trigger(); } }; -class TriggeredTrigger final : public Trigger<>, public AlarmControlPanelListener { +/// Template trigger that fires when entering a specific state +template +class StateEnterTrigger final : public Trigger<>, public AlarmControlPanelStateListener { public: - explicit TriggeredTrigger(AlarmControlPanel *alarm_control_panel) { alarm_control_panel->add_listener(this); } + explicit StateEnterTrigger(AlarmControlPanel *alarm_control_panel) { alarm_control_panel->add_listener(this); } void on_state(AlarmControlPanelState new_state, AlarmControlPanelState prev_state) override { - if (new_state == ACP_STATE_TRIGGERED) + if (new_state == State) this->trigger(); } }; -class ArmingTrigger final : public Trigger<>, public AlarmControlPanelListener { - public: - explicit ArmingTrigger(AlarmControlPanel *alarm_control_panel) { alarm_control_panel->add_listener(this); } - void on_state(AlarmControlPanelState new_state, AlarmControlPanelState prev_state) override { - if (new_state == ACP_STATE_ARMING) - this->trigger(); - } -}; +// Type aliases for state-specific triggers +using TriggeredTrigger = StateEnterTrigger; +using ArmingTrigger = StateEnterTrigger; +using PendingTrigger = StateEnterTrigger; +using ArmedHomeTrigger = StateEnterTrigger; +using ArmedNightTrigger = StateEnterTrigger; +using ArmedAwayTrigger = StateEnterTrigger; +using DisarmedTrigger = StateEnterTrigger; -class PendingTrigger final : public Trigger<>, public AlarmControlPanelListener { - public: - explicit PendingTrigger(AlarmControlPanel *alarm_control_panel) { alarm_control_panel->add_listener(this); } - void on_state(AlarmControlPanelState new_state, AlarmControlPanelState prev_state) override { - if (new_state == ACP_STATE_PENDING) - this->trigger(); - } -}; - -class ArmedHomeTrigger final : public Trigger<>, public AlarmControlPanelListener { - public: - explicit ArmedHomeTrigger(AlarmControlPanel *alarm_control_panel) { alarm_control_panel->add_listener(this); } - void on_state(AlarmControlPanelState new_state, AlarmControlPanelState prev_state) override { - if (new_state == ACP_STATE_ARMED_HOME) - this->trigger(); - } -}; - -class ArmedNightTrigger final : public Trigger<>, public AlarmControlPanelListener { - public: - explicit ArmedNightTrigger(AlarmControlPanel *alarm_control_panel) { alarm_control_panel->add_listener(this); } - void on_state(AlarmControlPanelState new_state, AlarmControlPanelState prev_state) override { - if (new_state == ACP_STATE_ARMED_NIGHT) - this->trigger(); - } -}; - -class ArmedAwayTrigger final : public Trigger<>, public AlarmControlPanelListener { - public: - explicit ArmedAwayTrigger(AlarmControlPanel *alarm_control_panel) { alarm_control_panel->add_listener(this); } - void on_state(AlarmControlPanelState new_state, AlarmControlPanelState prev_state) override { - if (new_state == ACP_STATE_ARMED_AWAY) - this->trigger(); - } -}; - -class DisarmedTrigger final : public Trigger<>, public AlarmControlPanelListener { - public: - explicit DisarmedTrigger(AlarmControlPanel *alarm_control_panel) { alarm_control_panel->add_listener(this); } - void on_state(AlarmControlPanelState new_state, AlarmControlPanelState prev_state) override { - if (new_state == ACP_STATE_DISARMED) - this->trigger(); - } -}; - -class ClearedTrigger final : public Trigger<>, public AlarmControlPanelListener { +/// Trigger when leaving TRIGGERED state (alarm cleared) +class ClearedTrigger final : public Trigger<>, public AlarmControlPanelStateListener { public: explicit ClearedTrigger(AlarmControlPanel *alarm_control_panel) { alarm_control_panel->add_listener(this); } void on_state(AlarmControlPanelState new_state, AlarmControlPanelState prev_state) override { @@ -84,13 +43,15 @@ class ClearedTrigger final : public Trigger<>, public AlarmControlPanelListener } }; -class ChimeTrigger final : public Trigger<>, public AlarmControlPanelListener { +/// Trigger on chime event (zone opened while disarmed) +class ChimeTrigger final : public Trigger<>, public AlarmControlPanelEventListener { public: explicit ChimeTrigger(AlarmControlPanel *alarm_control_panel) { alarm_control_panel->add_listener(this); } void on_chime() override { this->trigger(); } }; -class ReadyTrigger final : public Trigger<>, public AlarmControlPanelListener { +/// Trigger on ready state change +class ReadyTrigger final : public Trigger<>, public AlarmControlPanelEventListener { public: explicit ReadyTrigger(AlarmControlPanel *alarm_control_panel) { alarm_control_panel->add_listener(this); } void on_ready() override { this->trigger(); } diff --git a/esphome/components/mqtt/mqtt_alarm_control_panel.h b/esphome/components/mqtt/mqtt_alarm_control_panel.h index 47d8610d0b..c64b8c575d 100644 --- a/esphome/components/mqtt/mqtt_alarm_control_panel.h +++ b/esphome/components/mqtt/mqtt_alarm_control_panel.h @@ -12,7 +12,7 @@ namespace esphome { namespace mqtt { class MQTTAlarmControlPanelComponent final : public mqtt::MQTTComponent, - public alarm_control_panel::AlarmControlPanelListener { + public alarm_control_panel::AlarmControlPanelStateListener { public: explicit MQTTAlarmControlPanelComponent(alarm_control_panel::AlarmControlPanel *alarm_control_panel); @@ -26,7 +26,7 @@ class MQTTAlarmControlPanelComponent final : public mqtt::MQTTComponent, void dump_config() override; - // AlarmControlPanelListener interface + // AlarmControlPanelStateListener interface void on_state(alarm_control_panel::AlarmControlPanelState new_state, alarm_control_panel::AlarmControlPanelState prev_state) override { this->publish_state(); From 2060ed0a92f59cfe58b3daa374cfac068491ae93 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 28 Nov 2025 16:32:28 -0600 Subject: [PATCH 384/415] tests --- ...alarm_control_panel_state_transitions.yaml | 106 ++++++ ...t_alarm_control_panel_state_transitions.py | 310 ++++++++++++++++++ 2 files changed, 416 insertions(+) create mode 100644 tests/integration/fixtures/alarm_control_panel_state_transitions.yaml create mode 100644 tests/integration/test_alarm_control_panel_state_transitions.py diff --git a/tests/integration/fixtures/alarm_control_panel_state_transitions.yaml b/tests/integration/fixtures/alarm_control_panel_state_transitions.yaml new file mode 100644 index 0000000000..1edb401a0d --- /dev/null +++ b/tests/integration/fixtures/alarm_control_panel_state_transitions.yaml @@ -0,0 +1,106 @@ +esphome: + name: alarm-state-transitions + friendly_name: "Alarm Control Panel State Transitions Test" + +logger: + +host: + +globals: + - id: door_sensor_state + type: bool + initial_value: "false" + - id: chime_sensor_state + type: bool + initial_value: "false" + +switch: + # Switch to control the door sensor state + - platform: template + id: door_sensor_switch + name: "Door Sensor Switch" + optimistic: true + turn_on_action: + - globals.set: + id: door_sensor_state + value: "true" + turn_off_action: + - globals.set: + id: door_sensor_state + value: "false" + # Switch to control the chime sensor state + - platform: template + id: chime_sensor_switch + name: "Chime Sensor Switch" + optimistic: true + turn_on_action: + - globals.set: + id: chime_sensor_state + value: "true" + turn_off_action: + - globals.set: + id: chime_sensor_state + value: "false" + +binary_sensor: + - platform: template + id: door_sensor + name: "Door Sensor" + lambda: |- + return id(door_sensor_state); + - platform: template + id: chime_sensor + name: "Chime Sensor" + lambda: |- + return id(chime_sensor_state); + +alarm_control_panel: + - platform: template + id: test_alarm + name: "Test Alarm" + codes: + - "1234" + requires_code_to_arm: true + # Short timeouts for faster testing + arming_away_time: 50ms + arming_home_time: 50ms + arming_night_time: 50ms + pending_time: 50ms + trigger_time: 100ms + restore_mode: ALWAYS_DISARMED + binary_sensors: + - input: door_sensor + bypass_armed_home: false + bypass_armed_night: false + chime: false + trigger_mode: DELAYED + - input: chime_sensor + bypass_armed_home: true + bypass_armed_night: true + chime: true + trigger_mode: DELAYED + on_state: + - logger.log: "State changed" + on_disarmed: + - logger.log: "Alarm disarmed" + on_arming: + - logger.log: "Alarm arming" + on_armed_away: + - logger.log: "Alarm armed away" + on_armed_home: + - logger.log: "Alarm armed home" + on_armed_night: + - logger.log: "Alarm armed night" + on_pending: + - logger.log: "Alarm pending" + on_triggered: + - logger.log: "Alarm triggered" + on_cleared: + - logger.log: "Alarm cleared" + on_chime: + - logger.log: "Chime activated" + on_ready: + - logger.log: "Sensors ready state changed" + +api: + batch_delay: 0ms diff --git a/tests/integration/test_alarm_control_panel_state_transitions.py b/tests/integration/test_alarm_control_panel_state_transitions.py new file mode 100644 index 0000000000..f4521762df --- /dev/null +++ b/tests/integration/test_alarm_control_panel_state_transitions.py @@ -0,0 +1,310 @@ +"""Integration test for alarm control panel state transitions.""" + +from __future__ import annotations + +import asyncio +import re + +import aioesphomeapi +from aioesphomeapi import ( + AlarmControlPanelCommand, + AlarmControlPanelEntityState, + AlarmControlPanelInfo, + AlarmControlPanelState, + SwitchInfo, +) +import pytest + +from .types import APIClientConnectedFactory, RunCompiledFunction + + +@pytest.mark.asyncio +async def test_alarm_control_panel_state_transitions( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """Test alarm control panel state transitions. + + This comprehensive test verifies all state transitions and listener callbacks: + + 1. Basic arm/disarm sequences: + - DISARMED -> ARMING -> ARMED_AWAY -> DISARMED + - DISARMED -> ARMING -> ARMED_HOME -> DISARMED + - DISARMED -> ARMING -> ARMED_NIGHT -> DISARMED + + 2. Wrong code rejection + + 3. Sensor triggering while armed: + - ARMED_AWAY -> PENDING -> TRIGGERED (delayed sensor) + - TRIGGERED -> ARMED_AWAY (auto-reset after trigger_time, fires on_cleared) + + 4. Chime functionality: + - Sensor open while DISARMED triggers on_chime + + 5. Ready state: + - Sensor state changes trigger on_ready + """ + loop = asyncio.get_running_loop() + + # Track log messages for callback verification + log_lines: list[str] = [] + chime_future: asyncio.Future[bool] = loop.create_future() + ready_futures: list[asyncio.Future[bool]] = [] + cleared_future: asyncio.Future[bool] = loop.create_future() + + # Patterns to match log output from callbacks + chime_pattern = re.compile(r"Chime activated") + ready_pattern = re.compile(r"Sensors ready state changed") + cleared_pattern = re.compile(r"Alarm cleared") + + def on_log_line(line: str) -> None: + log_lines.append(line) + if not chime_future.done() and chime_pattern.search(line): + chime_future.set_result(True) + if ready_pattern.search(line): + # Create new future for each ready event + for fut in ready_futures: + if not fut.done(): + fut.set_result(True) + break + if not cleared_future.done() and cleared_pattern.search(line): + cleared_future.set_result(True) + + async with ( + run_compiled(yaml_config, line_callback=on_log_line), + api_client_connected() as client, + ): + entities, _ = await client.list_entities_services() + + # Find entities + alarm_info: AlarmControlPanelInfo | None = None + door_switch_info: SwitchInfo | None = None + chime_switch_info: SwitchInfo | None = None + + for entity in entities: + if isinstance(entity, AlarmControlPanelInfo): + alarm_info = entity + elif isinstance(entity, SwitchInfo): + if entity.name == "Door Sensor Switch": + door_switch_info = entity + elif entity.name == "Chime Sensor Switch": + chime_switch_info = entity + + assert alarm_info is not None, "Alarm control panel not found" + assert door_switch_info is not None, "Door sensor switch not found" + assert chime_switch_info is not None, "Chime sensor switch not found" + + # Track state changes + states_received: list[AlarmControlPanelState] = [] + state_event = asyncio.Event() + + def on_state(state: aioesphomeapi.EntityState) -> None: + if ( + isinstance(state, AlarmControlPanelEntityState) + and state.key == alarm_info.key + ): + states_received.append(state.state) + state_event.set() + + client.subscribe_states(on_state) + + # Helper to wait for specific state + async def wait_for_state( + expected: AlarmControlPanelState, timeout: float = 5.0 + ) -> None: + deadline = loop.time() + timeout + while True: + remaining = deadline - loop.time() + if remaining <= 0: + raise TimeoutError( + f"Timeout waiting for state {expected}, " + f"last state: {states_received[-1] if states_received else 'none'}" + ) + await asyncio.wait_for(state_event.wait(), timeout=remaining) + state_event.clear() + if states_received[-1] == expected: + return + + # Wait for initial DISARMED state + await wait_for_state(AlarmControlPanelState.DISARMED) + + # ===== Test wrong code rejection ===== + client.alarm_control_panel_command( + alarm_info.key, + AlarmControlPanelCommand.ARM_AWAY, + code="0000", # Wrong code + ) + + # Should NOT transition - wait a bit and verify still disarmed + with pytest.raises(asyncio.TimeoutError): + await asyncio.wait_for(state_event.wait(), timeout=0.5) + assert states_received[-1] == AlarmControlPanelState.DISARMED + + # ===== Test ARM_AWAY sequence ===== + client.alarm_control_panel_command( + alarm_info.key, + AlarmControlPanelCommand.ARM_AWAY, + code="1234", + ) + await wait_for_state(AlarmControlPanelState.ARMING) + await wait_for_state(AlarmControlPanelState.ARMED_AWAY) + + # Disarm + client.alarm_control_panel_command( + alarm_info.key, + AlarmControlPanelCommand.DISARM, + code="1234", + ) + await wait_for_state(AlarmControlPanelState.DISARMED) + + # ===== Test ARM_HOME sequence ===== + client.alarm_control_panel_command( + alarm_info.key, + AlarmControlPanelCommand.ARM_HOME, + code="1234", + ) + await wait_for_state(AlarmControlPanelState.ARMING) + await wait_for_state(AlarmControlPanelState.ARMED_HOME) + + # Disarm + client.alarm_control_panel_command( + alarm_info.key, + AlarmControlPanelCommand.DISARM, + code="1234", + ) + await wait_for_state(AlarmControlPanelState.DISARMED) + + # ===== Test ARM_NIGHT sequence ===== + client.alarm_control_panel_command( + alarm_info.key, + AlarmControlPanelCommand.ARM_NIGHT, + code="1234", + ) + await wait_for_state(AlarmControlPanelState.ARMING) + await wait_for_state(AlarmControlPanelState.ARMED_NIGHT) + + # Disarm + client.alarm_control_panel_command( + alarm_info.key, + AlarmControlPanelCommand.DISARM, + code="1234", + ) + await wait_for_state(AlarmControlPanelState.DISARMED) + + # Verify basic state sequence + expected_states = [ + AlarmControlPanelState.DISARMED, # Initial + AlarmControlPanelState.ARMING, # Arm away + AlarmControlPanelState.ARMED_AWAY, + AlarmControlPanelState.DISARMED, + AlarmControlPanelState.ARMING, # Arm home + AlarmControlPanelState.ARMED_HOME, + AlarmControlPanelState.DISARMED, + AlarmControlPanelState.ARMING, # Arm night + AlarmControlPanelState.ARMED_NIGHT, + AlarmControlPanelState.DISARMED, + ] + assert states_received == expected_states, ( + f"State sequence mismatch.\nExpected: {expected_states}\n" + f"Got: {states_received}" + ) + + # ===== Test PENDING -> TRIGGERED -> CLEARED sequence ===== + # This tests on_pending, on_triggered, and on_cleared callbacks + + # Arm away first + client.alarm_control_panel_command( + alarm_info.key, + AlarmControlPanelCommand.ARM_AWAY, + code="1234", + ) + await wait_for_state(AlarmControlPanelState.ARMING) + await wait_for_state(AlarmControlPanelState.ARMED_AWAY) + + # Trip the door sensor (delayed mode triggers PENDING first) + client.switch_command(door_switch_info.key, True) + + # Should go to PENDING (delayed sensor) + await wait_for_state(AlarmControlPanelState.PENDING) + + # Should go to TRIGGERED after pending_time (100ms) + await wait_for_state(AlarmControlPanelState.TRIGGERED) + + # Close the sensor + client.switch_command(door_switch_info.key, False) + + # Wait for trigger_time to expire and auto-reset (500ms) + # The alarm should go back to ARMED_AWAY after trigger_time + # This transition FROM TRIGGERED fires on_cleared + await wait_for_state(AlarmControlPanelState.ARMED_AWAY, timeout=2.0) + + # Verify on_cleared was logged + try: + await asyncio.wait_for(cleared_future, timeout=1.0) + except TimeoutError: + pytest.fail(f"on_cleared callback not fired. Log lines: {log_lines[-20:]}") + + # Disarm + client.alarm_control_panel_command( + alarm_info.key, + AlarmControlPanelCommand.DISARM, + code="1234", + ) + await wait_for_state(AlarmControlPanelState.DISARMED) + + # Verify trigger sequence was added + assert AlarmControlPanelState.PENDING in states_received + assert AlarmControlPanelState.TRIGGERED in states_received + + # ===== Test chime (sensor open while disarmed) ===== + # The chime_sensor has chime: true, so opening it while disarmed + # should trigger on_chime callback + + # We're currently DISARMED - open the chime sensor + client.switch_command(chime_switch_info.key, True) + + # Wait for chime callback to be logged + try: + await asyncio.wait_for(chime_future, timeout=2.0) + except TimeoutError: + pytest.fail(f"on_chime callback not fired. Log lines: {log_lines[-20:]}") + + # Close the chime sensor + client.switch_command(chime_switch_info.key, False) + + # ===== Test ready state changes ===== + # Opening/closing sensors while disarmed affects ready state + # The on_ready callback fires when sensors_ready changes + + # Set up futures for ready state changes + ready_future_1: asyncio.Future[bool] = loop.create_future() + ready_future_2: asyncio.Future[bool] = loop.create_future() + ready_futures.extend([ready_future_1, ready_future_2]) + + # Open door sensor (makes alarm not ready) + client.switch_command(door_switch_info.key, True) + + # Wait for first on_ready callback (not ready) + try: + await asyncio.wait_for(ready_future_1, timeout=2.0) + except TimeoutError: + pytest.fail( + f"on_ready callback not fired when sensor opened. " + f"Log lines: {log_lines[-20:]}" + ) + + # Close door sensor (makes alarm ready again) + client.switch_command(door_switch_info.key, False) + + # Wait for second on_ready callback (ready) + try: + await asyncio.wait_for(ready_future_2, timeout=2.0) + except TimeoutError: + pytest.fail( + f"on_ready callback not fired when sensor closed. " + f"Log lines: {log_lines[-20:]}" + ) + + # Final state should still be DISARMED + assert states_received[-1] == AlarmControlPanelState.DISARMED From d3918dc784d0680c1f69c2e0ed9c43112245292f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 28 Nov 2025 16:39:05 -0600 Subject: [PATCH 385/415] reduce --- .../alarm_control_panel.cpp | 30 ++-- .../alarm_control_panel/alarm_control_panel.h | 61 +++----- .../alarm_control_panel/automation.h | 143 ++++++++++++++---- .../mqtt/mqtt_alarm_control_panel.cpp | 2 +- .../mqtt/mqtt_alarm_control_panel.h | 9 +- ...t_alarm_control_panel_state_transitions.py | 25 ++- 6 files changed, 173 insertions(+), 97 deletions(-) diff --git a/esphome/components/alarm_control_panel/alarm_control_panel.cpp b/esphome/components/alarm_control_panel/alarm_control_panel.cpp index 733d255158..f938155dd3 100644 --- a/esphome/components/alarm_control_panel/alarm_control_panel.cpp +++ b/esphome/components/alarm_control_panel/alarm_control_panel.cpp @@ -35,15 +35,15 @@ void AlarmControlPanel::publish_state(AlarmControlPanelState state) { ESP_LOGD(TAG, "Set state to: %s, previous: %s", LOG_STR_ARG(alarm_control_panel_state_to_string(state)), LOG_STR_ARG(alarm_control_panel_state_to_string(prev_state))); this->current_state_ = state; - - for (auto *listener : this->state_listeners_) { - listener->on_state(state, prev_state); - } - + // Single state callback - triggers check get_state() for specific states + this->state_callback_.call(); #if defined(USE_ALARM_CONTROL_PANEL) && defined(USE_CONTROLLER_REGISTRY) ControllerRegistry::notify_alarm_control_panel_update(this); #endif - + // Cleared fires when leaving TRIGGERED state + if (prev_state == ACP_STATE_TRIGGERED) { + this->cleared_callback_.call(); + } if (state == this->desired_state_) { // only store when in the desired state this->pref_.save(&state); @@ -51,14 +51,20 @@ void AlarmControlPanel::publish_state(AlarmControlPanelState state) { } } -void AlarmControlPanel::notify_chime() { - for (auto *listener : this->event_listeners_) - listener->on_chime(); +void AlarmControlPanel::add_on_state_callback(std::function &&callback) { + this->state_callback_.add(std::move(callback)); } -void AlarmControlPanel::notify_ready() { - for (auto *listener : this->event_listeners_) - listener->on_ready(); +void AlarmControlPanel::add_on_cleared_callback(std::function &&callback) { + this->cleared_callback_.add(std::move(callback)); +} + +void AlarmControlPanel::add_on_chime_callback(std::function &&callback) { + this->chime_callback_.add(std::move(callback)); +} + +void AlarmControlPanel::add_on_ready_callback(std::function &&callback) { + this->ready_callback_.add(std::move(callback)); } void AlarmControlPanel::arm_away(optional code) { diff --git a/esphome/components/alarm_control_panel/alarm_control_panel.h b/esphome/components/alarm_control_panel/alarm_control_panel.h index 80d3906aa3..c46edc11c2 100644 --- a/esphome/components/alarm_control_panel/alarm_control_panel.h +++ b/esphome/components/alarm_control_panel/alarm_control_panel.h @@ -1,37 +1,17 @@ #pragma once -#include +#include #include "alarm_control_panel_call.h" #include "alarm_control_panel_state.h" -#include "esphome/core/component.h" +#include "esphome/core/automation.h" #include "esphome/core/entity_base.h" -#include "esphome/core/helpers.h" #include "esphome/core/log.h" -#include "esphome/core/preferences.h" namespace esphome { namespace alarm_control_panel { -/// Listener interface for alarm control panel state changes. -class AlarmControlPanelStateListener { - public: - virtual ~AlarmControlPanelStateListener() = default; - /// Called when state changes. Check new_state/prev_state to filter specific states. - virtual void on_state(AlarmControlPanelState new_state, AlarmControlPanelState prev_state) = 0; -}; - -/// Listener interface for alarm events (chime, ready, etc). -class AlarmControlPanelEventListener { - public: - virtual ~AlarmControlPanelEventListener() = default; - /// Called when a chime zone opens while disarmed. - virtual void on_chime() {} - /// Called when zones ready state changes. - virtual void on_ready() {} -}; - enum AlarmControlPanelFeature : uint8_t { // Matches Home Assistant values ACP_FEAT_ARM_HOME = 1 << 0, @@ -55,25 +35,30 @@ class AlarmControlPanel : public EntityBase { */ void publish_state(AlarmControlPanelState state); - /** Register a listener for state changes. + /** Add a callback for when the state of the alarm_control_panel changes. + * Triggers can check get_state() to determine the new state. * - * @param listener The listener to add (must remain valid for lifetime of panel) + * @param callback The callback function */ - void add_listener(AlarmControlPanelStateListener *listener) { this->state_listeners_.push_back(listener); } + void add_on_state_callback(std::function &&callback); - /** Register a listener for alarm events (chime/ready/etc). + /** Add a callback for when the state of the alarm_control_panel clears from triggered * - * @param listener The listener to add (must remain valid for lifetime of panel) + * @param callback The callback function */ - void add_listener(AlarmControlPanelEventListener *listener) { this->event_listeners_.push_back(listener); } + void add_on_cleared_callback(std::function &&callback); - /** Notify listeners of a chime event (zone opened while disarmed). + /** Add a callback for when a chime zone goes from closed to open + * + * @param callback The callback function */ - void notify_chime(); + void add_on_chime_callback(std::function &&callback); - /** Notify listeners of a ready state change. + /** Add a callback for when a ready state changes + * + * @param callback The callback function */ - void notify_ready(); + void add_on_ready_callback(std::function &&callback); /** A numeric representation of the supported features as per HomeAssistant * @@ -146,10 +131,14 @@ class AlarmControlPanel : public EntityBase { uint32_t last_update_; // the call control function virtual void control(const AlarmControlPanelCall &call) = 0; - // registered state listeners - std::vector state_listeners_; - // registered event listeners (chime/ready/etc) - std::vector event_listeners_; + // state callback - triggers check get_state() for specific state + CallbackManager state_callback_{}; + // clear callback - fires when leaving TRIGGERED state + CallbackManager cleared_callback_{}; + // chime callback + CallbackManager chime_callback_{}; + // ready callback + CallbackManager ready_callback_{}; }; } // namespace alarm_control_panel diff --git a/esphome/components/alarm_control_panel/automation.h b/esphome/components/alarm_control_panel/automation.h index 19a3662e83..b9a75faad8 100644 --- a/esphome/components/alarm_control_panel/automation.h +++ b/esphome/components/alarm_control_panel/automation.h @@ -7,54 +7,133 @@ namespace esphome { namespace alarm_control_panel { /// Trigger on any state change -class StateTrigger final : public Trigger<>, public AlarmControlPanelStateListener { +class StateTrigger : public Trigger<> { public: - explicit StateTrigger(AlarmControlPanel *alarm_control_panel) { alarm_control_panel->add_listener(this); } - void on_state(AlarmControlPanelState new_state, AlarmControlPanelState prev_state) override { this->trigger(); } -}; - -/// Template trigger that fires when entering a specific state -template -class StateEnterTrigger final : public Trigger<>, public AlarmControlPanelStateListener { - public: - explicit StateEnterTrigger(AlarmControlPanel *alarm_control_panel) { alarm_control_panel->add_listener(this); } - void on_state(AlarmControlPanelState new_state, AlarmControlPanelState prev_state) override { - if (new_state == State) - this->trigger(); + explicit StateTrigger(AlarmControlPanel *alarm_control_panel) { + alarm_control_panel->add_on_state_callback([this]() { this->trigger(); }); } }; -// Type aliases for state-specific triggers -using TriggeredTrigger = StateEnterTrigger; -using ArmingTrigger = StateEnterTrigger; -using PendingTrigger = StateEnterTrigger; -using ArmedHomeTrigger = StateEnterTrigger; -using ArmedNightTrigger = StateEnterTrigger; -using ArmedAwayTrigger = StateEnterTrigger; -using DisarmedTrigger = StateEnterTrigger; +/// Trigger when entering TRIGGERED state +class TriggeredTrigger : public Trigger<> { + public: + explicit TriggeredTrigger(AlarmControlPanel *alarm_control_panel) : alarm_control_panel_(alarm_control_panel) { + alarm_control_panel->add_on_state_callback([this]() { + if (this->alarm_control_panel_->get_state() == ACP_STATE_TRIGGERED) + this->trigger(); + }); + } + + protected: + AlarmControlPanel *alarm_control_panel_; +}; + +/// Trigger when entering ARMING state +class ArmingTrigger : public Trigger<> { + public: + explicit ArmingTrigger(AlarmControlPanel *alarm_control_panel) : alarm_control_panel_(alarm_control_panel) { + alarm_control_panel->add_on_state_callback([this]() { + if (this->alarm_control_panel_->get_state() == ACP_STATE_ARMING) + this->trigger(); + }); + } + + protected: + AlarmControlPanel *alarm_control_panel_; +}; + +/// Trigger when entering PENDING state +class PendingTrigger : public Trigger<> { + public: + explicit PendingTrigger(AlarmControlPanel *alarm_control_panel) : alarm_control_panel_(alarm_control_panel) { + alarm_control_panel->add_on_state_callback([this]() { + if (this->alarm_control_panel_->get_state() == ACP_STATE_PENDING) + this->trigger(); + }); + } + + protected: + AlarmControlPanel *alarm_control_panel_; +}; + +/// Trigger when entering ARMED_HOME state +class ArmedHomeTrigger : public Trigger<> { + public: + explicit ArmedHomeTrigger(AlarmControlPanel *alarm_control_panel) : alarm_control_panel_(alarm_control_panel) { + alarm_control_panel->add_on_state_callback([this]() { + if (this->alarm_control_panel_->get_state() == ACP_STATE_ARMED_HOME) + this->trigger(); + }); + } + + protected: + AlarmControlPanel *alarm_control_panel_; +}; + +/// Trigger when entering ARMED_NIGHT state +class ArmedNightTrigger : public Trigger<> { + public: + explicit ArmedNightTrigger(AlarmControlPanel *alarm_control_panel) : alarm_control_panel_(alarm_control_panel) { + alarm_control_panel->add_on_state_callback([this]() { + if (this->alarm_control_panel_->get_state() == ACP_STATE_ARMED_NIGHT) + this->trigger(); + }); + } + + protected: + AlarmControlPanel *alarm_control_panel_; +}; + +/// Trigger when entering ARMED_AWAY state +class ArmedAwayTrigger : public Trigger<> { + public: + explicit ArmedAwayTrigger(AlarmControlPanel *alarm_control_panel) : alarm_control_panel_(alarm_control_panel) { + alarm_control_panel->add_on_state_callback([this]() { + if (this->alarm_control_panel_->get_state() == ACP_STATE_ARMED_AWAY) + this->trigger(); + }); + } + + protected: + AlarmControlPanel *alarm_control_panel_; +}; + +/// Trigger when entering DISARMED state +class DisarmedTrigger : public Trigger<> { + public: + explicit DisarmedTrigger(AlarmControlPanel *alarm_control_panel) : alarm_control_panel_(alarm_control_panel) { + alarm_control_panel->add_on_state_callback([this]() { + if (this->alarm_control_panel_->get_state() == ACP_STATE_DISARMED) + this->trigger(); + }); + } + + protected: + AlarmControlPanel *alarm_control_panel_; +}; /// Trigger when leaving TRIGGERED state (alarm cleared) -class ClearedTrigger final : public Trigger<>, public AlarmControlPanelStateListener { +class ClearedTrigger : public Trigger<> { public: - explicit ClearedTrigger(AlarmControlPanel *alarm_control_panel) { alarm_control_panel->add_listener(this); } - void on_state(AlarmControlPanelState new_state, AlarmControlPanelState prev_state) override { - if (prev_state == ACP_STATE_TRIGGERED) - this->trigger(); + explicit ClearedTrigger(AlarmControlPanel *alarm_control_panel) { + alarm_control_panel->add_on_cleared_callback([this]() { this->trigger(); }); } }; /// Trigger on chime event (zone opened while disarmed) -class ChimeTrigger final : public Trigger<>, public AlarmControlPanelEventListener { +class ChimeTrigger : public Trigger<> { public: - explicit ChimeTrigger(AlarmControlPanel *alarm_control_panel) { alarm_control_panel->add_listener(this); } - void on_chime() override { this->trigger(); } + explicit ChimeTrigger(AlarmControlPanel *alarm_control_panel) { + alarm_control_panel->add_on_chime_callback([this]() { this->trigger(); }); + } }; /// Trigger on ready state change -class ReadyTrigger final : public Trigger<>, public AlarmControlPanelEventListener { +class ReadyTrigger : public Trigger<> { public: - explicit ReadyTrigger(AlarmControlPanel *alarm_control_panel) { alarm_control_panel->add_listener(this); } - void on_ready() override { this->trigger(); } + explicit ReadyTrigger(AlarmControlPanel *alarm_control_panel) { + alarm_control_panel->add_on_ready_callback([this]() { this->trigger(); }); + } }; template class ArmAwayAction : public Action { diff --git a/esphome/components/mqtt/mqtt_alarm_control_panel.cpp b/esphome/components/mqtt/mqtt_alarm_control_panel.cpp index c96d696862..dd3df5f8aa 100644 --- a/esphome/components/mqtt/mqtt_alarm_control_panel.cpp +++ b/esphome/components/mqtt/mqtt_alarm_control_panel.cpp @@ -16,7 +16,7 @@ using namespace esphome::alarm_control_panel; MQTTAlarmControlPanelComponent::MQTTAlarmControlPanelComponent(AlarmControlPanel *alarm_control_panel) : alarm_control_panel_(alarm_control_panel) {} void MQTTAlarmControlPanelComponent::setup() { - this->alarm_control_panel_->add_listener(this); + this->alarm_control_panel_->add_on_state_callback([this]() { this->publish_state(); }); this->subscribe(this->get_command_topic_(), [this](const std::string &topic, const std::string &payload) { auto call = this->alarm_control_panel_->make_call(); if (strcasecmp(payload.c_str(), "ARM_AWAY") == 0) { diff --git a/esphome/components/mqtt/mqtt_alarm_control_panel.h b/esphome/components/mqtt/mqtt_alarm_control_panel.h index c64b8c575d..4ad37b7314 100644 --- a/esphome/components/mqtt/mqtt_alarm_control_panel.h +++ b/esphome/components/mqtt/mqtt_alarm_control_panel.h @@ -11,8 +11,7 @@ namespace esphome { namespace mqtt { -class MQTTAlarmControlPanelComponent final : public mqtt::MQTTComponent, - public alarm_control_panel::AlarmControlPanelStateListener { +class MQTTAlarmControlPanelComponent : public mqtt::MQTTComponent { public: explicit MQTTAlarmControlPanelComponent(alarm_control_panel::AlarmControlPanel *alarm_control_panel); @@ -26,12 +25,6 @@ class MQTTAlarmControlPanelComponent final : public mqtt::MQTTComponent, void dump_config() override; - // AlarmControlPanelStateListener interface - void on_state(alarm_control_panel::AlarmControlPanelState new_state, - alarm_control_panel::AlarmControlPanelState prev_state) override { - this->publish_state(); - } - protected: std::string component_type() const override; const EntityBase *get_entity() const override; diff --git a/tests/integration/test_alarm_control_panel_state_transitions.py b/tests/integration/test_alarm_control_panel_state_transitions.py index f4521762df..06010aeaa7 100644 --- a/tests/integration/test_alarm_control_panel_state_transitions.py +++ b/tests/integration/test_alarm_control_panel_state_transitions.py @@ -15,6 +15,7 @@ from aioesphomeapi import ( ) import pytest +from .state_utils import InitialStateHelper from .types import APIClientConnectedFactory, RunCompiledFunction @@ -107,7 +108,18 @@ async def test_alarm_control_panel_state_transitions( states_received.append(state.state) state_event.set() - client.subscribe_states(on_state) + # Use InitialStateHelper to handle initial state broadcast + initial_state_helper = InitialStateHelper(entities) + client.subscribe_states(initial_state_helper.on_state_wrapper(on_state)) + + # Wait for initial states from all entities + await initial_state_helper.wait_for_initial_states() + + # Verify alarm panel started in DISARMED state + initial_alarm_state = initial_state_helper.initial_states.get(alarm_info.key) + assert initial_alarm_state is not None, "No initial alarm state received" + assert isinstance(initial_alarm_state, AlarmControlPanelEntityState) + assert initial_alarm_state.state == AlarmControlPanelState.DISARMED # Helper to wait for specific state async def wait_for_state( @@ -126,9 +138,6 @@ async def test_alarm_control_panel_state_transitions( if states_received[-1] == expected: return - # Wait for initial DISARMED state - await wait_for_state(AlarmControlPanelState.DISARMED) - # ===== Test wrong code rejection ===== client.alarm_control_panel_command( alarm_info.key, @@ -136,10 +145,11 @@ async def test_alarm_control_panel_state_transitions( code="0000", # Wrong code ) - # Should NOT transition - wait a bit and verify still disarmed + # Should NOT transition - wait a bit and verify no state changes with pytest.raises(asyncio.TimeoutError): await asyncio.wait_for(state_event.wait(), timeout=0.5) - assert states_received[-1] == AlarmControlPanelState.DISARMED + # No state changes should have occurred (list is empty) + assert len(states_received) == 0, f"Unexpected state changes: {states_received}" # ===== Test ARM_AWAY sequence ===== client.alarm_control_panel_command( @@ -192,9 +202,8 @@ async def test_alarm_control_panel_state_transitions( ) await wait_for_state(AlarmControlPanelState.DISARMED) - # Verify basic state sequence + # Verify basic state sequence (initial DISARMED is handled by InitialStateHelper) expected_states = [ - AlarmControlPanelState.DISARMED, # Initial AlarmControlPanelState.ARMING, # Arm away AlarmControlPanelState.ARMED_AWAY, AlarmControlPanelState.DISARMED, From 4ab1911d82765b6742b70566df2f6d15bb448d26 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 28 Nov 2025 16:40:27 -0600 Subject: [PATCH 386/415] reduce --- .../alarm_control_panel/alarm_control_panel.h | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/esphome/components/alarm_control_panel/alarm_control_panel.h b/esphome/components/alarm_control_panel/alarm_control_panel.h index c46edc11c2..08b7dc88be 100644 --- a/esphome/components/alarm_control_panel/alarm_control_panel.h +++ b/esphome/components/alarm_control_panel/alarm_control_panel.h @@ -60,6 +60,16 @@ class AlarmControlPanel : public EntityBase { */ void add_on_ready_callback(std::function &&callback); + /** Notify chime event listeners + * + */ + void notify_chime() { this->chime_callback_.call(); } + + /** Notify ready state change listeners + * + */ + void notify_ready() { this->ready_callback_.call(); } + /** A numeric representation of the supported features as per HomeAssistant * */ From 913581e7ee9416a56b1d4e7c2d16923594817d05 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 28 Nov 2025 16:42:03 -0600 Subject: [PATCH 387/415] reduce --- .../alarm_control_panel/alarm_control_panel.h | 10 ---------- .../template_alarm_control_panel.cpp | 4 ++-- 2 files changed, 2 insertions(+), 12 deletions(-) diff --git a/esphome/components/alarm_control_panel/alarm_control_panel.h b/esphome/components/alarm_control_panel/alarm_control_panel.h index 08b7dc88be..c46edc11c2 100644 --- a/esphome/components/alarm_control_panel/alarm_control_panel.h +++ b/esphome/components/alarm_control_panel/alarm_control_panel.h @@ -60,16 +60,6 @@ class AlarmControlPanel : public EntityBase { */ void add_on_ready_callback(std::function &&callback); - /** Notify chime event listeners - * - */ - void notify_chime() { this->chime_callback_.call(); } - - /** Notify ready state change listeners - * - */ - void notify_ready() { this->ready_callback_.call(); } - /** A numeric representation of the supported features as per HomeAssistant * */ diff --git a/esphome/components/template/alarm_control_panel/template_alarm_control_panel.cpp b/esphome/components/template/alarm_control_panel/template_alarm_control_panel.cpp index 003acb06ff..f025435261 100644 --- a/esphome/components/template/alarm_control_panel/template_alarm_control_panel.cpp +++ b/esphome/components/template/alarm_control_panel/template_alarm_control_panel.cpp @@ -133,7 +133,7 @@ void TemplateAlarmControlPanel::loop() { if ((!this->sensor_data_[info.store_index].last_chime_state) && (sensor->state)) { // Must be disarmed to chime if (this->current_state_ == ACP_STATE_DISARMED) { - this->notify_chime(); + this->chime_callback_.call(); } } // Record the sensor state change @@ -182,7 +182,7 @@ void TemplateAlarmControlPanel::loop() { // Call the ready state change callback if there was a change if (this->sensors_ready_ != sensors_ready) { this->sensors_ready_ = sensors_ready; - this->notify_ready(); + this->ready_callback_.call(); } #endif From c7e8a3eea56c7aab64348d16280f0f6350411e6a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 28 Nov 2025 16:43:22 -0600 Subject: [PATCH 388/415] reduce --- .../alarm_control_panel/automation.h | 99 +++---------------- 1 file changed, 12 insertions(+), 87 deletions(-) diff --git a/esphome/components/alarm_control_panel/automation.h b/esphome/components/alarm_control_panel/automation.h index b9a75faad8..af4a14e27a 100644 --- a/esphome/components/alarm_control_panel/automation.h +++ b/esphome/components/alarm_control_panel/automation.h @@ -14,12 +14,12 @@ class StateTrigger : public Trigger<> { } }; -/// Trigger when entering TRIGGERED state -class TriggeredTrigger : public Trigger<> { +/// Template trigger that fires when entering a specific state +template class StateEnterTrigger : public Trigger<> { public: - explicit TriggeredTrigger(AlarmControlPanel *alarm_control_panel) : alarm_control_panel_(alarm_control_panel) { + explicit StateEnterTrigger(AlarmControlPanel *alarm_control_panel) : alarm_control_panel_(alarm_control_panel) { alarm_control_panel->add_on_state_callback([this]() { - if (this->alarm_control_panel_->get_state() == ACP_STATE_TRIGGERED) + if (this->alarm_control_panel_->get_state() == State) this->trigger(); }); } @@ -28,89 +28,14 @@ class TriggeredTrigger : public Trigger<> { AlarmControlPanel *alarm_control_panel_; }; -/// Trigger when entering ARMING state -class ArmingTrigger : public Trigger<> { - public: - explicit ArmingTrigger(AlarmControlPanel *alarm_control_panel) : alarm_control_panel_(alarm_control_panel) { - alarm_control_panel->add_on_state_callback([this]() { - if (this->alarm_control_panel_->get_state() == ACP_STATE_ARMING) - this->trigger(); - }); - } - - protected: - AlarmControlPanel *alarm_control_panel_; -}; - -/// Trigger when entering PENDING state -class PendingTrigger : public Trigger<> { - public: - explicit PendingTrigger(AlarmControlPanel *alarm_control_panel) : alarm_control_panel_(alarm_control_panel) { - alarm_control_panel->add_on_state_callback([this]() { - if (this->alarm_control_panel_->get_state() == ACP_STATE_PENDING) - this->trigger(); - }); - } - - protected: - AlarmControlPanel *alarm_control_panel_; -}; - -/// Trigger when entering ARMED_HOME state -class ArmedHomeTrigger : public Trigger<> { - public: - explicit ArmedHomeTrigger(AlarmControlPanel *alarm_control_panel) : alarm_control_panel_(alarm_control_panel) { - alarm_control_panel->add_on_state_callback([this]() { - if (this->alarm_control_panel_->get_state() == ACP_STATE_ARMED_HOME) - this->trigger(); - }); - } - - protected: - AlarmControlPanel *alarm_control_panel_; -}; - -/// Trigger when entering ARMED_NIGHT state -class ArmedNightTrigger : public Trigger<> { - public: - explicit ArmedNightTrigger(AlarmControlPanel *alarm_control_panel) : alarm_control_panel_(alarm_control_panel) { - alarm_control_panel->add_on_state_callback([this]() { - if (this->alarm_control_panel_->get_state() == ACP_STATE_ARMED_NIGHT) - this->trigger(); - }); - } - - protected: - AlarmControlPanel *alarm_control_panel_; -}; - -/// Trigger when entering ARMED_AWAY state -class ArmedAwayTrigger : public Trigger<> { - public: - explicit ArmedAwayTrigger(AlarmControlPanel *alarm_control_panel) : alarm_control_panel_(alarm_control_panel) { - alarm_control_panel->add_on_state_callback([this]() { - if (this->alarm_control_panel_->get_state() == ACP_STATE_ARMED_AWAY) - this->trigger(); - }); - } - - protected: - AlarmControlPanel *alarm_control_panel_; -}; - -/// Trigger when entering DISARMED state -class DisarmedTrigger : public Trigger<> { - public: - explicit DisarmedTrigger(AlarmControlPanel *alarm_control_panel) : alarm_control_panel_(alarm_control_panel) { - alarm_control_panel->add_on_state_callback([this]() { - if (this->alarm_control_panel_->get_state() == ACP_STATE_DISARMED) - this->trigger(); - }); - } - - protected: - AlarmControlPanel *alarm_control_panel_; -}; +// Type aliases for state-specific triggers +using TriggeredTrigger = StateEnterTrigger; +using ArmingTrigger = StateEnterTrigger; +using PendingTrigger = StateEnterTrigger; +using ArmedHomeTrigger = StateEnterTrigger; +using ArmedNightTrigger = StateEnterTrigger; +using ArmedAwayTrigger = StateEnterTrigger; +using DisarmedTrigger = StateEnterTrigger; /// Trigger when leaving TRIGGERED state (alarm cleared) class ClearedTrigger : public Trigger<> { From b872d105832e5aa774906fd8cf7eac2296efb1d3 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 28 Nov 2025 16:52:46 -0600 Subject: [PATCH 389/415] simplify --- esphome/components/camera/camera.h | 2 +- esphome/components/esp32_camera/esp32_camera.h | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/esphome/components/camera/camera.h b/esphome/components/camera/camera.h index 9f46eb0c43..6e1fc8cc06 100644 --- a/esphome/components/camera/camera.h +++ b/esphome/components/camera/camera.h @@ -45,7 +45,7 @@ class CameraImage; */ class CameraListener { public: - virtual void on_camera_image(const std::shared_ptr &image) = 0; + virtual void on_camera_image(const std::shared_ptr &image) {} virtual void on_stream_start() {} virtual void on_stream_stop() {} }; diff --git a/esphome/components/esp32_camera/esp32_camera.h b/esphome/components/esp32_camera/esp32_camera.h index 96b11db65c..54a7d6064a 100644 --- a/esphome/components/esp32_camera/esp32_camera.h +++ b/esphome/components/esp32_camera/esp32_camera.h @@ -232,14 +232,12 @@ class ESP32CameraImageTrigger : public Trigger, public camera:: class ESP32CameraStreamStartTrigger : public Trigger<>, public camera::CameraListener { public: explicit ESP32CameraStreamStartTrigger(ESP32Camera *parent) { parent->add_listener(this); } - void on_camera_image(const std::shared_ptr &image) override {} void on_stream_start() override { this->trigger(); } }; class ESP32CameraStreamStopTrigger : public Trigger<>, public camera::CameraListener { public: explicit ESP32CameraStreamStopTrigger(ESP32Camera *parent) { parent->add_listener(this); } - void on_camera_image(const std::shared_ptr &image) override {} void on_stream_stop() override { this->trigger(); } }; From 7f7ccd6c9cd5308bf0d74bc47c16b6cb44a2d1b0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 28 Nov 2025 18:53:19 -0600 Subject: [PATCH 390/415] [api] Store device info strings in flash on ESP8266 --- esphome/components/api/api_connection.cpp | 50 +++++++++++++++++++---- 1 file changed, 41 insertions(+), 9 deletions(-) diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 9ad45dc6b7..b676dd5592 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -11,6 +11,9 @@ #include #include #include +#ifdef USE_ESP8266 +#include +#endif #include "esphome/components/network/util.h" #include "esphome/core/application.h" #include "esphome/core/entity_base.h" @@ -1468,35 +1471,64 @@ bool APIConnection::send_device_info_response(const DeviceInfoRequest &msg) { resp.set_compilation_time(App.get_compilation_time_ref()); - // Compile-time StringRef constants for manufacturers + // Manufacturer string - define once, handle ESP8266 PROGMEM separately #if defined(USE_ESP8266) || defined(USE_ESP32) - static constexpr auto MANUFACTURER = StringRef::from_lit("Espressif"); +#define ESPHOME_MANUFACTURER "Espressif" #elif defined(USE_RP2040) - static constexpr auto MANUFACTURER = StringRef::from_lit("Raspberry Pi"); +#define ESPHOME_MANUFACTURER "Raspberry Pi" #elif defined(USE_BK72XX) - static constexpr auto MANUFACTURER = StringRef::from_lit("Beken"); +#define ESPHOME_MANUFACTURER "Beken" #elif defined(USE_LN882X) - static constexpr auto MANUFACTURER = StringRef::from_lit("Lightning"); +#define ESPHOME_MANUFACTURER "Lightning" #elif defined(USE_NRF52) - static constexpr auto MANUFACTURER = StringRef::from_lit("Nordic Semiconductor"); +#define ESPHOME_MANUFACTURER "Nordic Semiconductor" #elif defined(USE_RTL87XX) - static constexpr auto MANUFACTURER = StringRef::from_lit("Realtek"); +#define ESPHOME_MANUFACTURER "Realtek" #elif defined(USE_HOST) - static constexpr auto MANUFACTURER = StringRef::from_lit("Host"); +#define ESPHOME_MANUFACTURER "Host" #endif - resp.set_manufacturer(MANUFACTURER); +#ifdef USE_ESP8266 + // ESP8266 requires PROGMEM for flash storage, copy to stack for memcpy compatibility + static const char MANUFACTURER_PROGMEM[] PROGMEM = ESPHOME_MANUFACTURER; + char manufacturer_buf[sizeof(MANUFACTURER_PROGMEM)]; + memcpy_P(manufacturer_buf, MANUFACTURER_PROGMEM, sizeof(MANUFACTURER_PROGMEM)); + resp.set_manufacturer(StringRef(manufacturer_buf, sizeof(MANUFACTURER_PROGMEM) - 1)); +#else + static constexpr auto MANUFACTURER = StringRef::from_lit(ESPHOME_MANUFACTURER); + resp.set_manufacturer(MANUFACTURER); +#endif +#undef ESPHOME_MANUFACTURER + +#ifdef USE_ESP8266 + static const char MODEL_PROGMEM[] PROGMEM = ESPHOME_BOARD; + char model_buf[sizeof(MODEL_PROGMEM)]; + memcpy_P(model_buf, MODEL_PROGMEM, sizeof(MODEL_PROGMEM)); + resp.set_model(StringRef(model_buf, sizeof(MODEL_PROGMEM) - 1)); +#else static constexpr auto MODEL = StringRef::from_lit(ESPHOME_BOARD); resp.set_model(MODEL); +#endif #ifdef USE_DEEP_SLEEP resp.has_deep_sleep = deep_sleep::global_has_deep_sleep; #endif #ifdef ESPHOME_PROJECT_NAME +#ifdef USE_ESP8266 + static const char PROJECT_NAME_PROGMEM[] PROGMEM = ESPHOME_PROJECT_NAME; + static const char PROJECT_VERSION_PROGMEM[] PROGMEM = ESPHOME_PROJECT_VERSION; + char project_name_buf[sizeof(PROJECT_NAME_PROGMEM)]; + char project_version_buf[sizeof(PROJECT_VERSION_PROGMEM)]; + memcpy_P(project_name_buf, PROJECT_NAME_PROGMEM, sizeof(PROJECT_NAME_PROGMEM)); + memcpy_P(project_version_buf, PROJECT_VERSION_PROGMEM, sizeof(PROJECT_VERSION_PROGMEM)); + resp.set_project_name(StringRef(project_name_buf, sizeof(PROJECT_NAME_PROGMEM) - 1)); + resp.set_project_version(StringRef(project_version_buf, sizeof(PROJECT_VERSION_PROGMEM) - 1)); +#else static constexpr auto PROJECT_NAME = StringRef::from_lit(ESPHOME_PROJECT_NAME); static constexpr auto PROJECT_VERSION = StringRef::from_lit(ESPHOME_PROJECT_VERSION); resp.set_project_name(PROJECT_NAME); resp.set_project_version(PROJECT_VERSION); #endif +#endif #ifdef USE_WEBSERVER resp.webserver_port = USE_WEBSERVER_PORT; #endif From 23a177f9d722d6f5f195d60f8cc9372bc853ba8a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 28 Nov 2025 19:05:52 -0600 Subject: [PATCH 391/415] [light] Store log_percent parameter strings in flash on ESP8266 --- esphome/components/light/light_call.cpp | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/esphome/components/light/light_call.cpp b/esphome/components/light/light_call.cpp index b3bdb16c73..f523b4451b 100644 --- a/esphome/components/light/light_call.cpp +++ b/esphome/components/light/light_call.cpp @@ -74,11 +74,11 @@ static const LogString *color_mode_to_human(ColorMode color_mode) { // Helper to log percentage values #if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_DEBUG -static void log_percent(const char *name, const char *param, float value) { - ESP_LOGD(TAG, " %s: %.0f%%", param, value * 100.0f); +static void log_percent(const LogString *param, float value) { + ESP_LOGD(TAG, " %s: %.0f%%", LOG_STR_ARG(param), value * 100.0f); } #else -#define log_percent(name, param, value) +#define log_percent(param, value) #endif void LightCall::perform() { @@ -104,11 +104,11 @@ void LightCall::perform() { } if (this->has_brightness()) { - log_percent(name, "Brightness", v.get_brightness()); + log_percent(LOG_STR("Brightness"), v.get_brightness()); } if (this->has_color_brightness()) { - log_percent(name, "Color brightness", v.get_color_brightness()); + log_percent(LOG_STR("Color brightness"), v.get_color_brightness()); } if (this->has_red() || this->has_green() || this->has_blue()) { ESP_LOGD(TAG, " Red: %.0f%%, Green: %.0f%%, Blue: %.0f%%", v.get_red() * 100.0f, v.get_green() * 100.0f, @@ -116,7 +116,7 @@ void LightCall::perform() { } if (this->has_white()) { - log_percent(name, "White", v.get_white()); + log_percent(LOG_STR("White"), v.get_white()); } if (this->has_color_temperature()) { ESP_LOGD(TAG, " Color temperature: %.1f mireds", v.get_color_temperature()); From bd958c5859dc997d59de00015bb9143ed5967093 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 28 Nov 2025 19:27:21 -0600 Subject: [PATCH 392/415] [api] Use shared static string for reboot timeout scheduler name --- esphome/components/api/api_server.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/esphome/components/api/api_server.cpp b/esphome/components/api/api_server.cpp index de0c4b24c9..fd6a39a666 100644 --- a/esphome/components/api/api_server.cpp +++ b/esphome/components/api/api_server.cpp @@ -117,9 +117,11 @@ void APIServer::setup() { #endif } +static const char *const REBOOT_TIMEOUT = "reboot"; + void APIServer::schedule_reboot_timeout_() { this->status_set_warning(); - this->set_timeout("api_reboot", this->reboot_timeout_, []() { + this->set_timeout(REBOOT_TIMEOUT, this->reboot_timeout_, []() { if (!global_api_server->is_connected()) { ESP_LOGE(TAG, "No clients; rebooting"); App.reboot(); @@ -155,7 +157,7 @@ void APIServer::loop() { // Clear warning status and cancel reboot when first client connects if (this->clients_.size() == 1 && this->reboot_timeout_ != 0) { this->status_clear_warning(); - this->cancel_timeout("api_reboot"); + this->cancel_timeout(REBOOT_TIMEOUT); } } } From 1120236f0638bae55891f69c1cf80b4eddb5260c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 28 Nov 2025 19:32:06 -0600 Subject: [PATCH 393/415] Revert "[api] Use shared static string for reboot timeout scheduler name" This reverts commit bd958c5859dc997d59de00015bb9143ed5967093. --- esphome/components/api/api_server.cpp | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/esphome/components/api/api_server.cpp b/esphome/components/api/api_server.cpp index 85ccf5ca29..c6f8b2079e 100644 --- a/esphome/components/api/api_server.cpp +++ b/esphome/components/api/api_server.cpp @@ -112,11 +112,9 @@ void APIServer::setup() { #endif } -static const char *const REBOOT_TIMEOUT = "reboot"; - void APIServer::schedule_reboot_timeout_() { this->status_set_warning(); - this->set_timeout(REBOOT_TIMEOUT, this->reboot_timeout_, []() { + this->set_timeout("api_reboot", this->reboot_timeout_, []() { if (!global_api_server->is_connected()) { ESP_LOGE(TAG, "No clients; rebooting"); App.reboot(); @@ -152,7 +150,7 @@ void APIServer::loop() { // Clear warning status and cancel reboot when first client connects if (this->clients_.size() == 1 && this->reboot_timeout_ != 0) { this->status_clear_warning(); - this->cancel_timeout(REBOOT_TIMEOUT); + this->cancel_timeout("api_reboot"); } } } From 1aaea4d3abbd2723a55d31cbbfa1fa504a0186d3 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 28 Nov 2025 20:52:31 -0600 Subject: [PATCH 394/415] [ble_client] Convert to C++17 namespace style --- esphome/components/ble_client/automation.cpp | 6 ++---- esphome/components/ble_client/automation.h | 6 ++---- esphome/components/ble_client/ble_client.cpp | 6 ++---- esphome/components/ble_client/ble_client.h | 6 ++---- esphome/components/ble_client/output/ble_binary_output.cpp | 6 ++---- esphome/components/ble_client/output/ble_binary_output.h | 6 ++---- esphome/components/ble_client/sensor/automation.h | 6 ++---- esphome/components/ble_client/sensor/ble_rssi_sensor.cpp | 6 ++---- esphome/components/ble_client/sensor/ble_rssi_sensor.h | 6 ++---- esphome/components/ble_client/sensor/ble_sensor.cpp | 6 ++---- esphome/components/ble_client/sensor/ble_sensor.h | 6 ++---- esphome/components/ble_client/switch/ble_switch.cpp | 6 ++---- esphome/components/ble_client/switch/ble_switch.h | 6 ++---- esphome/components/ble_client/text_sensor/automation.h | 6 ++---- .../components/ble_client/text_sensor/ble_text_sensor.cpp | 6 ++---- esphome/components/ble_client/text_sensor/ble_text_sensor.h | 6 ++---- 16 files changed, 32 insertions(+), 64 deletions(-) diff --git a/esphome/components/ble_client/automation.cpp b/esphome/components/ble_client/automation.cpp index 9a0233eb70..cd2802f617 100644 --- a/esphome/components/ble_client/automation.cpp +++ b/esphome/components/ble_client/automation.cpp @@ -2,12 +2,10 @@ #include "automation.h" -namespace esphome { -namespace ble_client { +namespace esphome::ble_client { const char *const Automation::TAG = "ble_client.automation"; -} // namespace ble_client -} // namespace esphome +} // namespace esphome::ble_client #endif diff --git a/esphome/components/ble_client/automation.h b/esphome/components/ble_client/automation.h index 788eac4a57..ccda894509 100644 --- a/esphome/components/ble_client/automation.h +++ b/esphome/components/ble_client/automation.h @@ -9,8 +9,7 @@ #include "esphome/components/ble_client/ble_client.h" #include "esphome/core/log.h" -namespace esphome { -namespace ble_client { +namespace esphome::ble_client { // placeholder class for static TAG . class Automation { @@ -391,7 +390,6 @@ template class BLEClientDisconnectAction : public Action, BLEClient *ble_client_; std::tuple var_{}; }; -} // namespace ble_client -} // namespace esphome +} // namespace esphome::ble_client #endif diff --git a/esphome/components/ble_client/ble_client.cpp b/esphome/components/ble_client/ble_client.cpp index b8968fe4ba..d41fb17961 100644 --- a/esphome/components/ble_client/ble_client.cpp +++ b/esphome/components/ble_client/ble_client.cpp @@ -7,8 +7,7 @@ #ifdef USE_ESP32 -namespace esphome { -namespace ble_client { +namespace esphome::ble_client { static const char *const TAG = "ble_client"; @@ -82,7 +81,6 @@ bool BLEClient::all_nodes_established_() { return true; } -} // namespace ble_client -} // namespace esphome +} // namespace esphome::ble_client #endif diff --git a/esphome/components/ble_client/ble_client.h b/esphome/components/ble_client/ble_client.h index e04f4a8042..ca523251ef 100644 --- a/esphome/components/ble_client/ble_client.h +++ b/esphome/components/ble_client/ble_client.h @@ -15,8 +15,7 @@ #include #include -namespace esphome { -namespace ble_client { +namespace esphome::ble_client { namespace espbt = esphome::esp32_ble_tracker; @@ -75,7 +74,6 @@ class BLEClient : public BLEClientBase { std::vector nodes_; }; -} // namespace ble_client -} // namespace esphome +} // namespace esphome::ble_client #endif diff --git a/esphome/components/ble_client/output/ble_binary_output.cpp b/esphome/components/ble_client/output/ble_binary_output.cpp index 84558717f8..1d874a65e4 100644 --- a/esphome/components/ble_client/output/ble_binary_output.cpp +++ b/esphome/components/ble_client/output/ble_binary_output.cpp @@ -3,8 +3,7 @@ #include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" #ifdef USE_ESP32 -namespace esphome { -namespace ble_client { +namespace esphome::ble_client { static const char *const TAG = "ble_binary_output"; @@ -75,6 +74,5 @@ void BLEBinaryOutput::write_state(bool state) { ESP_LOGW(TAG, "[%s] Write error, err=%d", this->char_uuid_.to_string().c_str(), err); } -} // namespace ble_client -} // namespace esphome +} // namespace esphome::ble_client #endif diff --git a/esphome/components/ble_client/output/ble_binary_output.h b/esphome/components/ble_client/output/ble_binary_output.h index 5e8bd6da62..299de9b860 100644 --- a/esphome/components/ble_client/output/ble_binary_output.h +++ b/esphome/components/ble_client/output/ble_binary_output.h @@ -7,8 +7,7 @@ #ifdef USE_ESP32 #include -namespace esphome { -namespace ble_client { +namespace esphome::ble_client { namespace espbt = esphome::esp32_ble_tracker; @@ -36,7 +35,6 @@ class BLEBinaryOutput : public output::BinaryOutput, public BLEClientNode, publi esp_gatt_write_type_t write_type_{}; }; -} // namespace ble_client -} // namespace esphome +} // namespace esphome::ble_client #endif diff --git a/esphome/components/ble_client/sensor/automation.h b/esphome/components/ble_client/sensor/automation.h index 56ab7ba4c9..84430cb7d9 100644 --- a/esphome/components/ble_client/sensor/automation.h +++ b/esphome/components/ble_client/sensor/automation.h @@ -5,8 +5,7 @@ #ifdef USE_ESP32 -namespace esphome { -namespace ble_client { +namespace esphome::ble_client { class BLESensorNotifyTrigger : public Trigger, public BLESensor { public: @@ -35,7 +34,6 @@ class BLESensorNotifyTrigger : public Trigger, public BLESensor { BLESensor *sensor_; }; -} // namespace ble_client -} // namespace esphome +} // namespace esphome::ble_client #endif diff --git a/esphome/components/ble_client/sensor/ble_rssi_sensor.cpp b/esphome/components/ble_client/sensor/ble_rssi_sensor.cpp index 4edcbd3877..dc032a7a98 100644 --- a/esphome/components/ble_client/sensor/ble_rssi_sensor.cpp +++ b/esphome/components/ble_client/sensor/ble_rssi_sensor.cpp @@ -6,8 +6,7 @@ #ifdef USE_ESP32 -namespace esphome { -namespace ble_client { +namespace esphome::ble_client { static const char *const TAG = "ble_rssi_sensor"; @@ -78,6 +77,5 @@ void BLEClientRSSISensor::get_rssi_() { } } -} // namespace ble_client -} // namespace esphome +} // namespace esphome::ble_client #endif diff --git a/esphome/components/ble_client/sensor/ble_rssi_sensor.h b/esphome/components/ble_client/sensor/ble_rssi_sensor.h index 76cd8345a6..570a5b423c 100644 --- a/esphome/components/ble_client/sensor/ble_rssi_sensor.h +++ b/esphome/components/ble_client/sensor/ble_rssi_sensor.h @@ -8,8 +8,7 @@ #ifdef USE_ESP32 #include -namespace esphome { -namespace ble_client { +namespace esphome::ble_client { namespace espbt = esphome::esp32_ble_tracker; @@ -29,6 +28,5 @@ class BLEClientRSSISensor : public sensor::Sensor, public PollingComponent, publ bool should_update_{false}; }; -} // namespace ble_client -} // namespace esphome +} // namespace esphome::ble_client #endif diff --git a/esphome/components/ble_client/sensor/ble_sensor.cpp b/esphome/components/ble_client/sensor/ble_sensor.cpp index 8e3e483003..38d90faff0 100644 --- a/esphome/components/ble_client/sensor/ble_sensor.cpp +++ b/esphome/components/ble_client/sensor/ble_sensor.cpp @@ -6,8 +6,7 @@ #ifdef USE_ESP32 -namespace esphome { -namespace ble_client { +namespace esphome::ble_client { static const char *const TAG = "ble_sensor"; @@ -147,6 +146,5 @@ void BLESensor::update() { } } -} // namespace ble_client -} // namespace esphome +} // namespace esphome::ble_client #endif diff --git a/esphome/components/ble_client/sensor/ble_sensor.h b/esphome/components/ble_client/sensor/ble_sensor.h index c6335d5836..fe5b5ecd53 100644 --- a/esphome/components/ble_client/sensor/ble_sensor.h +++ b/esphome/components/ble_client/sensor/ble_sensor.h @@ -10,8 +10,7 @@ #ifdef USE_ESP32 #include -namespace esphome { -namespace ble_client { +namespace esphome::ble_client { namespace espbt = esphome::esp32_ble_tracker; @@ -48,6 +47,5 @@ class BLESensor : public sensor::Sensor, public PollingComponent, public BLEClie espbt::ESPBTUUID descr_uuid_; }; -} // namespace ble_client -} // namespace esphome +} // namespace esphome::ble_client #endif diff --git a/esphome/components/ble_client/switch/ble_switch.cpp b/esphome/components/ble_client/switch/ble_switch.cpp index 9d92b1b2b5..5baca2adcf 100644 --- a/esphome/components/ble_client/switch/ble_switch.cpp +++ b/esphome/components/ble_client/switch/ble_switch.cpp @@ -4,8 +4,7 @@ #ifdef USE_ESP32 -namespace esphome { -namespace ble_client { +namespace esphome::ble_client { static const char *const TAG = "ble_switch"; @@ -31,6 +30,5 @@ void BLEClientSwitch::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_i void BLEClientSwitch::dump_config() { LOG_SWITCH("", "BLE Client Switch", this); } -} // namespace ble_client -} // namespace esphome +} // namespace esphome::ble_client #endif diff --git a/esphome/components/ble_client/switch/ble_switch.h b/esphome/components/ble_client/switch/ble_switch.h index 9809f904e7..9be6d06b1c 100644 --- a/esphome/components/ble_client/switch/ble_switch.h +++ b/esphome/components/ble_client/switch/ble_switch.h @@ -8,8 +8,7 @@ #ifdef USE_ESP32 #include -namespace esphome { -namespace ble_client { +namespace esphome::ble_client { namespace espbt = esphome::esp32_ble_tracker; @@ -24,6 +23,5 @@ class BLEClientSwitch : public switch_::Switch, public Component, public BLEClie void write_state(bool state) override; }; -} // namespace ble_client -} // namespace esphome +} // namespace esphome::ble_client #endif diff --git a/esphome/components/ble_client/text_sensor/automation.h b/esphome/components/ble_client/text_sensor/automation.h index c504c35a58..f7b077926b 100644 --- a/esphome/components/ble_client/text_sensor/automation.h +++ b/esphome/components/ble_client/text_sensor/automation.h @@ -5,8 +5,7 @@ #ifdef USE_ESP32 -namespace esphome { -namespace ble_client { +namespace esphome::ble_client { class BLETextSensorNotifyTrigger : public Trigger, public BLETextSensor { public: @@ -33,7 +32,6 @@ class BLETextSensorNotifyTrigger : public Trigger, public BLETextSe BLETextSensor *sensor_; }; -} // namespace ble_client -} // namespace esphome +} // namespace esphome::ble_client #endif diff --git a/esphome/components/ble_client/text_sensor/ble_text_sensor.cpp b/esphome/components/ble_client/text_sensor/ble_text_sensor.cpp index bb771aed99..415981a1ba 100644 --- a/esphome/components/ble_client/text_sensor/ble_text_sensor.cpp +++ b/esphome/components/ble_client/text_sensor/ble_text_sensor.cpp @@ -7,8 +7,7 @@ #ifdef USE_ESP32 -namespace esphome { -namespace ble_client { +namespace esphome::ble_client { static const char *const TAG = "ble_text_sensor"; @@ -138,6 +137,5 @@ void BLETextSensor::update() { } } -} // namespace ble_client -} // namespace esphome +} // namespace esphome::ble_client #endif diff --git a/esphome/components/ble_client/text_sensor/ble_text_sensor.h b/esphome/components/ble_client/text_sensor/ble_text_sensor.h index c75a4df952..3fbd64389c 100644 --- a/esphome/components/ble_client/text_sensor/ble_text_sensor.h +++ b/esphome/components/ble_client/text_sensor/ble_text_sensor.h @@ -8,8 +8,7 @@ #ifdef USE_ESP32 #include -namespace esphome { -namespace ble_client { +namespace esphome::ble_client { namespace espbt = esphome::esp32_ble_tracker; @@ -40,6 +39,5 @@ class BLETextSensor : public text_sensor::TextSensor, public PollingComponent, p espbt::ESPBTUUID descr_uuid_; }; -} // namespace ble_client -} // namespace esphome +} // namespace esphome::ble_client #endif From 192abf95ce44edfbf5f98b1ec85457a64ed2d8c0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 28 Nov 2025 21:07:02 -0600 Subject: [PATCH 395/415] [fan] Use uint8_t for speed_count and fix tuya max=256 validation bug --- esphome/components/fan/fan_traits.h | 8 ++++---- esphome/components/hbridge/fan/__init__.py | 2 +- esphome/components/hbridge/fan/hbridge_fan.h | 4 ++-- esphome/components/speed/fan/__init__.py | 2 +- esphome/components/speed/fan/speed_fan.h | 4 ++-- esphome/components/template/fan/__init__.py | 2 +- esphome/components/template/fan/template_fan.h | 4 ++-- esphome/components/tuya/fan/__init__.py | 2 +- esphome/components/tuya/fan/tuya_fan.h | 4 ++-- 9 files changed, 16 insertions(+), 16 deletions(-) diff --git a/esphome/components/fan/fan_traits.h b/esphome/components/fan/fan_traits.h index 24987fe984..e22ac916c5 100644 --- a/esphome/components/fan/fan_traits.h +++ b/esphome/components/fan/fan_traits.h @@ -11,7 +11,7 @@ namespace fan { class FanTraits { public: FanTraits() = default; - FanTraits(bool oscillation, bool speed, bool direction, int speed_count) + FanTraits(bool oscillation, bool speed, bool direction, uint8_t speed_count) : oscillation_(oscillation), speed_(speed), direction_(direction), speed_count_(speed_count) {} /// Return if this fan supports oscillation. @@ -23,9 +23,9 @@ class FanTraits { /// Set whether this fan supports speed levels. void set_speed(bool speed) { this->speed_ = speed; } /// Return how many speed levels the fan has - int supported_speed_count() const { return this->speed_count_; } + uint8_t supported_speed_count() const { return this->speed_count_; } /// Set how many speed levels this fan has. - void set_supported_speed_count(int speed_count) { this->speed_count_ = speed_count; } + void set_supported_speed_count(uint8_t speed_count) { this->speed_count_ = speed_count; } /// Return if this fan supports changing direction bool supports_direction() const { return this->direction_; } /// Set whether this fan supports changing direction @@ -61,7 +61,7 @@ class FanTraits { bool oscillation_{false}; bool speed_{false}; bool direction_{false}; - int speed_count_{}; + uint8_t speed_count_{}; std::vector preset_modes_{}; }; diff --git a/esphome/components/hbridge/fan/__init__.py b/esphome/components/hbridge/fan/__init__.py index 31a20a8981..7a86c1b6b7 100644 --- a/esphome/components/hbridge/fan/__init__.py +++ b/esphome/components/hbridge/fan/__init__.py @@ -39,7 +39,7 @@ CONFIG_SCHEMA = ( cv.Optional(CONF_DECAY_MODE, default="SLOW"): cv.enum( DECAY_MODE_OPTIONS, upper=True ), - cv.Optional(CONF_SPEED_COUNT, default=100): cv.int_range(min=1), + cv.Optional(CONF_SPEED_COUNT, default=100): cv.int_range(min=1, max=255), cv.Optional(CONF_ENABLE_PIN): cv.use_id(output.FloatOutput), cv.Optional(CONF_PRESET_MODES): validate_preset_modes, } diff --git a/esphome/components/hbridge/fan/hbridge_fan.h b/esphome/components/hbridge/fan/hbridge_fan.h index ec1e8ada0e..f6ffabfd30 100644 --- a/esphome/components/hbridge/fan/hbridge_fan.h +++ b/esphome/components/hbridge/fan/hbridge_fan.h @@ -15,7 +15,7 @@ enum DecayMode { class HBridgeFan : public Component, public fan::Fan { public: - HBridgeFan(int speed_count, DecayMode decay_mode) : speed_count_(speed_count), decay_mode_(decay_mode) {} + HBridgeFan(uint8_t speed_count, DecayMode decay_mode) : speed_count_(speed_count), decay_mode_(decay_mode) {} void set_pin_a(output::FloatOutput *pin_a) { pin_a_ = pin_a; } void set_pin_b(output::FloatOutput *pin_b) { pin_b_ = pin_b; } @@ -33,7 +33,7 @@ class HBridgeFan : public Component, public fan::Fan { output::FloatOutput *pin_b_; output::FloatOutput *enable_{nullptr}; output::BinaryOutput *oscillating_{nullptr}; - int speed_count_{}; + uint8_t speed_count_{}; DecayMode decay_mode_{DECAY_MODE_SLOW}; fan::FanTraits traits_; std::vector preset_modes_{}; diff --git a/esphome/components/speed/fan/__init__.py b/esphome/components/speed/fan/__init__.py index 3c495f3160..b5dbf03211 100644 --- a/esphome/components/speed/fan/__init__.py +++ b/esphome/components/speed/fan/__init__.py @@ -25,7 +25,7 @@ CONFIG_SCHEMA = ( cv.Optional(CONF_SPEED): cv.invalid( "Configuring individual speeds is deprecated." ), - cv.Optional(CONF_SPEED_COUNT, default=100): cv.int_range(min=1), + cv.Optional(CONF_SPEED_COUNT, default=100): cv.int_range(min=1, max=255), cv.Optional(CONF_PRESET_MODES): validate_preset_modes, } ) diff --git a/esphome/components/speed/fan/speed_fan.h b/esphome/components/speed/fan/speed_fan.h index e9a389e0f3..16e6c277ce 100644 --- a/esphome/components/speed/fan/speed_fan.h +++ b/esphome/components/speed/fan/speed_fan.h @@ -10,7 +10,7 @@ namespace speed { class SpeedFan : public Component, public fan::Fan { public: - SpeedFan(int speed_count) : speed_count_(speed_count) {} + SpeedFan(uint8_t speed_count) : speed_count_(speed_count) {} void setup() override; void dump_config() override; void set_output(output::FloatOutput *output) { this->output_ = output; } @@ -26,7 +26,7 @@ class SpeedFan : public Component, public fan::Fan { output::FloatOutput *output_; output::BinaryOutput *oscillating_{nullptr}; output::BinaryOutput *direction_{nullptr}; - int speed_count_{}; + uint8_t speed_count_{}; fan::FanTraits traits_; std::vector preset_modes_{}; }; diff --git a/esphome/components/template/fan/__init__.py b/esphome/components/template/fan/__init__.py index 72b20e1efe..1ab092ca0e 100644 --- a/esphome/components/template/fan/__init__.py +++ b/esphome/components/template/fan/__init__.py @@ -19,7 +19,7 @@ CONFIG_SCHEMA = ( { cv.Optional(CONF_HAS_DIRECTION, default=False): cv.boolean, cv.Optional(CONF_HAS_OSCILLATING, default=False): cv.boolean, - cv.Optional(CONF_SPEED_COUNT): cv.int_range(min=1), + cv.Optional(CONF_SPEED_COUNT): cv.int_range(min=1, max=255), cv.Optional(CONF_PRESET_MODES): validate_preset_modes, } ) diff --git a/esphome/components/template/fan/template_fan.h b/esphome/components/template/fan/template_fan.h index 052b385b93..a7bb75425d 100644 --- a/esphome/components/template/fan/template_fan.h +++ b/esphome/components/template/fan/template_fan.h @@ -13,7 +13,7 @@ class TemplateFan final : public Component, public fan::Fan { void dump_config() override; void set_has_direction(bool has_direction) { this->has_direction_ = has_direction; } void set_has_oscillating(bool has_oscillating) { this->has_oscillating_ = has_oscillating; } - void set_speed_count(int count) { this->speed_count_ = count; } + void set_speed_count(uint8_t count) { this->speed_count_ = count; } void set_preset_modes(std::initializer_list presets) { this->preset_modes_ = presets; } fan::FanTraits get_traits() override { return this->traits_; } @@ -22,7 +22,7 @@ class TemplateFan final : public Component, public fan::Fan { bool has_oscillating_{false}; bool has_direction_{false}; - int speed_count_{0}; + uint8_t speed_count_{0}; fan::FanTraits traits_; std::vector preset_modes_{}; }; diff --git a/esphome/components/tuya/fan/__init__.py b/esphome/components/tuya/fan/__init__.py index de95888b6b..dd8626115f 100644 --- a/esphome/components/tuya/fan/__init__.py +++ b/esphome/components/tuya/fan/__init__.py @@ -22,7 +22,7 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_SPEED_DATAPOINT): cv.uint8_t, cv.Optional(CONF_SWITCH_DATAPOINT): cv.uint8_t, cv.Optional(CONF_DIRECTION_DATAPOINT): cv.uint8_t, - cv.Optional(CONF_SPEED_COUNT, default=3): cv.int_range(min=1, max=256), + cv.Optional(CONF_SPEED_COUNT, default=3): cv.int_range(min=1, max=255), } ) .extend(cv.COMPONENT_SCHEMA), diff --git a/esphome/components/tuya/fan/tuya_fan.h b/esphome/components/tuya/fan/tuya_fan.h index 527efa8246..93b4fc39ae 100644 --- a/esphome/components/tuya/fan/tuya_fan.h +++ b/esphome/components/tuya/fan/tuya_fan.h @@ -9,7 +9,7 @@ namespace tuya { class TuyaFan : public Component, public fan::Fan { public: - TuyaFan(Tuya *parent, int speed_count) : parent_(parent), speed_count_(speed_count) {} + TuyaFan(Tuya *parent, uint8_t speed_count) : parent_(parent), speed_count_(speed_count) {} void setup() override; void dump_config() override; void set_speed_id(uint8_t speed_id) { this->speed_id_ = speed_id; } @@ -27,7 +27,7 @@ class TuyaFan : public Component, public fan::Fan { optional switch_id_{}; optional oscillation_id_{}; optional direction_id_{}; - int speed_count_{}; + uint8_t speed_count_{}; TuyaDatapointType speed_type_{}; TuyaDatapointType oscillation_type_{}; }; From 4e379ab235d4b6aa529d3b3b0d9edc59029ac134 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 28 Nov 2025 21:15:09 -0600 Subject: [PATCH 396/415] [number] Reduce NumberCall size by 4 bytes on 32-bit platforms --- esphome/components/number/number_call.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/number/number_call.h b/esphome/components/number/number_call.h index 0f6889dcb6..c6f8ec0d6e 100644 --- a/esphome/components/number/number_call.h +++ b/esphome/components/number/number_call.h @@ -8,7 +8,7 @@ namespace esphome::number { class Number; -enum NumberOperation { +enum NumberOperation : uint8_t { NUMBER_OP_NONE, NUMBER_OP_SET, NUMBER_OP_INCREMENT, @@ -38,8 +38,8 @@ class NumberCall { float limit); Number *const parent_; - NumberOperation operation_{NUMBER_OP_NONE}; optional value_; + NumberOperation operation_{NUMBER_OP_NONE}; bool cycle_; }; From a30786b055766f72c2d40e7baae41f73e85f31b5 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 28 Nov 2025 21:19:47 -0600 Subject: [PATCH 397/415] clamp --- esphome/components/fan/fan.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/fan/fan.cpp b/esphome/components/fan/fan.cpp index d37825a651..0dfe63b6c9 100644 --- a/esphome/components/fan/fan.cpp +++ b/esphome/components/fan/fan.cpp @@ -65,7 +65,7 @@ void FanCall::validate_() { auto traits = this->parent_.get_traits(); if (this->speed_.has_value()) { - this->speed_ = clamp(*this->speed_, 1, traits.supported_speed_count()); + this->speed_ = clamp(*this->speed_, 1, static_cast(traits.supported_speed_count())); // https://developers.home-assistant.io/docs/core/entity/fan/#preset-modes // "Manually setting a speed must disable any set preset mode" From e851493080b85ecbf1512b2f110fa07ca6345308 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 28 Nov 2025 21:20:42 -0600 Subject: [PATCH 398/415] Update esphome/components/number/number_call.h Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- esphome/components/number/number_call.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/number/number_call.h b/esphome/components/number/number_call.h index c6f8ec0d6e..584c13f413 100644 --- a/esphome/components/number/number_call.h +++ b/esphome/components/number/number_call.h @@ -40,7 +40,7 @@ class NumberCall { Number *const parent_; optional value_; NumberOperation operation_{NUMBER_OP_NONE}; - bool cycle_; + bool cycle_{false}; }; } // namespace esphome::number From 5fa4ff754c4306083ddd320edc68c706d568e862 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 28 Nov 2025 21:57:01 -0600 Subject: [PATCH 399/415] [ble_client] Convert to C++17 namespace style (#12176) --- esphome/components/ble_client/automation.cpp | 6 ++---- esphome/components/ble_client/automation.h | 6 ++---- esphome/components/ble_client/ble_client.cpp | 6 ++---- esphome/components/ble_client/ble_client.h | 6 ++---- esphome/components/ble_client/output/ble_binary_output.cpp | 6 ++---- esphome/components/ble_client/output/ble_binary_output.h | 6 ++---- esphome/components/ble_client/sensor/automation.h | 6 ++---- esphome/components/ble_client/sensor/ble_rssi_sensor.cpp | 6 ++---- esphome/components/ble_client/sensor/ble_rssi_sensor.h | 6 ++---- esphome/components/ble_client/sensor/ble_sensor.cpp | 6 ++---- esphome/components/ble_client/sensor/ble_sensor.h | 6 ++---- esphome/components/ble_client/switch/ble_switch.cpp | 6 ++---- esphome/components/ble_client/switch/ble_switch.h | 6 ++---- esphome/components/ble_client/text_sensor/automation.h | 6 ++---- .../components/ble_client/text_sensor/ble_text_sensor.cpp | 6 ++---- esphome/components/ble_client/text_sensor/ble_text_sensor.h | 6 ++---- 16 files changed, 32 insertions(+), 64 deletions(-) diff --git a/esphome/components/ble_client/automation.cpp b/esphome/components/ble_client/automation.cpp index 9a0233eb70..cd2802f617 100644 --- a/esphome/components/ble_client/automation.cpp +++ b/esphome/components/ble_client/automation.cpp @@ -2,12 +2,10 @@ #include "automation.h" -namespace esphome { -namespace ble_client { +namespace esphome::ble_client { const char *const Automation::TAG = "ble_client.automation"; -} // namespace ble_client -} // namespace esphome +} // namespace esphome::ble_client #endif diff --git a/esphome/components/ble_client/automation.h b/esphome/components/ble_client/automation.h index 788eac4a57..ccda894509 100644 --- a/esphome/components/ble_client/automation.h +++ b/esphome/components/ble_client/automation.h @@ -9,8 +9,7 @@ #include "esphome/components/ble_client/ble_client.h" #include "esphome/core/log.h" -namespace esphome { -namespace ble_client { +namespace esphome::ble_client { // placeholder class for static TAG . class Automation { @@ -391,7 +390,6 @@ template class BLEClientDisconnectAction : public Action, BLEClient *ble_client_; std::tuple var_{}; }; -} // namespace ble_client -} // namespace esphome +} // namespace esphome::ble_client #endif diff --git a/esphome/components/ble_client/ble_client.cpp b/esphome/components/ble_client/ble_client.cpp index b8968fe4ba..d41fb17961 100644 --- a/esphome/components/ble_client/ble_client.cpp +++ b/esphome/components/ble_client/ble_client.cpp @@ -7,8 +7,7 @@ #ifdef USE_ESP32 -namespace esphome { -namespace ble_client { +namespace esphome::ble_client { static const char *const TAG = "ble_client"; @@ -82,7 +81,6 @@ bool BLEClient::all_nodes_established_() { return true; } -} // namespace ble_client -} // namespace esphome +} // namespace esphome::ble_client #endif diff --git a/esphome/components/ble_client/ble_client.h b/esphome/components/ble_client/ble_client.h index e04f4a8042..ca523251ef 100644 --- a/esphome/components/ble_client/ble_client.h +++ b/esphome/components/ble_client/ble_client.h @@ -15,8 +15,7 @@ #include #include -namespace esphome { -namespace ble_client { +namespace esphome::ble_client { namespace espbt = esphome::esp32_ble_tracker; @@ -75,7 +74,6 @@ class BLEClient : public BLEClientBase { std::vector nodes_; }; -} // namespace ble_client -} // namespace esphome +} // namespace esphome::ble_client #endif diff --git a/esphome/components/ble_client/output/ble_binary_output.cpp b/esphome/components/ble_client/output/ble_binary_output.cpp index 84558717f8..1d874a65e4 100644 --- a/esphome/components/ble_client/output/ble_binary_output.cpp +++ b/esphome/components/ble_client/output/ble_binary_output.cpp @@ -3,8 +3,7 @@ #include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" #ifdef USE_ESP32 -namespace esphome { -namespace ble_client { +namespace esphome::ble_client { static const char *const TAG = "ble_binary_output"; @@ -75,6 +74,5 @@ void BLEBinaryOutput::write_state(bool state) { ESP_LOGW(TAG, "[%s] Write error, err=%d", this->char_uuid_.to_string().c_str(), err); } -} // namespace ble_client -} // namespace esphome +} // namespace esphome::ble_client #endif diff --git a/esphome/components/ble_client/output/ble_binary_output.h b/esphome/components/ble_client/output/ble_binary_output.h index 5e8bd6da62..299de9b860 100644 --- a/esphome/components/ble_client/output/ble_binary_output.h +++ b/esphome/components/ble_client/output/ble_binary_output.h @@ -7,8 +7,7 @@ #ifdef USE_ESP32 #include -namespace esphome { -namespace ble_client { +namespace esphome::ble_client { namespace espbt = esphome::esp32_ble_tracker; @@ -36,7 +35,6 @@ class BLEBinaryOutput : public output::BinaryOutput, public BLEClientNode, publi esp_gatt_write_type_t write_type_{}; }; -} // namespace ble_client -} // namespace esphome +} // namespace esphome::ble_client #endif diff --git a/esphome/components/ble_client/sensor/automation.h b/esphome/components/ble_client/sensor/automation.h index 56ab7ba4c9..84430cb7d9 100644 --- a/esphome/components/ble_client/sensor/automation.h +++ b/esphome/components/ble_client/sensor/automation.h @@ -5,8 +5,7 @@ #ifdef USE_ESP32 -namespace esphome { -namespace ble_client { +namespace esphome::ble_client { class BLESensorNotifyTrigger : public Trigger, public BLESensor { public: @@ -35,7 +34,6 @@ class BLESensorNotifyTrigger : public Trigger, public BLESensor { BLESensor *sensor_; }; -} // namespace ble_client -} // namespace esphome +} // namespace esphome::ble_client #endif diff --git a/esphome/components/ble_client/sensor/ble_rssi_sensor.cpp b/esphome/components/ble_client/sensor/ble_rssi_sensor.cpp index 4edcbd3877..dc032a7a98 100644 --- a/esphome/components/ble_client/sensor/ble_rssi_sensor.cpp +++ b/esphome/components/ble_client/sensor/ble_rssi_sensor.cpp @@ -6,8 +6,7 @@ #ifdef USE_ESP32 -namespace esphome { -namespace ble_client { +namespace esphome::ble_client { static const char *const TAG = "ble_rssi_sensor"; @@ -78,6 +77,5 @@ void BLEClientRSSISensor::get_rssi_() { } } -} // namespace ble_client -} // namespace esphome +} // namespace esphome::ble_client #endif diff --git a/esphome/components/ble_client/sensor/ble_rssi_sensor.h b/esphome/components/ble_client/sensor/ble_rssi_sensor.h index 76cd8345a6..570a5b423c 100644 --- a/esphome/components/ble_client/sensor/ble_rssi_sensor.h +++ b/esphome/components/ble_client/sensor/ble_rssi_sensor.h @@ -8,8 +8,7 @@ #ifdef USE_ESP32 #include -namespace esphome { -namespace ble_client { +namespace esphome::ble_client { namespace espbt = esphome::esp32_ble_tracker; @@ -29,6 +28,5 @@ class BLEClientRSSISensor : public sensor::Sensor, public PollingComponent, publ bool should_update_{false}; }; -} // namespace ble_client -} // namespace esphome +} // namespace esphome::ble_client #endif diff --git a/esphome/components/ble_client/sensor/ble_sensor.cpp b/esphome/components/ble_client/sensor/ble_sensor.cpp index 8e3e483003..38d90faff0 100644 --- a/esphome/components/ble_client/sensor/ble_sensor.cpp +++ b/esphome/components/ble_client/sensor/ble_sensor.cpp @@ -6,8 +6,7 @@ #ifdef USE_ESP32 -namespace esphome { -namespace ble_client { +namespace esphome::ble_client { static const char *const TAG = "ble_sensor"; @@ -147,6 +146,5 @@ void BLESensor::update() { } } -} // namespace ble_client -} // namespace esphome +} // namespace esphome::ble_client #endif diff --git a/esphome/components/ble_client/sensor/ble_sensor.h b/esphome/components/ble_client/sensor/ble_sensor.h index c6335d5836..fe5b5ecd53 100644 --- a/esphome/components/ble_client/sensor/ble_sensor.h +++ b/esphome/components/ble_client/sensor/ble_sensor.h @@ -10,8 +10,7 @@ #ifdef USE_ESP32 #include -namespace esphome { -namespace ble_client { +namespace esphome::ble_client { namespace espbt = esphome::esp32_ble_tracker; @@ -48,6 +47,5 @@ class BLESensor : public sensor::Sensor, public PollingComponent, public BLEClie espbt::ESPBTUUID descr_uuid_; }; -} // namespace ble_client -} // namespace esphome +} // namespace esphome::ble_client #endif diff --git a/esphome/components/ble_client/switch/ble_switch.cpp b/esphome/components/ble_client/switch/ble_switch.cpp index 9d92b1b2b5..5baca2adcf 100644 --- a/esphome/components/ble_client/switch/ble_switch.cpp +++ b/esphome/components/ble_client/switch/ble_switch.cpp @@ -4,8 +4,7 @@ #ifdef USE_ESP32 -namespace esphome { -namespace ble_client { +namespace esphome::ble_client { static const char *const TAG = "ble_switch"; @@ -31,6 +30,5 @@ void BLEClientSwitch::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_i void BLEClientSwitch::dump_config() { LOG_SWITCH("", "BLE Client Switch", this); } -} // namespace ble_client -} // namespace esphome +} // namespace esphome::ble_client #endif diff --git a/esphome/components/ble_client/switch/ble_switch.h b/esphome/components/ble_client/switch/ble_switch.h index 9809f904e7..9be6d06b1c 100644 --- a/esphome/components/ble_client/switch/ble_switch.h +++ b/esphome/components/ble_client/switch/ble_switch.h @@ -8,8 +8,7 @@ #ifdef USE_ESP32 #include -namespace esphome { -namespace ble_client { +namespace esphome::ble_client { namespace espbt = esphome::esp32_ble_tracker; @@ -24,6 +23,5 @@ class BLEClientSwitch : public switch_::Switch, public Component, public BLEClie void write_state(bool state) override; }; -} // namespace ble_client -} // namespace esphome +} // namespace esphome::ble_client #endif diff --git a/esphome/components/ble_client/text_sensor/automation.h b/esphome/components/ble_client/text_sensor/automation.h index c504c35a58..f7b077926b 100644 --- a/esphome/components/ble_client/text_sensor/automation.h +++ b/esphome/components/ble_client/text_sensor/automation.h @@ -5,8 +5,7 @@ #ifdef USE_ESP32 -namespace esphome { -namespace ble_client { +namespace esphome::ble_client { class BLETextSensorNotifyTrigger : public Trigger, public BLETextSensor { public: @@ -33,7 +32,6 @@ class BLETextSensorNotifyTrigger : public Trigger, public BLETextSe BLETextSensor *sensor_; }; -} // namespace ble_client -} // namespace esphome +} // namespace esphome::ble_client #endif diff --git a/esphome/components/ble_client/text_sensor/ble_text_sensor.cpp b/esphome/components/ble_client/text_sensor/ble_text_sensor.cpp index bb771aed99..415981a1ba 100644 --- a/esphome/components/ble_client/text_sensor/ble_text_sensor.cpp +++ b/esphome/components/ble_client/text_sensor/ble_text_sensor.cpp @@ -7,8 +7,7 @@ #ifdef USE_ESP32 -namespace esphome { -namespace ble_client { +namespace esphome::ble_client { static const char *const TAG = "ble_text_sensor"; @@ -138,6 +137,5 @@ void BLETextSensor::update() { } } -} // namespace ble_client -} // namespace esphome +} // namespace esphome::ble_client #endif diff --git a/esphome/components/ble_client/text_sensor/ble_text_sensor.h b/esphome/components/ble_client/text_sensor/ble_text_sensor.h index c75a4df952..3fbd64389c 100644 --- a/esphome/components/ble_client/text_sensor/ble_text_sensor.h +++ b/esphome/components/ble_client/text_sensor/ble_text_sensor.h @@ -8,8 +8,7 @@ #ifdef USE_ESP32 #include -namespace esphome { -namespace ble_client { +namespace esphome::ble_client { namespace espbt = esphome::esp32_ble_tracker; @@ -40,6 +39,5 @@ class BLETextSensor : public text_sensor::TextSensor, public PollingComponent, p espbt::ESPBTUUID descr_uuid_; }; -} // namespace ble_client -} // namespace esphome +} // namespace esphome::ble_client #endif From 2174795b273ef3ad26f8606e1e7d2d9a28c41646 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 28 Nov 2025 21:57:36 -0600 Subject: [PATCH 400/415] [number] Reduce NumberCall size by 4 bytes on 32-bit platforms (#12178) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- esphome/components/number/number_call.h | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/esphome/components/number/number_call.h b/esphome/components/number/number_call.h index 0f6889dcb6..584c13f413 100644 --- a/esphome/components/number/number_call.h +++ b/esphome/components/number/number_call.h @@ -8,7 +8,7 @@ namespace esphome::number { class Number; -enum NumberOperation { +enum NumberOperation : uint8_t { NUMBER_OP_NONE, NUMBER_OP_SET, NUMBER_OP_INCREMENT, @@ -38,9 +38,9 @@ class NumberCall { float limit); Number *const parent_; - NumberOperation operation_{NUMBER_OP_NONE}; optional value_; - bool cycle_; + NumberOperation operation_{NUMBER_OP_NONE}; + bool cycle_{false}; }; } // namespace esphome::number From b71d8010d25e0252262aff70b14a49857e4a4025 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 28 Nov 2025 21:59:31 -0600 Subject: [PATCH 401/415] [light] Store log_percent parameter strings in flash on ESP8266 (#12174) --- esphome/components/light/light_call.cpp | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/esphome/components/light/light_call.cpp b/esphome/components/light/light_call.cpp index b3bdb16c73..f523b4451b 100644 --- a/esphome/components/light/light_call.cpp +++ b/esphome/components/light/light_call.cpp @@ -74,11 +74,11 @@ static const LogString *color_mode_to_human(ColorMode color_mode) { // Helper to log percentage values #if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_DEBUG -static void log_percent(const char *name, const char *param, float value) { - ESP_LOGD(TAG, " %s: %.0f%%", param, value * 100.0f); +static void log_percent(const LogString *param, float value) { + ESP_LOGD(TAG, " %s: %.0f%%", LOG_STR_ARG(param), value * 100.0f); } #else -#define log_percent(name, param, value) +#define log_percent(param, value) #endif void LightCall::perform() { @@ -104,11 +104,11 @@ void LightCall::perform() { } if (this->has_brightness()) { - log_percent(name, "Brightness", v.get_brightness()); + log_percent(LOG_STR("Brightness"), v.get_brightness()); } if (this->has_color_brightness()) { - log_percent(name, "Color brightness", v.get_color_brightness()); + log_percent(LOG_STR("Color brightness"), v.get_color_brightness()); } if (this->has_red() || this->has_green() || this->has_blue()) { ESP_LOGD(TAG, " Red: %.0f%%, Green: %.0f%%, Blue: %.0f%%", v.get_red() * 100.0f, v.get_green() * 100.0f, @@ -116,7 +116,7 @@ void LightCall::perform() { } if (this->has_white()) { - log_percent(name, "White", v.get_white()); + log_percent(LOG_STR("White"), v.get_white()); } if (this->has_color_temperature()) { ESP_LOGD(TAG, " Color temperature: %.1f mireds", v.get_color_temperature()); From 64281631a1b2fe3d91418bb26e4c705eaa4dbc7b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 29 Nov 2025 02:32:40 -0600 Subject: [PATCH 402/415] [esp32] Place FreeRTOS functions in flash by default (prep for IDF 6.0) --- esphome/components/esp32/__init__.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index 35ef76634b..e5da9a7089 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -584,6 +584,7 @@ CONF_DISABLE_LIBC_LOCKS_IN_IRAM = "disable_libc_locks_in_iram" CONF_DISABLE_VFS_SUPPORT_TERMIOS = "disable_vfs_support_termios" CONF_DISABLE_VFS_SUPPORT_SELECT = "disable_vfs_support_select" CONF_DISABLE_VFS_SUPPORT_DIR = "disable_vfs_support_dir" +CONF_FREERTOS_IN_IRAM = "freertos_in_iram" CONF_LOOP_TASK_STACK_SIZE = "loop_task_stack_size" # VFS requirement tracking @@ -677,6 +678,7 @@ FRAMEWORK_SCHEMA = cv.Schema( cv.Optional(CONF_DISABLE_VFS_SUPPORT_TERMIOS, default=True): cv.boolean, cv.Optional(CONF_DISABLE_VFS_SUPPORT_SELECT, default=True): cv.boolean, cv.Optional(CONF_DISABLE_VFS_SUPPORT_DIR, default=True): cv.boolean, + cv.Optional(CONF_FREERTOS_IN_IRAM, default=False): cv.boolean, cv.Optional(CONF_EXECUTE_FROM_PSRAM, default=False): cv.boolean, cv.Optional(CONF_LOOP_TASK_STACK_SIZE, default=8192): cv.int_range( min=8192, max=32768 @@ -1003,6 +1005,15 @@ async def to_code(config): # Increase freertos tick speed from 100Hz to 1kHz so that delay() resolution is 1ms add_idf_sdkconfig_option("CONFIG_FREERTOS_HZ", 1000) + # Place non-ISR FreeRTOS functions into flash instead of IRAM + # This saves up to 8KB of IRAM. ISR-safe functions (FromISR variants) stay in IRAM. + # In ESP-IDF 6.0 this becomes the default and CONFIG_FREERTOS_PLACE_FUNCTIONS_INTO_FLASH + # is removed. We enable this now to match IDF 6.0 behavior and catch any issues early. + # Users can set freertos_in_iram: true as an escape hatch if they encounter problems + # with code that incorrectly calls FreeRTOS functions from ISRs with cache disabled. + if not conf[CONF_ADVANCED][CONF_FREERTOS_IN_IRAM]: + add_idf_sdkconfig_option("CONFIG_FREERTOS_PLACE_FUNCTIONS_INTO_FLASH", True) + # Setup watchdog add_idf_sdkconfig_option("CONFIG_ESP_TASK_WDT", True) add_idf_sdkconfig_option("CONFIG_ESP_TASK_WDT_PANIC", True) From c40e8e7f5c722cb70e44bf5a3c61eb7a18a55bb9 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Sat, 29 Nov 2025 19:38:29 +1100 Subject: [PATCH 403/415] [helpers] Add conversion from FixedVector to std::vector (#12179) --- esphome/core/helpers.h | 3 +++ 1 file changed, 3 insertions(+) diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index a43c55e06b..83a12b9bf0 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -242,6 +242,9 @@ template class FixedVector { other.reset_(); } + // Allow conversion to std::vector + operator std::vector() const { return {data_, data_ + size_}; } + FixedVector &operator=(FixedVector &&other) noexcept { if (this != &other) { // Delete our current data From cf444fc3b82e68e33715434f39f6b4762f2cf809 Mon Sep 17 00:00:00 2001 From: Javier Peletier Date: Sat, 29 Nov 2025 09:40:13 +0100 Subject: [PATCH 404/415] [mipi_spi] add guition JC4827W543 C/R (#12034) --- esphome/components/mipi_spi/models/jc.py | 105 +++++++++++++++++++++++ 1 file changed, 105 insertions(+) diff --git a/esphome/components/mipi_spi/models/jc.py b/esphome/components/mipi_spi/models/jc.py index 5dbf049ded..5b936fd956 100644 --- a/esphome/components/mipi_spi/models/jc.py +++ b/esphome/components/mipi_spi/models/jc.py @@ -484,4 +484,109 @@ DriverChip( ), ) +DriverChip( + "JC4827W543", + height=272, + width=480, + offset_height=0, + offset_width=0, + cs_pin={CONF_NUMBER: 45, CONF_IGNORE_STRAPPING_WARNING: True}, + invert_colors=True, + color_order=MODE_RGB, + bus_mode=TYPE_QUAD, + data_rate="20MHz", + initsequence=( + (0xFF, 0xA5), + (0x41, 0x03), + (0x44, 0x15), + (0x45, 0x15), + (0x7D, 0x03), + (0xC1, 0xBB), + (0xC2, 0x05), + (0xC3, 0x10), + (0xC6, 0x3E), + (0xC7, 0x25), + (0xC8, 0x11), + (0x7A, 0x5F), + (0x6F, 0x44), + (0x78, 0x70), + (0xC9, 0x00), + (0x67, 0x21), + (0x51, 0x0A), + (0x52, 0x76), + (0x53, 0x0A), + (0x54, 0x76), + (0x46, 0x0A), + (0x47, 0x2A), + (0x48, 0x0A), + (0x49, 0x1A), + (0x56, 0x43), + (0x57, 0x42), + (0x58, 0x3C), + (0x59, 0x64), + (0x5A, 0x41), + (0x5B, 0x3C), + (0x5C, 0x02), + (0x5D, 0x3C), + (0x5E, 0x1F), + (0x60, 0x80), + (0x61, 0x3F), + (0x62, 0x21), + (0x63, 0x07), + (0x64, 0xE0), + (0x65, 0x02), + (0xCA, 0x20), + (0xCB, 0x52), + (0xCC, 0x10), + (0xCD, 0x42), + (0xD0, 0x20), + (0xD1, 0x52), + (0xD2, 0x10), + (0xD3, 0x42), + (0xD4, 0x0A), + (0xD5, 0x32), + (0x80, 0x00), + (0xA0, 0x00), + (0x81, 0x07), + (0xA1, 0x06), + (0x82, 0x02), + (0xA2, 0x01), + (0x86, 0x11), + (0xA6, 0x10), + (0x87, 0x27), + (0xA7, 0x27), + (0x83, 0x37), + (0xA3, 0x37), + (0x84, 0x35), + (0xA4, 0x35), + (0x85, 0x3F), + (0xA5, 0x3F), + (0x88, 0x0B), + (0xA8, 0x0B), + (0x89, 0x14), + (0xA9, 0x14), + (0x8A, 0x1A), + (0xAA, 0x1A), + (0x8B, 0x0A), + (0xAB, 0x0A), + (0x8C, 0x14), + (0xAC, 0x08), + (0x8D, 0x17), + (0xAD, 0x07), + (0x8E, 0x16), + (0xAE, 0x06), + (0x8F, 0x1B), + (0xAF, 0x07), + (0x90, 0x04), + (0xB0, 0x04), + (0x91, 0x0A), + (0xB1, 0x0A), + (0x92, 0x16), + (0xB2, 0x15), + (0xFF, 0x00), + (0x11, 0x00), + (0x29, 0x00), + ), +) + models = {} From e7c54598cdfdc23e60dce14482b5e7bcfa0deb92 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 29 Nov 2025 02:44:27 -0600 Subject: [PATCH 405/415] tweak --- esphome/components/esp32/__init__.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index e5da9a7089..c49fc89fbd 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -1008,10 +1008,17 @@ async def to_code(config): # Place non-ISR FreeRTOS functions into flash instead of IRAM # This saves up to 8KB of IRAM. ISR-safe functions (FromISR variants) stay in IRAM. # In ESP-IDF 6.0 this becomes the default and CONFIG_FREERTOS_PLACE_FUNCTIONS_INTO_FLASH - # is removed. We enable this now to match IDF 6.0 behavior and catch any issues early. + # is removed (replaced by CONFIG_FREERTOS_IN_IRAM to restore old behavior). + # We enable this now to match IDF 6.0 behavior and catch any issues early. # Users can set freertos_in_iram: true as an escape hatch if they encounter problems # with code that incorrectly calls FreeRTOS functions from ISRs with cache disabled. - if not conf[CONF_ADVANCED][CONF_FREERTOS_IN_IRAM]: + if conf[CONF_ADVANCED][CONF_FREERTOS_IN_IRAM]: + # IDF 5.x: don't set the flash option (keeps functions in IRAM) + # IDF 6.0+: will need CONFIG_FREERTOS_IN_IRAM=y to restore IRAM placement + add_idf_sdkconfig_option("CONFIG_FREERTOS_IN_IRAM", True) + else: + # IDF 5.x: explicitly place functions in flash + # IDF 6.0+: this is the default, option no longer exists add_idf_sdkconfig_option("CONFIG_FREERTOS_PLACE_FUNCTIONS_INTO_FLASH", True) # Setup watchdog From 22de35b202328033a6dfade154e6b5c1296b87fc Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 29 Nov 2025 03:17:08 -0600 Subject: [PATCH 406/415] [esp32] Place ring buffer functions in flash by default (prep for IDF 6.0) --- esphome/components/esp32/__init__.py | 26 +++++++++++++++++++ esphome/components/i2s_audio/__init__.py | 9 ++++++- .../components/micro_wake_word/__init__.py | 3 +++ 3 files changed, 37 insertions(+), 1 deletion(-) diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index c49fc89fbd..0c1ea316f2 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -592,6 +592,9 @@ CONF_LOOP_TASK_STACK_SIZE = "loop_task_stack_size" KEY_VFS_SELECT_REQUIRED = "vfs_select_required" KEY_VFS_DIR_REQUIRED = "vfs_dir_required" +# Ring buffer IRAM requirement tracking +KEY_RINGBUF_IN_IRAM = "ringbuf_in_iram" + def require_vfs_select() -> None: """Mark that VFS select support is required by a component. @@ -611,6 +614,17 @@ def require_vfs_dir() -> None: CORE.data[KEY_VFS_DIR_REQUIRED] = True +def enable_ringbuf_in_iram() -> None: + """Keep ring buffer functions in IRAM instead of moving them to flash. + + Call this from components that use esphome/core/ring_buffer.cpp and need + the ring buffer functions to remain in IRAM for performance reasons + (e.g., voice assistants, audio components). + This prevents CONFIG_RINGBUF_PLACE_FUNCTIONS_INTO_FLASH from being enabled. + """ + CORE.data[KEY_RINGBUF_IN_IRAM] = True + + def _parse_idf_component(value: str) -> ConfigType: """Parse IDF component shorthand syntax like 'owner/component^version'""" if "^" not in value: @@ -1021,6 +1035,18 @@ async def to_code(config): # IDF 6.0+: this is the default, option no longer exists add_idf_sdkconfig_option("CONFIG_FREERTOS_PLACE_FUNCTIONS_INTO_FLASH", True) + # Place ring buffer functions into flash instead of IRAM by default + # This saves IRAM but may impact performance for audio/voice components. + # Components that need ring buffer in IRAM call enable_ringbuf_in_iram(). + # In ESP-IDF 6.0 flash placement becomes the default. + if CORE.data.get(KEY_RINGBUF_IN_IRAM, False): + # Component requires ring buffer in IRAM for performance + # IDF 6.0+: will need CONFIG_RINGBUF_PLACE_ISR_FUNCTIONS_INTO_FLASH=n + add_idf_sdkconfig_option("CONFIG_RINGBUF_PLACE_ISR_FUNCTIONS_INTO_FLASH", False) + else: + # No component needs it - place in flash to save IRAM + add_idf_sdkconfig_option("CONFIG_RINGBUF_PLACE_FUNCTIONS_INTO_FLASH", True) + # Setup watchdog add_idf_sdkconfig_option("CONFIG_ESP_TASK_WDT", True) add_idf_sdkconfig_option("CONFIG_ESP_TASK_WDT_PANIC", True) diff --git a/esphome/components/i2s_audio/__init__.py b/esphome/components/i2s_audio/__init__.py index 907429ee0e..0e7c3a5937 100644 --- a/esphome/components/i2s_audio/__init__.py +++ b/esphome/components/i2s_audio/__init__.py @@ -1,6 +1,10 @@ from esphome import pins import esphome.codegen as cg -from esphome.components.esp32 import add_idf_sdkconfig_option, get_esp32_variant +from esphome.components.esp32 import ( + add_idf_sdkconfig_option, + enable_ringbuf_in_iram, + get_esp32_variant, +) from esphome.components.esp32.const import ( VARIANT_ESP32, VARIANT_ESP32C3, @@ -274,6 +278,9 @@ async def to_code(config): # Helps avoid callbacks being skipped due to processor load add_idf_sdkconfig_option("CONFIG_I2S_ISR_IRAM_SAFE", True) + # Keep ring buffer functions in IRAM for audio performance + enable_ringbuf_in_iram() + cg.add(var.set_lrclk_pin(config[CONF_I2S_LRCLK_PIN])) if CONF_I2S_BCLK_PIN in config: cg.add(var.set_bclk_pin(config[CONF_I2S_BCLK_PIN])) diff --git a/esphome/components/micro_wake_word/__init__.py b/esphome/components/micro_wake_word/__init__.py index 575fb97799..aab7fcd5cb 100644 --- a/esphome/components/micro_wake_word/__init__.py +++ b/esphome/components/micro_wake_word/__init__.py @@ -448,6 +448,9 @@ async def to_code(config): # The inference task queues detection events that need immediate processing socket.require_wake_loop_threadsafe() + # Keep ring buffer functions in IRAM for audio performance + esp32.enable_ringbuf_in_iram() + mic_source = await microphone.microphone_source_to_code(config[CONF_MICROPHONE]) cg.add(var.set_microphone_source(mic_source)) From b44abfce5703068e4f2c66c57c542ecee1b094f4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 29 Nov 2025 03:18:39 -0600 Subject: [PATCH 407/415] [esp32] Place ring buffer functions in flash by default (prep for IDF 6.0) --- esphome/components/esp32/__init__.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index 0c1ea316f2..6c5aaca869 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -585,6 +585,7 @@ CONF_DISABLE_VFS_SUPPORT_TERMIOS = "disable_vfs_support_termios" CONF_DISABLE_VFS_SUPPORT_SELECT = "disable_vfs_support_select" CONF_DISABLE_VFS_SUPPORT_DIR = "disable_vfs_support_dir" CONF_FREERTOS_IN_IRAM = "freertos_in_iram" +CONF_RINGBUF_IN_IRAM = "ringbuf_in_iram" CONF_LOOP_TASK_STACK_SIZE = "loop_task_stack_size" # VFS requirement tracking @@ -693,6 +694,7 @@ FRAMEWORK_SCHEMA = cv.Schema( cv.Optional(CONF_DISABLE_VFS_SUPPORT_SELECT, default=True): cv.boolean, cv.Optional(CONF_DISABLE_VFS_SUPPORT_DIR, default=True): cv.boolean, cv.Optional(CONF_FREERTOS_IN_IRAM, default=False): cv.boolean, + cv.Optional(CONF_RINGBUF_IN_IRAM, default=False): cv.boolean, cv.Optional(CONF_EXECUTE_FROM_PSRAM, default=False): cv.boolean, cv.Optional(CONF_LOOP_TASK_STACK_SIZE, default=8192): cv.int_range( min=8192, max=32768 @@ -1038,9 +1040,12 @@ async def to_code(config): # Place ring buffer functions into flash instead of IRAM by default # This saves IRAM but may impact performance for audio/voice components. # Components that need ring buffer in IRAM call enable_ringbuf_in_iram(). + # Users can also set ringbuf_in_iram: true to force IRAM placement. # In ESP-IDF 6.0 flash placement becomes the default. - if CORE.data.get(KEY_RINGBUF_IN_IRAM, False): - # Component requires ring buffer in IRAM for performance + if conf[CONF_ADVANCED][CONF_RINGBUF_IN_IRAM] or CORE.data.get( + KEY_RINGBUF_IN_IRAM, False + ): + # User config or component requires ring buffer in IRAM for performance # IDF 6.0+: will need CONFIG_RINGBUF_PLACE_ISR_FUNCTIONS_INTO_FLASH=n add_idf_sdkconfig_option("CONFIG_RINGBUF_PLACE_ISR_FUNCTIONS_INTO_FLASH", False) else: From 1f47797007419e10046513b7eb49d18fa200d1e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20=C4=8Cerm=C3=A1k?= Date: Sat, 29 Nov 2025 23:26:25 +0100 Subject: [PATCH 408/415] Add MEASUREMENT_ANGLE to SensorStateClass (#12085) --- esphome/components/api/api.proto | 1 + esphome/components/api/api_pb2.h | 1 + esphome/components/api/api_pb2_dump.cpp | 2 ++ esphome/components/sensor/__init__.py | 1 + esphome/components/sensor/sensor.cpp | 2 ++ esphome/components/sensor/sensor.h | 1 + esphome/const.py | 3 +++ tests/components/sensor/common.yaml | 7 +++++++ 8 files changed, 18 insertions(+) diff --git a/esphome/components/api/api.proto b/esphome/components/api/api.proto index 74a8e8ff7f..5450c2536c 100644 --- a/esphome/components/api/api.proto +++ b/esphome/components/api/api.proto @@ -589,6 +589,7 @@ enum SensorStateClass { STATE_CLASS_MEASUREMENT = 1; STATE_CLASS_TOTAL_INCREASING = 2; STATE_CLASS_TOTAL = 3; + STATE_CLASS_MEASUREMENT_ANGLE = 4; } // Deprecated in API version 1.5 diff --git a/esphome/components/api/api_pb2.h b/esphome/components/api/api_pb2.h index 93ece74d85..74d3834bf5 100644 --- a/esphome/components/api/api_pb2.h +++ b/esphome/components/api/api_pb2.h @@ -51,6 +51,7 @@ enum SensorStateClass : uint32_t { STATE_CLASS_MEASUREMENT = 1, STATE_CLASS_TOTAL_INCREASING = 2, STATE_CLASS_TOTAL = 3, + STATE_CLASS_MEASUREMENT_ANGLE = 4, }; #endif enum LogLevel : uint32_t { diff --git a/esphome/components/api/api_pb2_dump.cpp b/esphome/components/api/api_pb2_dump.cpp index a985e052ac..bea7fc53c4 100644 --- a/esphome/components/api/api_pb2_dump.cpp +++ b/esphome/components/api/api_pb2_dump.cpp @@ -179,6 +179,8 @@ template<> const char *proto_enum_to_string(enums::Sens return "STATE_CLASS_TOTAL_INCREASING"; case enums::STATE_CLASS_TOTAL: return "STATE_CLASS_TOTAL"; + case enums::STATE_CLASS_MEASUREMENT_ANGLE: + return "STATE_CLASS_MEASUREMENT_ANGLE"; default: return "UNKNOWN"; } diff --git a/esphome/components/sensor/__init__.py b/esphome/components/sensor/__init__.py index f83226d10f..027d9a69b8 100644 --- a/esphome/components/sensor/__init__.py +++ b/esphome/components/sensor/__init__.py @@ -182,6 +182,7 @@ STATE_CLASSES = { "measurement": StateClasses.STATE_CLASS_MEASUREMENT, "total_increasing": StateClasses.STATE_CLASS_TOTAL_INCREASING, "total": StateClasses.STATE_CLASS_TOTAL, + "measurement_angle": StateClasses.STATE_CLASS_MEASUREMENT_ANGLE, } validate_state_class = cv.enum(STATE_CLASSES, lower=True, space="_") diff --git a/esphome/components/sensor/sensor.cpp b/esphome/components/sensor/sensor.cpp index df6bd644e8..49dc56edaa 100644 --- a/esphome/components/sensor/sensor.cpp +++ b/esphome/components/sensor/sensor.cpp @@ -44,6 +44,8 @@ const LogString *state_class_to_string(StateClass state_class) { return LOG_STR("total_increasing"); case STATE_CLASS_TOTAL: return LOG_STR("total"); + case STATE_CLASS_MEASUREMENT_ANGLE: + return LOG_STR("measurement_angle"); case STATE_CLASS_NONE: default: return LOG_STR(""); diff --git a/esphome/components/sensor/sensor.h b/esphome/components/sensor/sensor.h index a4210e5e6c..5d387a1ad7 100644 --- a/esphome/components/sensor/sensor.h +++ b/esphome/components/sensor/sensor.h @@ -31,6 +31,7 @@ enum StateClass : uint8_t { STATE_CLASS_MEASUREMENT = 1, STATE_CLASS_TOTAL_INCREASING = 2, STATE_CLASS_TOTAL = 3, + STATE_CLASS_MEASUREMENT_ANGLE = 4 }; const LogString *state_class_to_string(StateClass state_class); diff --git a/esphome/const.py b/esphome/const.py index 2b6b60d395..59bf0e8b8a 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1346,6 +1346,9 @@ STATE_CLASS_NONE = "" # The state represents a measurement in present time STATE_CLASS_MEASUREMENT = "measurement" +# The state represents a measurement in present time for angles measured in degrees (°) +STATE_CLASS_MEASUREMENT_ANGLE = "measurement_angle" + # The state represents a total that only increases, a decrease is considered a reset. STATE_CLASS_TOTAL_INCREASING = "total_increasing" diff --git a/tests/components/sensor/common.yaml b/tests/components/sensor/common.yaml index 2180f66da8..1961c98685 100644 --- a/tests/components/sensor/common.yaml +++ b/tests/components/sensor/common.yaml @@ -236,3 +236,10 @@ sensor: - multiply: 2.0 - offset: 10.0 - lambda: return x * 3.0; + + # Testing measurement_angle state class + - platform: template + name: "Angle Sensor" + lambda: return 42.0; + update_interval: 1s + state_class: "measurement_angle" From 46567c47161a53ea48262c0ffde665ba3224885b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 29 Nov 2025 22:55:27 +0000 Subject: [PATCH 409/415] Bump aioesphomeapi from 42.8.0 to 42.9.0 (#12189) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index a5c919e95f..45ae3d5925 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,7 +12,7 @@ platformio==6.1.18 # When updating platformio, also update /docker/Dockerfile esptool==5.1.0 click==8.1.7 esphome-dashboard==20251013.0 -aioesphomeapi==42.8.0 +aioesphomeapi==42.9.0 zeroconf==0.148.0 puremagic==1.30 ruamel.yaml==0.18.16 # dashboard_import From ec88bf0cb12be0159a1aa1a8519fd8a770fc1c6d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 29 Nov 2025 22:56:26 +0000 Subject: [PATCH 410/415] Bump ruff from 0.14.5 to 0.14.7 (#12190) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: J. Nick Koston --- .pre-commit-config.yaml | 2 +- requirements_test.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index b86d00f2aa..412a678d02 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -11,7 +11,7 @@ ci: repos: - repo: https://github.com/astral-sh/ruff-pre-commit # Ruff version. - rev: v0.14.5 + rev: v0.14.7 hooks: # Run the linter. - id: ruff diff --git a/requirements_test.txt b/requirements_test.txt index 7f6d3f8e26..3aec877126 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -1,6 +1,6 @@ pylint==4.0.3 flake8==7.3.0 # also change in .pre-commit-config.yaml when updating -ruff==0.14.5 # also change in .pre-commit-config.yaml when updating +ruff==0.14.7 # also change in .pre-commit-config.yaml when updating pyupgrade==3.21.2 # also change in .pre-commit-config.yaml when updating pre-commit From d82a92b406238162bbd01756cb301776f901a6d9 Mon Sep 17 00:00:00 2001 From: Darsey Litzenberger Date: Sat, 29 Nov 2025 16:41:47 -0700 Subject: [PATCH 411/415] [ade7953_base] Add missing CODEOWNERS (#12181) --- CODEOWNERS | 1 + esphome/components/ade7953_base/__init__.py | 2 ++ 2 files changed, 3 insertions(+) diff --git a/CODEOWNERS b/CODEOWNERS index c6332e3933..7861871323 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -21,6 +21,7 @@ esphome/components/adc128s102/* @DeerMaximum esphome/components/addressable_light/* @justfalter esphome/components/ade7880/* @kpfleming esphome/components/ade7953/* @angelnu +esphome/components/ade7953_base/* @angelnu esphome/components/ade7953_i2c/* @angelnu esphome/components/ade7953_spi/* @angelnu esphome/components/ads1118/* @solomondg1 diff --git a/esphome/components/ade7953_base/__init__.py b/esphome/components/ade7953_base/__init__.py index 42b6c8ba24..4fc35352f9 100644 --- a/esphome/components/ade7953_base/__init__.py +++ b/esphome/components/ade7953_base/__init__.py @@ -24,6 +24,8 @@ from esphome.const import ( UNIT_WATT, ) +CODEOWNERS = ["@angelnu"] + CONF_CURRENT_A = "current_a" CONF_CURRENT_B = "current_b" CONF_ACTIVE_POWER_A = "active_power_a" From 0dc6c6f563f62d52f66163074f6e8a1758ffabc7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 29 Nov 2025 18:19:00 -0600 Subject: [PATCH 412/415] [hlk_fm22x] Fix Action::play method signatures --- esphome/components/hlk_fm22x/hlk_fm22x.h | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/esphome/components/hlk_fm22x/hlk_fm22x.h b/esphome/components/hlk_fm22x/hlk_fm22x.h index 5ecc715ea1..9c981d3c44 100644 --- a/esphome/components/hlk_fm22x/hlk_fm22x.h +++ b/esphome/components/hlk_fm22x/hlk_fm22x.h @@ -189,7 +189,7 @@ template class EnrollmentAction : public Action, public P TEMPLATABLE_VALUE(std::string, name) TEMPLATABLE_VALUE(uint8_t, direction) - void play(Ts... x) override { + void play(const Ts &...x) override { auto name = this->name_.value(x...); auto direction = (HlkFm22xFaceDirection) this->direction_.value(x...); this->parent_->enroll_face(name, direction); @@ -200,7 +200,7 @@ template class DeleteAction : public Action, public Paren public: TEMPLATABLE_VALUE(int16_t, face_id) - void play(Ts... x) override { + void play(const Ts &...x) override { auto face_id = this->face_id_.value(x...); this->parent_->delete_face(face_id); } @@ -208,17 +208,17 @@ template class DeleteAction : public Action, public Paren template class DeleteAllAction : public Action, public Parented { public: - void play(Ts... x) override { this->parent_->delete_all_faces(); } + void play(const Ts &...x) override { this->parent_->delete_all_faces(); } }; template class ScanAction : public Action, public Parented { public: - void play(Ts... x) override { this->parent_->scan_face(); } + void play(const Ts &...x) override { this->parent_->scan_face(); } }; template class ResetAction : public Action, public Parented { public: - void play(Ts... x) override { this->parent_->reset(); } + void play(const Ts &...x) override { this->parent_->reset(); } }; } // namespace esphome::hlk_fm22x From a80435af0f4a5f1b5a520af2f3c8c012c4464bed Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 29 Nov 2025 18:20:18 -0600 Subject: [PATCH 413/415] [lock] Refactor trigger classes to template and add integration tests --- esphome/components/lock/automation.h | 18 ++---- .../fixtures/lock_automations.yaml | 17 ++++++ tests/integration/test_lock_automations.py | 58 +++++++++++++++++++ 3 files changed, 80 insertions(+), 13 deletions(-) create mode 100644 tests/integration/fixtures/lock_automations.yaml create mode 100644 tests/integration/test_lock_automations.py diff --git a/esphome/components/lock/automation.h b/esphome/components/lock/automation.h index cba2c3fdda..011c6cc6af 100644 --- a/esphome/components/lock/automation.h +++ b/esphome/components/lock/automation.h @@ -49,26 +49,18 @@ template class LockCondition : public Condition { bool state_; }; -class LockLockTrigger : public Trigger<> { +template class LockStateTrigger : public Trigger<> { public: - LockLockTrigger(Lock *a_lock) { + explicit LockStateTrigger(Lock *a_lock) { a_lock->add_on_state_callback([this, a_lock]() { - if (a_lock->state == LockState::LOCK_STATE_LOCKED) { + if (a_lock->state == State) { this->trigger(); } }); } }; -class LockUnlockTrigger : public Trigger<> { - public: - LockUnlockTrigger(Lock *a_lock) { - a_lock->add_on_state_callback([this, a_lock]() { - if (a_lock->state == LockState::LOCK_STATE_UNLOCKED) { - this->trigger(); - } - }); - } -}; +using LockLockTrigger = LockStateTrigger; +using LockUnlockTrigger = LockStateTrigger; } // namespace esphome::lock diff --git a/tests/integration/fixtures/lock_automations.yaml b/tests/integration/fixtures/lock_automations.yaml new file mode 100644 index 0000000000..fe11e656fa --- /dev/null +++ b/tests/integration/fixtures/lock_automations.yaml @@ -0,0 +1,17 @@ +esphome: + name: lock-automations-test + +host: +api: # Port will be automatically injected +logger: + level: DEBUG + +lock: + - platform: template + id: test_lock + name: "Test Lock" + optimistic: true + on_lock: + - logger.log: "TRIGGER: on_lock fired" + on_unlock: + - logger.log: "TRIGGER: on_unlock fired" diff --git a/tests/integration/test_lock_automations.py b/tests/integration/test_lock_automations.py new file mode 100644 index 0000000000..e200a2eacd --- /dev/null +++ b/tests/integration/test_lock_automations.py @@ -0,0 +1,58 @@ +"""Integration test for lock automation triggers. + +Tests that on_lock and on_unlock triggers work correctly. +""" + +import asyncio + +import pytest + +from .types import APIClientConnectedFactory, RunCompiledFunction + + +@pytest.mark.asyncio +async def test_lock_automations( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """Test lock on_lock and on_unlock triggers.""" + loop = asyncio.get_running_loop() + + # Futures for log line detection + on_lock_future: asyncio.Future[bool] = loop.create_future() + on_unlock_future: asyncio.Future[bool] = loop.create_future() + + def check_output(line: str) -> None: + """Check log output for trigger messages.""" + if "TRIGGER: on_lock fired" in line and not on_lock_future.done(): + on_lock_future.set_result(True) + elif "TRIGGER: on_unlock fired" in line and not on_unlock_future.done(): + on_unlock_future.set_result(True) + + async with ( + run_compiled(yaml_config, line_callback=check_output), + api_client_connected() as client, + ): + # Import here to avoid import errors when aioesphomeapi is not installed + from aioesphomeapi import LockCommand + + # Get entities + entities = await client.list_entities_services() + lock = next(e for e in entities[0] if e.object_id == "test_lock") + + # Test 1: Lock - should trigger on_lock + client.lock_command(key=lock.key, command=LockCommand.LOCK) + + try: + await asyncio.wait_for(on_lock_future, timeout=5.0) + except TimeoutError: + pytest.fail("on_lock trigger did not fire") + + # Test 2: Unlock - should trigger on_unlock + client.lock_command(key=lock.key, command=LockCommand.UNLOCK) + + try: + await asyncio.wait_for(on_unlock_future, timeout=5.0) + except TimeoutError: + pytest.fail("on_unlock trigger did not fire") From 24f34cf782f4c71f0bd894f7cdf9dcd61e419693 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 29 Nov 2025 18:29:46 -0600 Subject: [PATCH 414/415] [climate] Use C++17 nested namespace syntax --- esphome/components/climate/automation.h | 6 ++---- esphome/components/climate/climate.cpp | 6 ++---- esphome/components/climate/climate.h | 6 ++---- esphome/components/climate/climate_mode.cpp | 6 ++---- esphome/components/climate/climate_mode.h | 6 ++---- esphome/components/climate/climate_traits.cpp | 6 ++---- esphome/components/climate/climate_traits.h | 6 ++---- 7 files changed, 14 insertions(+), 28 deletions(-) diff --git a/esphome/components/climate/automation.h b/esphome/components/climate/automation.h index 36cc8f4f21..fac56d9d9e 100644 --- a/esphome/components/climate/automation.h +++ b/esphome/components/climate/automation.h @@ -3,8 +3,7 @@ #include "esphome/core/automation.h" #include "climate.h" -namespace esphome { -namespace climate { +namespace esphome::climate { template class ControlAction : public Action { public: @@ -58,5 +57,4 @@ class StateTrigger : public Trigger { } }; -} // namespace climate -} // namespace esphome +} // namespace esphome::climate diff --git a/esphome/components/climate/climate.cpp b/esphome/components/climate/climate.cpp index 82b75660ba..b0fba6aa62 100644 --- a/esphome/components/climate/climate.cpp +++ b/esphome/components/climate/climate.cpp @@ -3,8 +3,7 @@ #include "esphome/core/controller_registry.h" #include "esphome/core/macros.h" -namespace esphome { -namespace climate { +namespace esphome::climate { static const char *const TAG = "climate"; @@ -762,5 +761,4 @@ void Climate::dump_traits_(const char *tag) { } } -} // namespace climate -} // namespace esphome +} // namespace esphome::climate diff --git a/esphome/components/climate/climate.h b/esphome/components/climate/climate.h index b277877c3e..28a73d8c05 100644 --- a/esphome/components/climate/climate.h +++ b/esphome/components/climate/climate.h @@ -8,8 +8,7 @@ #include "climate_mode.h" #include "climate_traits.h" -namespace esphome { -namespace climate { +namespace esphome::climate { #define LOG_CLIMATE(prefix, type, obj) \ if ((obj) != nullptr) { \ @@ -345,5 +344,4 @@ class Climate : public EntityBase { const char *custom_preset_{nullptr}; }; -} // namespace climate -} // namespace esphome +} // namespace esphome::climate diff --git a/esphome/components/climate/climate_mode.cpp b/esphome/components/climate/climate_mode.cpp index 794f45ccd6..b153ee0424 100644 --- a/esphome/components/climate/climate_mode.cpp +++ b/esphome/components/climate/climate_mode.cpp @@ -1,7 +1,6 @@ #include "climate_mode.h" -namespace esphome { -namespace climate { +namespace esphome::climate { const LogString *climate_mode_to_string(ClimateMode mode) { switch (mode) { @@ -107,5 +106,4 @@ const LogString *climate_preset_to_string(ClimatePreset preset) { } } -} // namespace climate -} // namespace esphome +} // namespace esphome::climate diff --git a/esphome/components/climate/climate_mode.h b/esphome/components/climate/climate_mode.h index 44423d2f22..c961c44248 100644 --- a/esphome/components/climate/climate_mode.h +++ b/esphome/components/climate/climate_mode.h @@ -3,8 +3,7 @@ #include #include "esphome/core/log.h" -namespace esphome { -namespace climate { +namespace esphome::climate { /// Enum for all modes a climate device can be in. /// NOTE: If adding values, update ClimateModeMask in climate_traits.h to use the new last value @@ -132,5 +131,4 @@ const LogString *climate_swing_mode_to_string(ClimateSwingMode mode); /// Convert the given PresetMode to a human-readable string. const LogString *climate_preset_to_string(ClimatePreset preset); -} // namespace climate -} // namespace esphome +} // namespace esphome::climate diff --git a/esphome/components/climate/climate_traits.cpp b/esphome/components/climate/climate_traits.cpp index 342dffaad6..9bf2d9acd3 100644 --- a/esphome/components/climate/climate_traits.cpp +++ b/esphome/components/climate/climate_traits.cpp @@ -1,7 +1,6 @@ #include "climate_traits.h" -namespace esphome { -namespace climate { +namespace esphome::climate { int8_t ClimateTraits::get_target_temperature_accuracy_decimals() const { return step_to_accuracy_decimals(this->visual_target_temperature_step_); @@ -11,5 +10,4 @@ int8_t ClimateTraits::get_current_temperature_accuracy_decimals() const { return step_to_accuracy_decimals(this->visual_current_temperature_step_); } -} // namespace climate -} // namespace esphome +} // namespace esphome::climate diff --git a/esphome/components/climate/climate_traits.h b/esphome/components/climate/climate_traits.h index 0eecf9789f..d358293475 100644 --- a/esphome/components/climate/climate_traits.h +++ b/esphome/components/climate/climate_traits.h @@ -6,8 +6,7 @@ #include "esphome/core/finite_set_mask.h" #include "esphome/core/helpers.h" -namespace esphome { -namespace climate { +namespace esphome::climate { // Type aliases for climate enum bitmasks // These replace std::set to eliminate red-black tree overhead @@ -292,5 +291,4 @@ class ClimateTraits { std::vector supported_custom_presets_; }; -} // namespace climate -} // namespace esphome +} // namespace esphome::climate From a54a0e54b254d0f499c88e2ba397cba6bff95f8c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 29 Nov 2025 18:34:50 -0600 Subject: [PATCH 415/415] [cover] Store cover state strings in flash on ESP8266 --- esphome/components/cover/cover.cpp | 22 ++++++++++---------- esphome/components/cover/cover.h | 3 ++- esphome/components/web_server/web_server.cpp | 10 +++++---- 3 files changed, 19 insertions(+), 16 deletions(-) diff --git a/esphome/components/cover/cover.cpp b/esphome/components/cover/cover.cpp index 8f735982f1..feac9823b9 100644 --- a/esphome/components/cover/cover.cpp +++ b/esphome/components/cover/cover.cpp @@ -13,25 +13,25 @@ static const char *const TAG = "cover"; const float COVER_OPEN = 1.0f; const float COVER_CLOSED = 0.0f; -const char *cover_command_to_str(float pos) { +const LogString *cover_command_to_str(float pos) { if (pos == COVER_OPEN) { - return "OPEN"; + return LOG_STR("OPEN"); } else if (pos == COVER_CLOSED) { - return "CLOSE"; + return LOG_STR("CLOSE"); } else { - return "UNKNOWN"; + return LOG_STR("UNKNOWN"); } } -const char *cover_operation_to_str(CoverOperation op) { +const LogString *cover_operation_to_str(CoverOperation op) { switch (op) { case COVER_OPERATION_IDLE: - return "IDLE"; + return LOG_STR("IDLE"); case COVER_OPERATION_OPENING: - return "OPENING"; + return LOG_STR("OPENING"); case COVER_OPERATION_CLOSING: - return "CLOSING"; + return LOG_STR("CLOSING"); default: - return "UNKNOWN"; + return LOG_STR("UNKNOWN"); } } @@ -87,7 +87,7 @@ void CoverCall::perform() { if (traits.get_supports_position()) { ESP_LOGD(TAG, " Position: %.0f%%", *this->position_ * 100.0f); } else { - ESP_LOGD(TAG, " Command: %s", cover_command_to_str(*this->position_)); + ESP_LOGD(TAG, " Command: %s", LOG_STR_ARG(cover_command_to_str(*this->position_))); } } if (this->tilt_.has_value()) { @@ -169,7 +169,7 @@ void Cover::publish_state(bool save) { if (traits.get_supports_tilt()) { ESP_LOGD(TAG, " Tilt: %.0f%%", this->tilt * 100.0f); } - ESP_LOGD(TAG, " Current Operation: %s", cover_operation_to_str(this->current_operation)); + ESP_LOGD(TAG, " Current Operation: %s", LOG_STR_ARG(cover_operation_to_str(this->current_operation))); this->state_callback_.call(); #if defined(USE_COVER) && defined(USE_CONTROLLER_REGISTRY) diff --git a/esphome/components/cover/cover.h b/esphome/components/cover/cover.h index 6c69c05e71..d8c45ab2bd 100644 --- a/esphome/components/cover/cover.h +++ b/esphome/components/cover/cover.h @@ -3,6 +3,7 @@ #include "esphome/core/component.h" #include "esphome/core/entity_base.h" #include "esphome/core/helpers.h" +#include "esphome/core/log.h" #include "esphome/core/preferences.h" #include "cover_traits.h" @@ -86,7 +87,7 @@ enum CoverOperation : uint8_t { COVER_OPERATION_CLOSING, }; -const char *cover_operation_to_str(CoverOperation op); +const LogString *cover_operation_to_str(CoverOperation op); /** Base class for all cover devices. * diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index f5ca674161..b02ed8dcb9 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -41,6 +41,10 @@ namespace web_server { static const char *const TAG = "web_server"; +// Longest: HORIZONTAL (10 chars + null terminator, rounded up) +static constexpr size_t PSTR_LOCAL_SIZE = 16; +#define PSTR_LOCAL(mode_s) ESPHOME_strncpy_P(buf, (ESPHOME_PGM_P) ((mode_s)), PSTR_LOCAL_SIZE - 1) + #ifdef USE_WEBSERVER_PRIVATE_NETWORK_ACCESS static const char *const HEADER_PNA_NAME = "Private-Network-Access-Name"; static const char *const HEADER_PNA_ID = "Private-Network-Access-ID"; @@ -908,7 +912,8 @@ std::string WebServer::cover_json(cover::Cover *obj, JsonDetail start_config) { set_json_icon_state_value(root, obj, "cover", obj->is_fully_closed() ? "CLOSED" : "OPEN", obj->position, start_config); - root["current_operation"] = cover::cover_operation_to_str(obj->current_operation); + char buf[PSTR_LOCAL_SIZE]; + root["current_operation"] = PSTR_LOCAL(cover::cover_operation_to_str(obj->current_operation)); if (obj->get_traits().get_supports_position()) root["position"] = obj->position; @@ -1272,9 +1277,6 @@ std::string WebServer::select_json(select::Select *obj, const char *value, JsonD } #endif -// Longest: HORIZONTAL -#define PSTR_LOCAL(mode_s) ESPHOME_strncpy_P(buf, (ESPHOME_PGM_P) ((mode_s)), 15) - #ifdef USE_CLIMATE void WebServer::on_climate_update(climate::Climate *obj) { if (!this->include_internal_ && obj->is_internal())