From da0b01f4d01a81db99fbed91d5fb9e0c5ac31a13 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 8 Jan 2026 10:51:18 -1000 Subject: [PATCH 1/9] [logger] Enable loop disable optimization for LibreTiny task log buffer (#13078) --- esphome/components/logger/logger.cpp | 6 +++--- esphome/components/logger/logger.h | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/esphome/components/logger/logger.cpp b/esphome/components/logger/logger.cpp index bb00a230ee..1b41bc3d47 100644 --- a/esphome/components/logger/logger.cpp +++ b/esphome/components/logger/logger.cpp @@ -197,8 +197,8 @@ void Logger::init_log_buffer(size_t total_buffer_size) { this->log_buffer_ = esphome::make_unique(total_buffer_size); #endif -#ifdef USE_ESP32 - // Start with loop disabled when using task buffer (unless using USB CDC) +#if defined(USE_ESP32) || defined(USE_LIBRETINY) + // Start with loop disabled when using task buffer (unless using USB CDC on ESP32) // The loop will be enabled automatically when messages arrive this->disable_loop_when_buffer_empty_(); #endif @@ -247,7 +247,7 @@ void Logger::process_messages_() { } #endif } -#ifdef USE_ESP32 +#if defined(USE_ESP32) || defined(USE_LIBRETINY) else { // No messages to process, disable loop if appropriate // This reduces overhead when there's no async logging activity diff --git a/esphome/components/logger/logger.h b/esphome/components/logger/logger.h index 79299c2b1c..c58ca8ddce 100644 --- a/esphome/components/logger/logger.h +++ b/esphome/components/logger/logger.h @@ -609,8 +609,8 @@ class Logger : public Component { this->write_body_to_buffer_(ESPHOME_LOG_RESET_COLOR, RESET_COLOR_LEN, buffer, buffer_at, buffer_size); } -#ifdef USE_ESP32 - // Disable loop when task buffer is empty (with USB CDC check) +#if defined(USE_ESP32) || defined(USE_LIBRETINY) + // Disable loop when task buffer is empty (with USB CDC check on ESP32) inline void disable_loop_when_buffer_empty_() { // Thread safety note: This is safe even if another task calls enable_loop_soon_any_context() // concurrently. If that happens between our check and disable_loop(), the enable request From c9ab4ca0181df99681fbea08e74bbf8fa88ec884 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 8 Jan 2026 10:51:35 -1000 Subject: [PATCH 2/9] [libretiny] Bump to 1.9.2 (#13077) --- .clang-tidy.hash | 2 +- esphome/components/libretiny/__init__.py | 6 +++--- platformio.ini | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.clang-tidy.hash b/.clang-tidy.hash index 0a71b6859f..9661c2ca02 100644 --- a/.clang-tidy.hash +++ b/.clang-tidy.hash @@ -1 +1 @@ -191a0e6ab5842d153dd77a2023bc5742f9d4333c334de8d81b57f2b8d4d4b65e +d272a88e8ca28ae9340a9a03295a566432a52cb696501908f57764475bf7ca65 diff --git a/esphome/components/libretiny/__init__.py b/esphome/components/libretiny/__init__.py index 93b66888da..4c8a1999f9 100644 --- a/esphome/components/libretiny/__init__.py +++ b/esphome/components/libretiny/__init__.py @@ -174,9 +174,9 @@ def _notify_old_style(config): # The dev and latest branches will be at *least* this version, which is what matters. ARDUINO_VERSIONS = { - "dev": (cv.Version(1, 9, 1), "https://github.com/libretiny-eu/libretiny.git"), - "latest": (cv.Version(1, 9, 1), "libretiny"), - "recommended": (cv.Version(1, 9, 1), None), + "dev": (cv.Version(1, 9, 2), "https://github.com/libretiny-eu/libretiny.git"), + "latest": (cv.Version(1, 9, 2), "libretiny"), + "recommended": (cv.Version(1, 9, 2), None), } diff --git a/platformio.ini b/platformio.ini index d96e9ad2cc..4180971b54 100644 --- a/platformio.ini +++ b/platformio.ini @@ -212,7 +212,7 @@ build_unflags = ; This are common settings for the LibreTiny (all variants) using Arduino. [common:libretiny-arduino] extends = common:arduino -platform = libretiny@1.9.1 +platform = libretiny@1.9.2 framework = arduino lib_compat_mode = soft lib_deps = From eb5c4f34e2199fe3de3bab7f875be57a4653d29d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 8 Jan 2026 10:51:58 -1000 Subject: [PATCH 3/9] [wifi] Disable SoftAP support on Arduino ESP32 when ap: not configured (#13076) --- 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 824944d4a2..7ba1b5e417 100644 --- a/esphome/components/wifi/__init__.py +++ b/esphome/components/wifi/__init__.py @@ -466,7 +466,7 @@ async def to_code(config): ) cg.add(var.set_ap_timeout(conf[CONF_AP_TIMEOUT])) cg.add_define("USE_WIFI_AP") - elif CORE.is_esp32 and not CORE.using_arduino: + elif CORE.is_esp32: add_idf_sdkconfig_option("CONFIG_ESP_WIFI_SOFTAP_SUPPORT", False) add_idf_sdkconfig_option("CONFIG_LWIP_DHCPS", False) From 423a617b156e5d03716d8cb3ba5e616645c1767c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 8 Jan 2026 10:52:27 -1000 Subject: [PATCH 4/9] [core] Improve minimum_chip_revision warning for PSRAM users (#13074) --- esphome/core/application.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/esphome/core/application.cpp b/esphome/core/application.cpp index f8fa3b333e..55eb25ce09 100644 --- a/esphome/core/application.cpp +++ b/esphome/core/application.cpp @@ -215,8 +215,13 @@ void Application::loop() { #if defined(USE_ESP32_VARIANT_ESP32) && !defined(USE_ESP32_MIN_CHIP_REVISION_SET) // Suggest optimization for chips that don't need the PSRAM cache workaround if (chip_info.revision >= 300) { +#ifdef USE_PSRAM + ESP_LOGW(TAG, "Set minimum_chip_revision: \"%d.%d\" to save ~10KB IRAM", chip_info.revision / 100, + chip_info.revision % 100); +#else ESP_LOGW(TAG, "Set minimum_chip_revision: \"%d.%d\" to reduce binary size", chip_info.revision / 100, chip_info.revision % 100); +#endif } #endif #endif From 325c9380749d1a7344cf52ce606ef7454a1dd2e5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 8 Jan 2026 20:57:30 +0000 Subject: [PATCH 5/9] Bump ruff from 0.14.10 to 0.14.11 (#13082) 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 de7d30cfa2..3295cf070a 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.10 + rev: v0.14.11 hooks: # Run the linter. - id: ruff diff --git a/requirements_test.txt b/requirements_test.txt index f00bcd0a0d..092a06fd66 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -1,6 +1,6 @@ pylint==4.0.4 flake8==7.3.0 # also change in .pre-commit-config.yaml when updating -ruff==0.14.10 # also change in .pre-commit-config.yaml when updating +ruff==0.14.11 # 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 52459d1bc7144d10a1bd63fdd6820d7b00ae7614 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 8 Jan 2026 11:42:06 -1000 Subject: [PATCH 6/9] [wifi] Fix infinite roaming when best-signal AP is crashed/broken (#13071) --- esphome/components/wifi/wifi_component.cpp | 146 ++++++++++++--------- esphome/components/wifi/wifi_component.h | 15 ++- 2 files changed, 96 insertions(+), 65 deletions(-) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index 6654474329..afdaa0b6e8 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -151,48 +151,51 @@ static const char *const TAG = "wifi"; /// │ Purpose: Handle AP reboot or power loss scenarios where device │ /// │ connects to suboptimal AP and never switches back │ /// │ │ -/// │ Loop call site: roaming enabled && attempts < 3 && 5 min elapsed │ -/// │ ↓ │ -/// │ ┌─────────────────┐ Hidden? ┌──────────────────────────┐ │ -/// │ │ check_roaming_ ├───────────→│ attempts = MAX, stop │ │ -/// │ └────────┬────────┘ └──────────────────────────┘ │ -/// │ ↓ │ -/// │ attempts++, update last_check │ -/// │ ↓ │ -/// │ RSSI > -49 dBm? ────Yes────→ Skip scan (excellent signal)─┐ │ -/// │ ↓ No │ │ -/// │ ┌─────────────────┐ │ │ -/// │ │ Start scan │ │ │ -/// │ └────────┬────────┘ │ │ -/// │ ↓ │ │ -/// │ ┌────────────────────────┐ │ │ -/// │ │ process_roaming_scan_ │ │ │ -/// │ └────────┬───────────────┘ │ │ -/// │ ↓ │ │ -/// │ ┌─────────────────┐ No ┌───────────────┐ │ │ -/// │ │ +10 dB better AP├────────→│ Stay connected│───────────────┤ │ -/// │ └────────┬────────┘ └───────────────┘ │ │ -/// │ │ Yes │ │ -/// │ ↓ │ │ -/// │ ┌─────────────────┐ │ │ -/// │ │ start_connecting│ (roaming_connect_active_ = true) │ │ -/// │ └────────┬────────┘ │ │ -/// │ ↓ │ │ -/// │ ┌────┴────┐ │ │ -/// │ ↓ ↓ │ │ -/// │ ┌───────┐ ┌───────┐ │ │ -/// │ │SUCCESS│ │FAILED │ │ │ -/// │ └───┬───┘ └───┬───┘ │ │ -/// │ ↓ ↓ │ │ -/// │ Keep counter retry_connect() → normal reconnect flow │ │ -/// │ (no reset) (keeps counter, handles retries) │ │ -/// │ │ │ │ │ -/// │ └──────────────┴────────────────────────────────────────┘ │ +/// │ State Machine (RoamingState): │ /// │ │ -/// │ After 3 checks: attempts >= 3, stop checking │ -/// │ Non-roaming disconnect: clear_roaming_state_() resets counter │ -/// │ Roaming success: counter preserved (prevents ping-pong) │ -/// │ Roaming fail: normal flow handles reconnection, counter preserved │ +/// │ ┌─────────────────────────────────────────────────────────────┐ │ +/// │ │ IDLE │ │ +/// │ │ (waiting for 5 min timer, attempts < 3) │ │ +/// │ └─────────────────────────┬───────────────────────────────────┘ │ +/// │ │ 5 min elapsed, RSSI < -49 dBm │ +/// │ ↓ │ +/// │ ┌─────────────────────────────────────────────────────────────┐ │ +/// │ │ SCANNING │ │ +/// │ │ (attempts++ in check_roaming_ before entering this state) │ │ +/// │ └─────────────────────────┬───────────────────────────────────┘ │ +/// │ │ │ +/// │ ┌──────────────┼──────────────┐ │ +/// │ ↓ ↓ ↓ │ +/// │ scan error no better AP +10 dB better AP │ +/// │ │ │ │ │ +/// │ ↓ ↓ ↓ │ +/// │ ┌──────────────────────────────┐ ┌──────────────────────────┐ │ +/// │ │ → IDLE │ │ CONNECTING │ │ +/// │ │ (counter preserved) │ │ (process_roaming_scan_) │ │ +/// │ └──────────────────────────────┘ └────────────┬─────────────┘ │ +/// │ │ │ +/// │ ┌───────────────────┴───────────────┐ │ +/// │ ↓ ↓ │ +/// │ SUCCESS FAILED │ +/// │ │ │ │ +/// │ ↓ ↓ │ +/// │ ┌──────────────────────────────────┐ ┌─────────────────────────┐ +/// │ │ → IDLE │ │ RECONNECTING │ +/// │ │ (counter reset to 0) │ │ (retry_connect called) │ +/// │ └──────────────────────────────────┘ └───────────┬─────────────┘ +/// │ │ │ +/// │ ↓ │ +/// │ ┌───────────────────────┐ │ +/// │ │ → IDLE │ │ +/// │ │ (counter preserved!) │ │ +/// │ └───────────────────────┘ │ +/// │ │ +/// │ Key behaviors: │ +/// │ - After 3 checks: attempts >= 3, stop checking │ +/// │ - Non-roaming disconnect: clear_roaming_state_() resets counter │ +/// │ - Scan error (SCANNING→IDLE): counter preserved │ +/// │ - Roaming success (CONNECTING→IDLE): counter reset (can roam again) │ +/// │ - Roaming fail (RECONNECTING→IDLE): counter preserved (ping-pong) │ /// └──────────────────────────────────────────────────────────────────────┘ static const LogString *retry_phase_to_log_string(WiFiRetryPhase phase) { @@ -574,12 +577,12 @@ void WiFiComponent::loop() { // Post-connect roaming: check for better AP if (this->post_connect_roaming_) { - if (this->roaming_scan_active_) { + if (this->roaming_state_ == RoamingState::SCANNING) { if (this->scan_done_) { this->process_roaming_scan_(); } // else: scan in progress, wait - } else if (this->roaming_attempts_ < ROAMING_MAX_ATTEMPTS && + } else if (this->roaming_state_ == RoamingState::IDLE && this->roaming_attempts_ < ROAMING_MAX_ATTEMPTS && now - this->roaming_last_check_ >= ROAMING_CHECK_INTERVAL) { this->check_roaming_(now); } @@ -1302,12 +1305,20 @@ void WiFiComponent::check_connecting_finished(uint32_t now) { // Reset roaming state on successful connection this->roaming_last_check_ = now; - // Only reset attempts if this wasn't a roaming-triggered connection - // (prevents ping-pong between APs) - if (!this->roaming_connect_active_) { + // Only preserve attempts if reconnecting after a failed roam attempt + // This prevents ping-pong between APs when a roam target is unreachable + if (this->roaming_state_ == RoamingState::CONNECTING) { + // Successful roam to better AP - reset attempts so we can roam again later + ESP_LOGD(TAG, "Roam successful"); + this->roaming_attempts_ = 0; + } else if (this->roaming_state_ == RoamingState::RECONNECTING) { + // Failed roam, reconnected via normal recovery - keep attempts to prevent ping-pong + ESP_LOGD(TAG, "Reconnected after failed roam (attempt %u/%u)", this->roaming_attempts_, ROAMING_MAX_ATTEMPTS); + } else { + // Normal connection (boot, credentials changed, etc.) this->roaming_attempts_ = 0; } - this->roaming_connect_active_ = false; + this->roaming_state_ = RoamingState::IDLE; // Clear all priority penalties - the next reconnect will happen when an AP disconnects, // which means the landscape has likely changed and previous tracked failures are stale @@ -1733,16 +1744,21 @@ void WiFiComponent::advance_to_next_target_or_increment_retry_() { } void WiFiComponent::retry_connect() { - // If this was a roaming attempt, preserve roaming_attempts_ count - // (so we stop roaming after ROAMING_MAX_ATTEMPTS failures) - // Otherwise reset all roaming state - if (this->roaming_connect_active_) { - this->roaming_connect_active_ = false; - this->roaming_scan_active_ = false; - // Keep roaming_attempts_ - will prevent further roaming after max failures - } else { + // Handle roaming state transitions - preserve attempts counter to prevent ping-pong + // to unreachable APs after ROAMING_MAX_ATTEMPTS failures + if (this->roaming_state_ == RoamingState::CONNECTING) { + // Roam connection failed - transition to reconnecting + ESP_LOGD(TAG, "Roam failed, reconnecting (attempt %u/%u)", this->roaming_attempts_, ROAMING_MAX_ATTEMPTS); + this->roaming_state_ = RoamingState::RECONNECTING; + } else if (this->roaming_state_ == RoamingState::SCANNING) { + // Roam scan failed (e.g., scan error on ESP8266) - go back to idle, keep counter + ESP_LOGD(TAG, "Roam scan failed (attempt %u/%u)", this->roaming_attempts_, ROAMING_MAX_ATTEMPTS); + this->roaming_state_ = RoamingState::IDLE; + } else if (this->roaming_state_ == RoamingState::IDLE) { + // Not a roaming-triggered reconnect, reset state this->clear_roaming_state_(); } + // RECONNECTING: keep state and counter, still trying to reconnect this->log_and_adjust_priority_for_failed_connect_(); @@ -1989,8 +2005,7 @@ bool WiFiScanResult::operator==(const WiFiScanResult &rhs) const { return this-> void WiFiComponent::clear_roaming_state_() { this->roaming_attempts_ = 0; this->roaming_last_check_ = 0; - this->roaming_scan_active_ = false; - this->roaming_connect_active_ = false; + this->roaming_state_ = RoamingState::IDLE; } void WiFiComponent::release_scan_results_() { @@ -2018,17 +2033,21 @@ void WiFiComponent::check_roaming_(uint32_t now) { // Guard: skip scan if signal is already good (no meaningful improvement possible) int8_t rssi = this->wifi_rssi(); - if (rssi > ROAMING_GOOD_RSSI) + if (rssi > ROAMING_GOOD_RSSI) { + ESP_LOGV(TAG, "Roam check skipped, signal good (%d dBm, attempt %u/%u)", rssi, this->roaming_attempts_, + ROAMING_MAX_ATTEMPTS); return; + } - ESP_LOGD(TAG, "Roam scan (%d dBm)", rssi); - this->roaming_scan_active_ = true; + ESP_LOGD(TAG, "Roam scan (%d dBm, attempt %u/%u)", rssi, this->roaming_attempts_, ROAMING_MAX_ATTEMPTS); + this->roaming_state_ = RoamingState::SCANNING; this->wifi_scan_start_(this->passive_scan_); } void WiFiComponent::process_roaming_scan_() { this->scan_done_ = false; - this->roaming_scan_active_ = false; + // Default to IDLE - will be set to CONNECTING if we find a better AP + this->roaming_state_ = RoamingState::IDLE; // Get current connection info int8_t current_rssi = this->wifi_rssi(); @@ -2066,7 +2085,8 @@ void WiFiComponent::process_roaming_scan_() { const WiFiAP *selected = this->get_selected_sta_(); int8_t improvement = (best == nullptr) ? 0 : best->get_rssi() - current_rssi; if (selected == nullptr || improvement < ROAMING_MIN_IMPROVEMENT) { - ESP_LOGV(TAG, "Roam best %+d dB (need +%d)", improvement, ROAMING_MIN_IMPROVEMENT); + ESP_LOGV(TAG, "Roam best %+d dB (need +%d), attempt %u/%u", improvement, ROAMING_MIN_IMPROVEMENT, + this->roaming_attempts_, ROAMING_MAX_ATTEMPTS); this->release_scan_results_(); return; } @@ -2079,7 +2099,7 @@ void WiFiComponent::process_roaming_scan_() { this->release_scan_results_(); // Mark as roaming attempt - affects retry behavior if connection fails - this->roaming_connect_active_ = true; + this->roaming_state_ = RoamingState::CONNECTING; // Connect directly - wifi_sta_connect_ handles disconnect internally this->error_from_callback_ = false; diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index 09af384725..9b606bd692 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -112,6 +112,18 @@ enum class WiFiRetryPhase : uint8_t { RESTARTING_ADAPTER, }; +/// Tracks post-connect roaming state machine +enum class RoamingState : uint8_t { + /// Not roaming, waiting for next check interval + IDLE, + /// Scanning for better AP + SCANNING, + /// Attempting to connect to better AP found in scan + CONNECTING, + /// Roam connection failed, reconnecting to any available AP + RECONNECTING, +}; + /// Struct for setting static IPs in WiFiComponent. struct ManualIP { network::IPAddress static_ip; @@ -667,8 +679,7 @@ class WiFiComponent : public Component { bool did_scan_this_cycle_{false}; bool skip_cooldown_next_cycle_{false}; bool post_connect_roaming_{true}; // Enabled by default - bool roaming_scan_active_{false}; - bool roaming_connect_active_{false}; // True during roaming connection attempt (preserves roaming_attempts_) + RoamingState roaming_state_{RoamingState::IDLE}; #if defined(USE_ESP32) && defined(USE_WIFI_RUNTIME_POWER_SAVE) WiFiPowerSaveMode configured_power_save_{WIFI_POWER_SAVE_NONE}; bool is_high_performance_mode_{false}; From 40f108116b0b4dd28f0bc701acb7138eb38145a8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 8 Jan 2026 11:42:18 -1000 Subject: [PATCH 7/9] [mqtt] Reduce heap allocations in topic string building (#13072) --- esphome/components/mqtt/__init__.py | 17 +++- .../mqtt/mqtt_alarm_control_panel.cpp | 2 +- .../mqtt/mqtt_alarm_control_panel.h | 2 +- .../components/mqtt/mqtt_binary_sensor.cpp | 2 +- esphome/components/mqtt/mqtt_binary_sensor.h | 2 +- esphome/components/mqtt/mqtt_button.cpp | 2 +- esphome/components/mqtt/mqtt_button.h | 2 +- esphome/components/mqtt/mqtt_climate.cpp | 2 +- esphome/components/mqtt/mqtt_climate.h | 2 +- esphome/components/mqtt/mqtt_component.cpp | 77 ++++++++++++++++--- esphome/components/mqtt/mqtt_component.h | 21 ++++- esphome/components/mqtt/mqtt_cover.cpp | 2 +- esphome/components/mqtt/mqtt_cover.h | 2 +- esphome/components/mqtt/mqtt_date.cpp | 2 +- esphome/components/mqtt/mqtt_date.h | 2 +- esphome/components/mqtt/mqtt_datetime.cpp | 2 +- esphome/components/mqtt/mqtt_datetime.h | 2 +- esphome/components/mqtt/mqtt_event.cpp | 2 +- esphome/components/mqtt/mqtt_event.h | 2 +- esphome/components/mqtt/mqtt_fan.cpp | 2 +- esphome/components/mqtt/mqtt_fan.h | 2 +- esphome/components/mqtt/mqtt_light.cpp | 2 +- esphome/components/mqtt/mqtt_light.h | 2 +- esphome/components/mqtt/mqtt_lock.cpp | 2 +- esphome/components/mqtt/mqtt_lock.h | 2 +- esphome/components/mqtt/mqtt_number.cpp | 2 +- esphome/components/mqtt/mqtt_number.h | 2 +- esphome/components/mqtt/mqtt_select.cpp | 2 +- esphome/components/mqtt/mqtt_select.h | 2 +- esphome/components/mqtt/mqtt_sensor.cpp | 2 +- esphome/components/mqtt/mqtt_sensor.h | 2 +- esphome/components/mqtt/mqtt_switch.cpp | 2 +- esphome/components/mqtt/mqtt_switch.h | 2 +- esphome/components/mqtt/mqtt_text.cpp | 2 +- esphome/components/mqtt/mqtt_text.h | 2 +- esphome/components/mqtt/mqtt_text_sensor.cpp | 2 +- esphome/components/mqtt/mqtt_text_sensor.h | 2 +- esphome/components/mqtt/mqtt_time.cpp | 2 +- esphome/components/mqtt/mqtt_time.h | 2 +- esphome/components/mqtt/mqtt_update.cpp | 2 +- esphome/components/mqtt/mqtt_update.h | 2 +- esphome/components/mqtt/mqtt_valve.cpp | 2 +- esphome/components/mqtt/mqtt_valve.h | 2 +- esphome/core/config.py | 2 + esphome/core/entity_base.h | 5 +- 45 files changed, 145 insertions(+), 57 deletions(-) diff --git a/esphome/components/mqtt/__init__.py b/esphome/components/mqtt/__init__.py index e73de49fef..f01c928b30 100644 --- a/esphome/components/mqtt/__init__.py +++ b/esphome/components/mqtt/__init__.py @@ -77,6 +77,13 @@ CONF_DISCOVER_IP = "discover_ip" CONF_IDF_SEND_ASYNC = "idf_send_async" CONF_WAIT_FOR_CONNECTION = "wait_for_connection" +# Max lengths for stack-based topic building. +# These values are used in cv.Length() validators below to ensure the C++ code +# in mqtt_component.cpp can safely use fixed-size stack buffers without overflow. +# If you change these, update the corresponding constants in mqtt_component.cpp. +TOPIC_PREFIX_MAX_LEN = 64 # Default is device name, typically short +DISCOVERY_PREFIX_MAX_LEN = 64 # Default is "homeassistant" (13 chars) + def validate_message_just_topic(value): value = cv.publish_topic(value) @@ -253,9 +260,9 @@ CONFIG_SCHEMA = cv.All( ), cv.Optional(CONF_DISCOVERY_RETAIN, default=True): cv.boolean, cv.Optional(CONF_DISCOVER_IP, default=True): cv.boolean, - cv.Optional( - CONF_DISCOVERY_PREFIX, default="homeassistant" - ): cv.publish_topic, + cv.Optional(CONF_DISCOVERY_PREFIX, default="homeassistant"): cv.All( + cv.publish_topic, cv.Length(max=DISCOVERY_PREFIX_MAX_LEN) + ), cv.Optional(CONF_DISCOVERY_UNIQUE_ID_GENERATOR, default="legacy"): cv.enum( MQTT_DISCOVERY_UNIQUE_ID_GENERATOR_OPTIONS ), @@ -266,7 +273,9 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_BIRTH_MESSAGE): MQTT_MESSAGE_SCHEMA, cv.Optional(CONF_WILL_MESSAGE): MQTT_MESSAGE_SCHEMA, cv.Optional(CONF_SHUTDOWN_MESSAGE): MQTT_MESSAGE_SCHEMA, - cv.Optional(CONF_TOPIC_PREFIX, default=lambda: CORE.name): cv.publish_topic, + cv.Optional(CONF_TOPIC_PREFIX, default=lambda: CORE.name): cv.All( + cv.publish_topic, cv.Length(max=TOPIC_PREFIX_MAX_LEN) + ), cv.Optional(CONF_LOG_TOPIC): cv.Any( None, MQTT_MESSAGE_BASE.extend( diff --git a/esphome/components/mqtt/mqtt_alarm_control_panel.cpp b/esphome/components/mqtt/mqtt_alarm_control_panel.cpp index eb46c3b10c..6245d10882 100644 --- a/esphome/components/mqtt/mqtt_alarm_control_panel.cpp +++ b/esphome/components/mqtt/mqtt_alarm_control_panel.cpp @@ -79,7 +79,7 @@ void MQTTAlarmControlPanelComponent::send_discovery(JsonObject root, mqtt::SendD root[MQTT_CODE_ARM_REQUIRED] = this->alarm_control_panel_->get_requires_code_to_arm(); } -std::string MQTTAlarmControlPanelComponent::component_type() const { return "alarm_control_panel"; } +MQTT_COMPONENT_TYPE(MQTTAlarmControlPanelComponent, "alarm_control_panel") const EntityBase *MQTTAlarmControlPanelComponent::get_entity() const { return this->alarm_control_panel_; } bool MQTTAlarmControlPanelComponent::send_initial_state() { return this->publish_state(); } diff --git a/esphome/components/mqtt/mqtt_alarm_control_panel.h b/esphome/components/mqtt/mqtt_alarm_control_panel.h index cf4fac1511..89a0ff1be8 100644 --- a/esphome/components/mqtt/mqtt_alarm_control_panel.h +++ b/esphome/components/mqtt/mqtt_alarm_control_panel.h @@ -25,7 +25,7 @@ class MQTTAlarmControlPanelComponent : public mqtt::MQTTComponent { void dump_config() override; protected: - std::string component_type() const override; + const char *component_type() const override; const EntityBase *get_entity() const override; alarm_control_panel::AlarmControlPanel *alarm_control_panel_; diff --git a/esphome/components/mqtt/mqtt_binary_sensor.cpp b/esphome/components/mqtt/mqtt_binary_sensor.cpp index 146ca46f68..a37043406b 100644 --- a/esphome/components/mqtt/mqtt_binary_sensor.cpp +++ b/esphome/components/mqtt/mqtt_binary_sensor.cpp @@ -10,7 +10,7 @@ namespace esphome::mqtt { static const char *const TAG = "mqtt.binary_sensor"; -std::string MQTTBinarySensorComponent::component_type() const { return "binary_sensor"; } +MQTT_COMPONENT_TYPE(MQTTBinarySensorComponent, "binary_sensor") const EntityBase *MQTTBinarySensorComponent::get_entity() const { return this->binary_sensor_; } void MQTTBinarySensorComponent::setup() { diff --git a/esphome/components/mqtt/mqtt_binary_sensor.h b/esphome/components/mqtt/mqtt_binary_sensor.h index 82176ec97b..5917a9966c 100644 --- a/esphome/components/mqtt/mqtt_binary_sensor.h +++ b/esphome/components/mqtt/mqtt_binary_sensor.h @@ -29,7 +29,7 @@ class MQTTBinarySensorComponent : public mqtt::MQTTComponent { bool publish_state(bool state); protected: - std::string component_type() const override; + const char *component_type() const override; const EntityBase *get_entity() const override; binary_sensor::BinarySensor *binary_sensor_; diff --git a/esphome/components/mqtt/mqtt_button.cpp b/esphome/components/mqtt/mqtt_button.cpp index 2b700a4962..718fe93016 100644 --- a/esphome/components/mqtt/mqtt_button.cpp +++ b/esphome/components/mqtt/mqtt_button.cpp @@ -39,7 +39,7 @@ void MQTTButtonComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryCon // NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks) } -std::string MQTTButtonComponent::component_type() const { return "button"; } +MQTT_COMPONENT_TYPE(MQTTButtonComponent, "button") const EntityBase *MQTTButtonComponent::get_entity() const { return this->button_; } } // namespace esphome::mqtt diff --git a/esphome/components/mqtt/mqtt_button.h b/esphome/components/mqtt/mqtt_button.h index ec802664df..a2db64d39d 100644 --- a/esphome/components/mqtt/mqtt_button.h +++ b/esphome/components/mqtt/mqtt_button.h @@ -26,7 +26,7 @@ class MQTTButtonComponent : public mqtt::MQTTComponent { protected: /// "button" component type. - std::string component_type() const override; + const char *component_type() const override; const EntityBase *get_entity() const override; button::Button *button_; diff --git a/esphome/components/mqtt/mqtt_climate.cpp b/esphome/components/mqtt/mqtt_climate.cpp index d402fff6e6..77aabb2461 100644 --- a/esphome/components/mqtt/mqtt_climate.cpp +++ b/esphome/components/mqtt/mqtt_climate.cpp @@ -254,7 +254,7 @@ void MQTTClimateComponent::setup() { } MQTTClimateComponent::MQTTClimateComponent(Climate *device) : device_(device) {} bool MQTTClimateComponent::send_initial_state() { return this->publish_state_(); } -std::string MQTTClimateComponent::component_type() const { return "climate"; } +MQTT_COMPONENT_TYPE(MQTTClimateComponent, "climate") const EntityBase *MQTTClimateComponent::get_entity() const { return this->device_; } bool MQTTClimateComponent::publish_state_() { diff --git a/esphome/components/mqtt/mqtt_climate.h b/esphome/components/mqtt/mqtt_climate.h index f561627ac9..f0715929d4 100644 --- a/esphome/components/mqtt/mqtt_climate.h +++ b/esphome/components/mqtt/mqtt_climate.h @@ -15,7 +15,7 @@ class MQTTClimateComponent : public mqtt::MQTTComponent { MQTTClimateComponent(climate::Climate *device); void send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) override; bool send_initial_state() override; - std::string component_type() const override; + const char *component_type() const override; void setup() override; MQTT_COMPONENT_CUSTOM_TOPIC(current_temperature, state) diff --git a/esphome/components/mqtt/mqtt_component.cpp b/esphome/components/mqtt/mqtt_component.cpp index ccbdb2ea91..d838d1789f 100644 --- a/esphome/components/mqtt/mqtt_component.cpp +++ b/esphome/components/mqtt/mqtt_component.cpp @@ -13,6 +13,34 @@ namespace esphome::mqtt { static const char *const TAG = "mqtt.component"; +// Helper functions for building topic strings on stack +inline char *append_str(char *p, const char *s, size_t len) { + memcpy(p, s, len); + return p + len; +} + +inline char *append_char(char *p, char c) { + *p = c; + return p + 1; +} + +// Max lengths for stack-based topic building. +// These limits are enforced at Python config validation time in mqtt/__init__.py +// using cv.Length() validators for topic_prefix and discovery_prefix. +// MQTT_COMPONENT_TYPE_MAX_LEN and MQTT_SUFFIX_MAX_LEN are defined in mqtt_component.h. +// ESPHOME_DEVICE_NAME_MAX_LEN and OBJECT_ID_MAX_LEN are defined in entity_base.h. +// This ensures the stack buffers below are always large enough. +static constexpr size_t TOPIC_PREFIX_MAX_LEN = 64; // Validated in Python: cv.Length(max=64) +static constexpr size_t DISCOVERY_PREFIX_MAX_LEN = 64; // Validated in Python: cv.Length(max=64) + +// Stack buffer sizes - safe because all inputs are length-validated at config time +// Format: prefix + "/" + type + "/" + object_id + "/" + suffix + null +static constexpr size_t DEFAULT_TOPIC_MAX_LEN = + TOPIC_PREFIX_MAX_LEN + 1 + MQTT_COMPONENT_TYPE_MAX_LEN + 1 + OBJECT_ID_MAX_LEN + 1 + MQTT_SUFFIX_MAX_LEN + 1; +// Format: prefix + "/" + type + "/" + name + "/" + object_id + "/config" + null +static constexpr size_t DISCOVERY_TOPIC_MAX_LEN = DISCOVERY_PREFIX_MAX_LEN + 1 + MQTT_COMPONENT_TYPE_MAX_LEN + 1 + + ESPHOME_DEVICE_NAME_MAX_LEN + 1 + OBJECT_ID_MAX_LEN + 7 + 1; + void MQTTComponent::set_qos(uint8_t qos) { this->qos_ = qos; } void MQTTComponent::set_subscribe_qos(uint8_t qos) { this->subscribe_qos_ = qos; } @@ -21,8 +49,23 @@ void MQTTComponent::set_retain(bool retain) { this->retain_ = retain; } std::string MQTTComponent::get_discovery_topic_(const MQTTDiscoveryInfo &discovery_info) const { std::string sanitized_name = str_sanitize(App.get_name()); - return discovery_info.prefix + "/" + this->component_type() + "/" + sanitized_name + "/" + - this->get_default_object_id_() + "/config"; + const char *comp_type = this->component_type(); + char object_id_buf[OBJECT_ID_MAX_LEN]; + StringRef object_id = this->get_default_object_id_to_(object_id_buf); + + char buf[DISCOVERY_TOPIC_MAX_LEN]; + char *p = buf; + + p = append_str(p, discovery_info.prefix.data(), discovery_info.prefix.size()); + p = append_char(p, '/'); + p = append_str(p, comp_type, strlen(comp_type)); + p = append_char(p, '/'); + p = append_str(p, sanitized_name.data(), sanitized_name.size()); + p = append_char(p, '/'); + p = append_str(p, object_id.c_str(), object_id.size()); + p = append_str(p, "/config", 7); + + return std::string(buf, p - buf); } std::string MQTTComponent::get_default_topic_for_(const std::string &suffix) const { @@ -32,7 +75,22 @@ std::string MQTTComponent::get_default_topic_for_(const std::string &suffix) con return ""; } - return topic_prefix + "/" + this->component_type() + "/" + this->get_default_object_id_() + "/" + suffix; + const char *comp_type = this->component_type(); + char object_id_buf[OBJECT_ID_MAX_LEN]; + StringRef object_id = this->get_default_object_id_to_(object_id_buf); + + char buf[DEFAULT_TOPIC_MAX_LEN]; + char *p = buf; + + p = append_str(p, topic_prefix.data(), topic_prefix.size()); + p = append_char(p, '/'); + p = append_str(p, comp_type, strlen(comp_type)); + p = append_char(p, '/'); + p = append_str(p, object_id.c_str(), object_id.size()); + p = append_char(p, '/'); + p = append_str(p, suffix.data(), suffix.size()); + + return std::string(buf, p - buf); } std::string MQTTComponent::get_state_topic_() const { @@ -123,6 +181,8 @@ bool MQTTComponent::send_discovery_() { } const MQTTDiscoveryInfo &discovery_info = global_mqtt_client->get_discovery_info(); + char object_id_buf[OBJECT_ID_MAX_LEN]; + StringRef object_id = this->get_default_object_id_to_(object_id_buf); if (discovery_info.unique_id_generator == MQTT_MAC_ADDRESS_UNIQUE_ID_GENERATOR) { char friendly_name_hash[9]; sprintf(friendly_name_hash, "%08" PRIx32, fnv1_hash(this->friendly_name_())); @@ -131,12 +191,12 @@ bool MQTTComponent::send_discovery_() { } else { // default to almost-unique ID. It's a hack but the only way to get that // gorgeous device registry view. - root[MQTT_UNIQUE_ID] = "ESP" + this->component_type() + this->get_default_object_id_(); + root[MQTT_UNIQUE_ID] = "ESP" + std::string(this->component_type()) + object_id.c_str(); } const std::string &node_name = App.get_name(); if (discovery_info.object_id_generator == MQTT_DEVICE_NAME_OBJECT_ID_GENERATOR) - root[MQTT_OBJECT_ID] = node_name + "_" + this->get_default_object_id_(); + root[MQTT_OBJECT_ID] = node_name + "_" + object_id.c_str(); const std::string &friendly_name_ref = App.get_friendly_name(); const std::string &node_friendly_name = friendly_name_ref.empty() ? node_name : friendly_name_ref; @@ -194,10 +254,6 @@ bool MQTTComponent::is_discovery_enabled() const { return this->discovery_enabled_ && global_mqtt_client->is_discovery_enabled(); } -std::string MQTTComponent::get_default_object_id_() const { - return str_sanitize(str_snake_case(this->friendly_name_())); -} - void MQTTComponent::subscribe(const std::string &topic, mqtt_callback_t callback, uint8_t qos) { global_mqtt_client->subscribe(topic, std::move(callback), qos); } @@ -280,6 +336,9 @@ bool MQTTComponent::is_connected_() const { return global_mqtt_client->is_connec // Pull these properties from EntityBase if not overridden std::string MQTTComponent::friendly_name_() const { return this->get_entity()->get_name(); } +StringRef MQTTComponent::get_default_object_id_to_(std::span buf) const { + return this->get_entity()->get_object_id_to(buf); +} StringRef MQTTComponent::get_icon_ref_() const { return this->get_entity()->get_icon_ref(); } bool MQTTComponent::is_disabled_by_default_() const { return this->get_entity()->is_disabled_by_default(); } bool MQTTComponent::is_internal() { diff --git a/esphome/components/mqtt/mqtt_component.h b/esphome/components/mqtt/mqtt_component.h index e5f9664f77..e0b751f05f 100644 --- a/esphome/components/mqtt/mqtt_component.h +++ b/esphome/components/mqtt/mqtt_component.h @@ -19,6 +19,10 @@ struct SendDiscoveryConfig { bool command_topic{true}; ///< If the command topic should be included. Default to true. }; +// Max lengths for stack-based topic building (must match mqtt_component.cpp) +static constexpr size_t MQTT_COMPONENT_TYPE_MAX_LEN = 20; +static constexpr size_t MQTT_SUFFIX_MAX_LEN = 32; + #define LOG_MQTT_COMPONENT(state_topic, command_topic) \ if (state_topic) { \ ESP_LOGCONFIG(TAG, " State Topic: '%s'", this->get_state_topic_().c_str()); \ @@ -27,7 +31,18 @@ struct SendDiscoveryConfig { ESP_LOGCONFIG(TAG, " Command Topic: '%s'", this->get_command_topic_().c_str()); \ } +// Macro to define component_type() with compile-time length verification +// Usage: MQTT_COMPONENT_TYPE(MQTTSensorComponent, "sensor") +#define MQTT_COMPONENT_TYPE(class_name, type_str) \ + const char *class_name::component_type() const { return type_str; } \ + static_assert(sizeof(type_str) - 1 <= MQTT_COMPONENT_TYPE_MAX_LEN, \ + #class_name "::component_type() exceeds MQTT_COMPONENT_TYPE_MAX_LEN"); + +// Macro to define custom topic getter/setter with compile-time suffix length verification #define MQTT_COMPONENT_CUSTOM_TOPIC_(name, type) \ + static_assert(sizeof(#name "/" #type) - 1 <= MQTT_SUFFIX_MAX_LEN, \ + "topic suffix " #name "/" #type " exceeds MQTT_SUFFIX_MAX_LEN"); \ +\ protected: \ std::string custom_##name##_##type##_topic_{}; \ \ @@ -92,7 +107,7 @@ class MQTTComponent : public Component { void set_subscribe_qos(uint8_t qos); /// Override this method to return the component type (e.g. "light", "sensor", ...) - virtual std::string component_type() const = 0; + virtual const char *component_type() const = 0; /// Set a custom state topic. Set to "" for default behavior. void set_custom_state_topic(const char *custom_state_topic); @@ -185,8 +200,8 @@ class MQTTComponent : public Component { // ========== INTERNAL METHODS ========== // (In most use cases you won't need these) - /// Generate the Home Assistant MQTT discovery object id by automatically transforming the friendly name. - std::string get_default_object_id_() const; + /// Get the object ID for this MQTT component, writing to the provided buffer. + StringRef get_default_object_id_to_(std::span buf) const; StringRef custom_state_topic_{}; StringRef custom_command_topic_{}; diff --git a/esphome/components/mqtt/mqtt_cover.cpp b/esphome/components/mqtt/mqtt_cover.cpp index e628ac37a9..4505027485 100644 --- a/esphome/components/mqtt/mqtt_cover.cpp +++ b/esphome/components/mqtt/mqtt_cover.cpp @@ -90,7 +90,7 @@ void MQTTCoverComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConf } } -std::string MQTTCoverComponent::component_type() const { return "cover"; } +MQTT_COMPONENT_TYPE(MQTTCoverComponent, "cover") const EntityBase *MQTTCoverComponent::get_entity() const { return this->cover_; } bool MQTTCoverComponent::send_initial_state() { return this->publish_state(); } diff --git a/esphome/components/mqtt/mqtt_cover.h b/esphome/components/mqtt/mqtt_cover.h index 6b874af16a..13582d14d1 100644 --- a/esphome/components/mqtt/mqtt_cover.h +++ b/esphome/components/mqtt/mqtt_cover.h @@ -29,7 +29,7 @@ class MQTTCoverComponent : public mqtt::MQTTComponent { void dump_config() override; protected: - std::string component_type() const override; + const char *component_type() const override; const EntityBase *get_entity() const override; cover::Cover *cover_; diff --git a/esphome/components/mqtt/mqtt_date.cpp b/esphome/components/mqtt/mqtt_date.cpp index 1715384c5f..dba7c1a671 100644 --- a/esphome/components/mqtt/mqtt_date.cpp +++ b/esphome/components/mqtt/mqtt_date.cpp @@ -39,7 +39,7 @@ void MQTTDateComponent::dump_config() { LOG_MQTT_COMPONENT(true, true) } -std::string MQTTDateComponent::component_type() const { return "date"; } +MQTT_COMPONENT_TYPE(MQTTDateComponent, "date") const EntityBase *MQTTDateComponent::get_entity() const { return this->date_; } void MQTTDateComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { diff --git a/esphome/components/mqtt/mqtt_date.h b/esphome/components/mqtt/mqtt_date.h index 380bb69e0e..4a626becb2 100644 --- a/esphome/components/mqtt/mqtt_date.h +++ b/esphome/components/mqtt/mqtt_date.h @@ -31,7 +31,7 @@ class MQTTDateComponent : public mqtt::MQTTComponent { bool publish_state(uint16_t year, uint8_t month, uint8_t day); protected: - std::string component_type() const override; + const char *component_type() const override; const EntityBase *get_entity() const override; datetime::DateEntity *date_; diff --git a/esphome/components/mqtt/mqtt_datetime.cpp b/esphome/components/mqtt/mqtt_datetime.cpp index 79a2c82180..5f1cf19b97 100644 --- a/esphome/components/mqtt/mqtt_datetime.cpp +++ b/esphome/components/mqtt/mqtt_datetime.cpp @@ -50,7 +50,7 @@ void MQTTDateTimeComponent::dump_config() { LOG_MQTT_COMPONENT(true, true) } -std::string MQTTDateTimeComponent::component_type() const { return "datetime"; } +MQTT_COMPONENT_TYPE(MQTTDateTimeComponent, "datetime") const EntityBase *MQTTDateTimeComponent::get_entity() const { return this->datetime_; } void MQTTDateTimeComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { diff --git a/esphome/components/mqtt/mqtt_datetime.h b/esphome/components/mqtt/mqtt_datetime.h index 8706bfcf75..d02d6f579c 100644 --- a/esphome/components/mqtt/mqtt_datetime.h +++ b/esphome/components/mqtt/mqtt_datetime.h @@ -31,7 +31,7 @@ class MQTTDateTimeComponent : public mqtt::MQTTComponent { bool publish_state(uint16_t year, uint8_t month, uint8_t day, uint8_t hour, uint8_t minute, uint8_t second); protected: - std::string component_type() const override; + const char *component_type() const override; const EntityBase *get_entity() const override; datetime::DateTimeEntity *datetime_; diff --git a/esphome/components/mqtt/mqtt_event.cpp b/esphome/components/mqtt/mqtt_event.cpp index 67a7aab5bd..42fbc1eabd 100644 --- a/esphome/components/mqtt/mqtt_event.cpp +++ b/esphome/components/mqtt/mqtt_event.cpp @@ -50,7 +50,7 @@ bool MQTTEventComponent::publish_event_(const std::string &event_type) { }); } -std::string MQTTEventComponent::component_type() const { return "event"; } +MQTT_COMPONENT_TYPE(MQTTEventComponent, "event") const EntityBase *MQTTEventComponent::get_entity() const { return this->event_; } } // namespace esphome::mqtt diff --git a/esphome/components/mqtt/mqtt_event.h b/esphome/components/mqtt/mqtt_event.h index fc6e778d44..e6d5b6f278 100644 --- a/esphome/components/mqtt/mqtt_event.h +++ b/esphome/components/mqtt/mqtt_event.h @@ -25,7 +25,7 @@ class MQTTEventComponent : public mqtt::MQTTComponent { protected: bool publish_event_(const std::string &event_type); - std::string component_type() const override; + const char *component_type() const override; const EntityBase *get_entity() const override; event::Event *event_; diff --git a/esphome/components/mqtt/mqtt_fan.cpp b/esphome/components/mqtt/mqtt_fan.cpp index ffecd9c663..bd6c98b679 100644 --- a/esphome/components/mqtt/mqtt_fan.cpp +++ b/esphome/components/mqtt/mqtt_fan.cpp @@ -15,7 +15,7 @@ using namespace esphome::fan; MQTTFanComponent::MQTTFanComponent(Fan *state) : state_(state) {} Fan *MQTTFanComponent::get_state() const { return this->state_; } -std::string MQTTFanComponent::component_type() const { return "fan"; } +MQTT_COMPONENT_TYPE(MQTTFanComponent, "fan") const EntityBase *MQTTFanComponent::get_entity() const { return this->state_; } void MQTTFanComponent::setup() { diff --git a/esphome/components/mqtt/mqtt_fan.h b/esphome/components/mqtt/mqtt_fan.h index 16ce246853..43ef67e733 100644 --- a/esphome/components/mqtt/mqtt_fan.h +++ b/esphome/components/mqtt/mqtt_fan.h @@ -36,7 +36,7 @@ class MQTTFanComponent : public mqtt::MQTTComponent { bool send_initial_state() override; bool publish_state(); /// 'fan' component type for discovery. - std::string component_type() const override; + const char *component_type() const override; fan::Fan *get_state() const; diff --git a/esphome/components/mqtt/mqtt_light.cpp b/esphome/components/mqtt/mqtt_light.cpp index 0dafe487ff..2d588ed10b 100644 --- a/esphome/components/mqtt/mqtt_light.cpp +++ b/esphome/components/mqtt/mqtt_light.cpp @@ -14,7 +14,7 @@ static const char *const TAG = "mqtt.light"; using namespace esphome::light; -std::string MQTTJSONLightComponent::component_type() const { return "light"; } +MQTT_COMPONENT_TYPE(MQTTJSONLightComponent, "light") const EntityBase *MQTTJSONLightComponent::get_entity() const { return this->state_; } void MQTTJSONLightComponent::setup() { diff --git a/esphome/components/mqtt/mqtt_light.h b/esphome/components/mqtt/mqtt_light.h index 2cc631c901..41981655ef 100644 --- a/esphome/components/mqtt/mqtt_light.h +++ b/esphome/components/mqtt/mqtt_light.h @@ -28,7 +28,7 @@ class MQTTJSONLightComponent : public mqtt::MQTTComponent, public light::LightRe void on_light_remote_values_update() override; protected: - std::string component_type() const override; + const char *component_type() const override; const EntityBase *get_entity() const override; bool publish_state_(); diff --git a/esphome/components/mqtt/mqtt_lock.cpp b/esphome/components/mqtt/mqtt_lock.cpp index 58fa675eb7..43ef60bdf4 100644 --- a/esphome/components/mqtt/mqtt_lock.cpp +++ b/esphome/components/mqtt/mqtt_lock.cpp @@ -34,7 +34,7 @@ void MQTTLockComponent::dump_config() { LOG_MQTT_COMPONENT(true, true); } -std::string MQTTLockComponent::component_type() const { return "lock"; } +MQTT_COMPONENT_TYPE(MQTTLockComponent, "lock") const EntityBase *MQTTLockComponent::get_entity() const { return this->lock_; } void MQTTLockComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson diff --git a/esphome/components/mqtt/mqtt_lock.h b/esphome/components/mqtt/mqtt_lock.h index 6fb4998b25..666882c73d 100644 --- a/esphome/components/mqtt/mqtt_lock.h +++ b/esphome/components/mqtt/mqtt_lock.h @@ -27,7 +27,7 @@ class MQTTLockComponent : public mqtt::MQTTComponent { protected: /// "lock" component type. - std::string component_type() const override; + const char *component_type() const override; const EntityBase *get_entity() const override; lock::Lock *lock_; diff --git a/esphome/components/mqtt/mqtt_number.cpp b/esphome/components/mqtt/mqtt_number.cpp index 381574ae56..8342210ee4 100644 --- a/esphome/components/mqtt/mqtt_number.cpp +++ b/esphome/components/mqtt/mqtt_number.cpp @@ -33,7 +33,7 @@ void MQTTNumberComponent::dump_config() { LOG_MQTT_COMPONENT(true, false) } -std::string MQTTNumberComponent::component_type() const { return "number"; } +MQTT_COMPONENT_TYPE(MQTTNumberComponent, "number") const EntityBase *MQTTNumberComponent::get_entity() const { return this->number_; } void MQTTNumberComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { diff --git a/esphome/components/mqtt/mqtt_number.h b/esphome/components/mqtt/mqtt_number.h index b89e78a454..021a539988 100644 --- a/esphome/components/mqtt/mqtt_number.h +++ b/esphome/components/mqtt/mqtt_number.h @@ -32,7 +32,7 @@ class MQTTNumberComponent : public mqtt::MQTTComponent { protected: /// Override for MQTTComponent, returns "number". - std::string component_type() const override; + const char *component_type() const override; const EntityBase *get_entity() const override; number::Number *number_; diff --git a/esphome/components/mqtt/mqtt_select.cpp b/esphome/components/mqtt/mqtt_select.cpp index 5edc5c50dc..09d90ed46e 100644 --- a/esphome/components/mqtt/mqtt_select.cpp +++ b/esphome/components/mqtt/mqtt_select.cpp @@ -28,7 +28,7 @@ void MQTTSelectComponent::dump_config() { LOG_MQTT_COMPONENT(true, false) } -std::string MQTTSelectComponent::component_type() const { return "select"; } +MQTT_COMPONENT_TYPE(MQTTSelectComponent, "select") const EntityBase *MQTTSelectComponent::get_entity() const { return this->select_; } void MQTTSelectComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { diff --git a/esphome/components/mqtt/mqtt_select.h b/esphome/components/mqtt/mqtt_select.h index 19aad662e5..aaf174ff72 100644 --- a/esphome/components/mqtt/mqtt_select.h +++ b/esphome/components/mqtt/mqtt_select.h @@ -32,7 +32,7 @@ class MQTTSelectComponent : public mqtt::MQTTComponent { protected: /// Override for MQTTComponent, returns "select". - std::string component_type() const override; + const char *component_type() const override; const EntityBase *get_entity() const override; select::Select *select_; diff --git a/esphome/components/mqtt/mqtt_sensor.cpp b/esphome/components/mqtt/mqtt_sensor.cpp index bd79ae40fe..14eb160e72 100644 --- a/esphome/components/mqtt/mqtt_sensor.cpp +++ b/esphome/components/mqtt/mqtt_sensor.cpp @@ -31,7 +31,7 @@ void MQTTSensorComponent::dump_config() { LOG_MQTT_COMPONENT(true, false) } -std::string MQTTSensorComponent::component_type() const { return "sensor"; } +MQTT_COMPONENT_TYPE(MQTTSensorComponent, "sensor") const EntityBase *MQTTSensorComponent::get_entity() const { return this->sensor_; } uint32_t MQTTSensorComponent::get_expire_after() const { diff --git a/esphome/components/mqtt/mqtt_sensor.h b/esphome/components/mqtt/mqtt_sensor.h index 8c60199e1b..e8202aa8e2 100644 --- a/esphome/components/mqtt/mqtt_sensor.h +++ b/esphome/components/mqtt/mqtt_sensor.h @@ -43,7 +43,7 @@ class MQTTSensorComponent : public mqtt::MQTTComponent { protected: /// Override for MQTTComponent, returns "sensor". - std::string component_type() const override; + const char *component_type() const override; const EntityBase *get_entity() const override; sensor::Sensor *sensor_; diff --git a/esphome/components/mqtt/mqtt_switch.cpp b/esphome/components/mqtt/mqtt_switch.cpp index a35ae8f9b6..a985ec66be 100644 --- a/esphome/components/mqtt/mqtt_switch.cpp +++ b/esphome/components/mqtt/mqtt_switch.cpp @@ -41,7 +41,7 @@ void MQTTSwitchComponent::dump_config() { LOG_MQTT_COMPONENT(true, true); } -std::string MQTTSwitchComponent::component_type() const { return "switch"; } +MQTT_COMPONENT_TYPE(MQTTSwitchComponent, "switch") const EntityBase *MQTTSwitchComponent::get_entity() const { return this->switch_; } void MQTTSwitchComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson diff --git a/esphome/components/mqtt/mqtt_switch.h b/esphome/components/mqtt/mqtt_switch.h index fb6a13f172..5f6cb841fd 100644 --- a/esphome/components/mqtt/mqtt_switch.h +++ b/esphome/components/mqtt/mqtt_switch.h @@ -27,7 +27,7 @@ class MQTTSwitchComponent : public mqtt::MQTTComponent { protected: /// "switch" component type. - std::string component_type() const override; + const char *component_type() const override; const EntityBase *get_entity() const override; switch_::Switch *switch_; diff --git a/esphome/components/mqtt/mqtt_text.cpp b/esphome/components/mqtt/mqtt_text.cpp index 3cb851fd38..cee94965c6 100644 --- a/esphome/components/mqtt/mqtt_text.cpp +++ b/esphome/components/mqtt/mqtt_text.cpp @@ -29,7 +29,7 @@ void MQTTTextComponent::dump_config() { LOG_MQTT_COMPONENT(true, true) } -std::string MQTTTextComponent::component_type() const { return "text"; } +MQTT_COMPONENT_TYPE(MQTTTextComponent, "text") const EntityBase *MQTTTextComponent::get_entity() const { return this->text_; } void MQTTTextComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { diff --git a/esphome/components/mqtt/mqtt_text.h b/esphome/components/mqtt/mqtt_text.h index 0480b89395..8ae0b9e29a 100644 --- a/esphome/components/mqtt/mqtt_text.h +++ b/esphome/components/mqtt/mqtt_text.h @@ -32,7 +32,7 @@ class MQTTTextComponent : public mqtt::MQTTComponent { protected: /// Override for MQTTComponent, returns "text". - std::string component_type() const override; + const char *component_type() const override; const EntityBase *get_entity() const override; text::Text *text_; diff --git a/esphome/components/mqtt/mqtt_text_sensor.cpp b/esphome/components/mqtt/mqtt_text_sensor.cpp index c87f22fb8e..5346923b41 100644 --- a/esphome/components/mqtt/mqtt_text_sensor.cpp +++ b/esphome/components/mqtt/mqtt_text_sensor.cpp @@ -39,7 +39,7 @@ bool MQTTTextSensor::send_initial_state() { return true; } } -std::string MQTTTextSensor::component_type() const { return "sensor"; } +MQTT_COMPONENT_TYPE(MQTTTextSensor, "sensor") const EntityBase *MQTTTextSensor::get_entity() const { return this->sensor_; } } // namespace esphome::mqtt diff --git a/esphome/components/mqtt/mqtt_text_sensor.h b/esphome/components/mqtt/mqtt_text_sensor.h index d4d38d7eb2..d8f9315c1e 100644 --- a/esphome/components/mqtt/mqtt_text_sensor.h +++ b/esphome/components/mqtt/mqtt_text_sensor.h @@ -25,7 +25,7 @@ class MQTTTextSensor : public mqtt::MQTTComponent { bool send_initial_state() override; protected: - std::string component_type() const override; + const char *component_type() const override; const EntityBase *get_entity() const override; text_sensor::TextSensor *sensor_; diff --git a/esphome/components/mqtt/mqtt_time.cpp b/esphome/components/mqtt/mqtt_time.cpp index 01b8dd3483..b75325022a 100644 --- a/esphome/components/mqtt/mqtt_time.cpp +++ b/esphome/components/mqtt/mqtt_time.cpp @@ -39,7 +39,7 @@ void MQTTTimeComponent::dump_config() { LOG_MQTT_COMPONENT(true, true) } -std::string MQTTTimeComponent::component_type() const { return "time"; } +MQTT_COMPONENT_TYPE(MQTTTimeComponent, "time") const EntityBase *MQTTTimeComponent::get_entity() const { return this->time_; } void MQTTTimeComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { diff --git a/esphome/components/mqtt/mqtt_time.h b/esphome/components/mqtt/mqtt_time.h index 60345c37ae..cf5780da2d 100644 --- a/esphome/components/mqtt/mqtt_time.h +++ b/esphome/components/mqtt/mqtt_time.h @@ -31,7 +31,7 @@ class MQTTTimeComponent : public mqtt::MQTTComponent { bool publish_state(uint8_t hour, uint8_t minute, uint8_t second); protected: - std::string component_type() const override; + const char *component_type() const override; const EntityBase *get_entity() const override; datetime::TimeEntity *time_; diff --git a/esphome/components/mqtt/mqtt_update.cpp b/esphome/components/mqtt/mqtt_update.cpp index aedf2414c1..99e0c85509 100644 --- a/esphome/components/mqtt/mqtt_update.cpp +++ b/esphome/components/mqtt/mqtt_update.cpp @@ -52,7 +52,7 @@ void MQTTUpdateComponent::dump_config() { LOG_MQTT_COMPONENT(true, true); } -std::string MQTTUpdateComponent::component_type() const { return "update"; } +MQTT_COMPONENT_TYPE(MQTTUpdateComponent, "update") const EntityBase *MQTTUpdateComponent::get_entity() const { return this->update_; } } // namespace esphome::mqtt diff --git a/esphome/components/mqtt/mqtt_update.h b/esphome/components/mqtt/mqtt_update.h index d04d22d25f..ec1adb1fcd 100644 --- a/esphome/components/mqtt/mqtt_update.h +++ b/esphome/components/mqtt/mqtt_update.h @@ -27,7 +27,7 @@ class MQTTUpdateComponent : public mqtt::MQTTComponent { protected: /// "update" component type. - std::string component_type() const override; + const char *component_type() const override; const EntityBase *get_entity() const override; update::UpdateEntity *update_; diff --git a/esphome/components/mqtt/mqtt_valve.cpp b/esphome/components/mqtt/mqtt_valve.cpp index 8ee693121b..a4c893f84b 100644 --- a/esphome/components/mqtt/mqtt_valve.cpp +++ b/esphome/components/mqtt/mqtt_valve.cpp @@ -65,7 +65,7 @@ void MQTTValveComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConf } } -std::string MQTTValveComponent::component_type() const { return "valve"; } +MQTT_COMPONENT_TYPE(MQTTValveComponent, "valve") const EntityBase *MQTTValveComponent::get_entity() const { return this->valve_; } bool MQTTValveComponent::send_initial_state() { return this->publish_state(); } diff --git a/esphome/components/mqtt/mqtt_valve.h b/esphome/components/mqtt/mqtt_valve.h index 9e5221e495..d3b724a8ba 100644 --- a/esphome/components/mqtt/mqtt_valve.h +++ b/esphome/components/mqtt/mqtt_valve.h @@ -27,7 +27,7 @@ class MQTTValveComponent : public mqtt::MQTTComponent { void dump_config() override; protected: - std::string component_type() const override; + const char *component_type() const override; const EntityBase *get_entity() const override; valve::Valve *valve_; diff --git a/esphome/core/config.py b/esphome/core/config.py index f9c3011507..b7e6ab9bee 100644 --- a/esphome/core/config.py +++ b/esphome/core/config.py @@ -76,6 +76,7 @@ VALID_INCLUDE_EXTS = {".h", ".hpp", ".tcc", ".ino", ".cpp", ".c"} def validate_hostname(config): + # Keep in sync with ESPHOME_DEVICE_NAME_MAX_LEN in esphome/core/entity_base.h max_length = 31 if config[CONF_NAME_ADD_MAC_SUFFIX]: max_length -= 7 # "-AABBCC" is appended when add mac suffix option is used @@ -207,6 +208,7 @@ CONFIG_SCHEMA = cv.All( cv.Schema( { cv.Required(CONF_NAME): cv.valid_name, + # Keep max=120 in sync with OBJECT_ID_MAX_LEN in esphome/core/entity_base.h cv.Optional(CONF_FRIENDLY_NAME, ""): cv.All( cv.string_no_slash, cv.Length(max=120) ), diff --git a/esphome/core/entity_base.h b/esphome/core/entity_base.h index a45c7795bf..1649077dd0 100644 --- a/esphome/core/entity_base.h +++ b/esphome/core/entity_base.h @@ -13,7 +13,10 @@ namespace esphome { -// Maximum size for object_id buffer (friendly_name max ~120 + margin) +// Maximum device name length - keep in sync with validate_hostname() in esphome/core/config.py +static constexpr size_t ESPHOME_DEVICE_NAME_MAX_LEN = 31; + +// Maximum size for object_id buffer - keep in sync with friendly_name cv.Length(max=120) in esphome/core/config.py static constexpr size_t OBJECT_ID_MAX_LEN = 128; enum EntityCategory : uint8_t { From d4969f581aaafc27200da114078918886e3a1f88 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 8 Jan 2026 11:42:30 -1000 Subject: [PATCH 8/9] [wifi] Limit ignored disconnect events on LibreTiny to speed up AP failover (#13070) --- .../wifi/wifi_component_libretiny.cpp | 29 ++++++++++++++++--- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/esphome/components/wifi/wifi_component_libretiny.cpp b/esphome/components/wifi/wifi_component_libretiny.cpp index 68fcc3577d..c5b6a8ad96 100644 --- a/esphome/components/wifi/wifi_component_libretiny.cpp +++ b/esphome/components/wifi/wifi_component_libretiny.cpp @@ -86,6 +86,14 @@ enum class LTWiFiSTAState : uint8_t { static LTWiFiSTAState s_sta_state = LTWiFiSTAState::IDLE; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) +// Count of ignored disconnect events during connection - too many indicates real failure +static uint8_t s_ignored_disconnect_count = 0; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) +// Threshold for ignored disconnect events before treating as connection failure +// LibreTiny sends spurious "Association Leave" events, but more than this many +// indicates the connection is failing repeatedly. Value of 3 balances fast failure +// detection with tolerance for occasional spurious events on successful connections. +static constexpr uint8_t IGNORED_DISCONNECT_THRESHOLD = 3; + bool WiFiComponent::wifi_mode_(optional sta, optional ap) { uint8_t current_mode = WiFi.getMode(); bool current_sta = current_mode & 0b01; @@ -201,8 +209,9 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) { this->wifi_apply_hostname_(); - // Reset state machine before connecting + // Reset state machine and disconnect counter before connecting s_sta_state = LTWiFiSTAState::CONNECTING; + s_ignored_disconnect_count = 0; WiFiStatus status = WiFi.begin(ap.get_ssid().c_str(), ap.get_password().empty() ? NULL : ap.get_password().c_str(), ap.get_channel(), // 0 = auto @@ -474,10 +483,22 @@ void WiFiComponent::wifi_process_event_(LTWiFiEvent *event) { // causing wifi_sta_connect_status_() to return an error. The main loop would then // call retry_connect(), aborting a connection that may succeed moments later. // Only ignore benign reasons - real failures like NO_AP_FOUND should still be processed. + // However, if we get too many of these events (IGNORED_DISCONNECT_THRESHOLD), treat it + // as a real connection failure to avoid waiting the full timeout for a failing connection. if (it.ssid_len == 0 && s_sta_state == LTWiFiSTAState::CONNECTING && it.reason != WIFI_REASON_NO_AP_FOUND) { - ESP_LOGV(TAG, "Ignoring disconnect event with empty ssid while connecting (reason=%s)", - get_disconnect_reason_str(it.reason)); - break; + s_ignored_disconnect_count++; + if (s_ignored_disconnect_count >= IGNORED_DISCONNECT_THRESHOLD) { + ESP_LOGW(TAG, "Too many disconnect events (%u) while connecting, treating as failure (reason=%s)", + s_ignored_disconnect_count, get_disconnect_reason_str(it.reason)); + s_sta_state = LTWiFiSTAState::ERROR_FAILED; + WiFi.disconnect(); + this->error_from_callback_ = true; + // Don't break - fall through to notify listeners + } else { + ESP_LOGV(TAG, "Ignoring disconnect event with empty ssid while connecting (reason=%s, count=%u)", + get_disconnect_reason_str(it.reason), s_ignored_disconnect_count); + break; + } } if (it.reason == WIFI_REASON_NO_AP_FOUND) { From 012a1e2afd2c1c532f6d1c0cdd3a247fdc845179 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rodrigo=20Mart=C3=ADn?= Date: Thu, 8 Jan 2026 23:05:53 +0100 Subject: [PATCH 9/9] [mqtt] Include session_present and reason parameters in connection callbacks (#12413) 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/mqtt/__init__.py | 15 +++++++++++---- esphome/components/mqtt/mqtt_client.h | 8 ++++---- tests/components/mqtt/common.yaml | 4 ++++ 3 files changed, 19 insertions(+), 8 deletions(-) diff --git a/esphome/components/mqtt/__init__.py b/esphome/components/mqtt/__init__.py index f01c928b30..d57cedd144 100644 --- a/esphome/components/mqtt/__init__.py +++ b/esphome/components/mqtt/__init__.py @@ -113,6 +113,7 @@ MQTT_MESSAGE_SCHEMA = cv.Any( mqtt_ns = cg.esphome_ns.namespace("mqtt") MQTTMessage = mqtt_ns.struct("MQTTMessage") +MQTTClientDisconnectReason = mqtt_ns.enum("MQTTClientDisconnectReason") MQTTClientComponent = mqtt_ns.class_("MQTTClientComponent", cg.Component) MQTTPublishAction = mqtt_ns.class_("MQTTPublishAction", automation.Action) MQTTPublishJsonAction = mqtt_ns.class_("MQTTPublishJsonAction", automation.Action) @@ -124,9 +125,11 @@ MQTTMessageTrigger = mqtt_ns.class_( MQTTJsonMessageTrigger = mqtt_ns.class_( "MQTTJsonMessageTrigger", automation.Trigger.template(cg.JsonObjectConst) ) -MQTTConnectTrigger = mqtt_ns.class_("MQTTConnectTrigger", automation.Trigger.template()) +MQTTConnectTrigger = mqtt_ns.class_( + "MQTTConnectTrigger", automation.Trigger.template(cg.bool_) +) MQTTDisconnectTrigger = mqtt_ns.class_( - "MQTTDisconnectTrigger", automation.Trigger.template() + "MQTTDisconnectTrigger", automation.Trigger.template(MQTTClientDisconnectReason) ) MQTTComponent = mqtt_ns.class_("MQTTComponent", cg.Component) MQTTConnectedCondition = mqtt_ns.class_("MQTTConnectedCondition", Condition) @@ -475,11 +478,15 @@ async def to_code(config): for conf in config.get(CONF_ON_CONNECT, []): trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [], conf) + await automation.build_automation( + trigger, [(cg.bool_, "session_present")], conf + ) for conf in config.get(CONF_ON_DISCONNECT, []): trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [], conf) + await automation.build_automation( + trigger, [(MQTTClientDisconnectReason, "reason")], conf + ) cg.add(var.set_publish_nan_as_none(config[CONF_PUBLISH_NAN_AS_NONE])) diff --git a/esphome/components/mqtt/mqtt_client.h b/esphome/components/mqtt/mqtt_client.h index 4189e7ae77..9e9db03b19 100644 --- a/esphome/components/mqtt/mqtt_client.h +++ b/esphome/components/mqtt/mqtt_client.h @@ -378,17 +378,17 @@ class MQTTJsonMessageTrigger : public Trigger { } }; -class MQTTConnectTrigger : public Trigger<> { +class MQTTConnectTrigger : public Trigger { public: explicit MQTTConnectTrigger(MQTTClientComponent *&client) { - client->set_on_connect([this](bool session_present) { this->trigger(); }); + client->set_on_connect([this](bool session_present) { this->trigger(session_present); }); } }; -class MQTTDisconnectTrigger : public Trigger<> { +class MQTTDisconnectTrigger : public Trigger { public: explicit MQTTDisconnectTrigger(MQTTClientComponent *&client) { - client->set_on_disconnect([this](MQTTClientDisconnectReason reason) { this->trigger(); }); + client->set_on_disconnect([this](MQTTClientDisconnectReason reason) { this->trigger(reason); }); } }; diff --git a/tests/components/mqtt/common.yaml b/tests/components/mqtt/common.yaml index 284ac30337..33988cebb4 100644 --- a/tests/components/mqtt/common.yaml +++ b/tests/components/mqtt/common.yaml @@ -57,10 +57,14 @@ mqtt: - mqtt.publish: topic: some/topic payload: Hello + - lambda: |- + ESP_LOGD("MQTT", "Session present %d", session_present); on_disconnect: - mqtt.publish: topic: some/topic payload: Good-bye + - lambda: |- + ESP_LOGD("MQTT", "Disconnect reason %d", reason); publish_nan_as_none: false binary_sensor: