From 7c27835c03749370fd46f9e38fef2334b8237532 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 31 Jan 2026 02:28:07 -0600 Subject: [PATCH 01/11] [wifi] Defer ESP8266 listener callbacks to main loop to fix stack overflow --- esphome/components/wifi/wifi_component.cpp | 8 +- esphome/components/wifi/wifi_component.h | 27 ++++- .../wifi/wifi_component_esp8266.cpp | 103 +++++++++++------- .../wifi/wifi_component_esp_idf.cpp | 2 +- .../wifi/wifi_component_libretiny.cpp | 6 +- 5 files changed, 98 insertions(+), 48 deletions(-) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index 65c653a62a..af4142591d 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -643,7 +643,7 @@ void WiFiComponent::restart_adapter() { // through start_connecting() first. Without this clear, stale errors would // trigger spurious "failed (callback)" logs. The canonical clear location // is in start_connecting(); this is the only exception to that pattern. - this->error_from_callback_ = false; + this->error_from_callback_ = 0; } void WiFiComponent::loop() { @@ -1063,7 +1063,7 @@ void WiFiComponent::start_connecting(const WiFiAP &ap) { // This is the canonical location for clearing the flag since all connection // attempts go through start_connecting(). The only other clear is in // restart_adapter() which enters COOLDOWN without calling start_connecting(). - this->error_from_callback_ = false; + this->error_from_callback_ = 0; if (!this->wifi_sta_connect_(ap)) { ESP_LOGE(TAG, "wifi_sta_connect_ failed"); @@ -1468,7 +1468,11 @@ void WiFiComponent::check_connecting_finished(uint32_t now) { } if (this->error_from_callback_) { + // ESP8266: logging done in callback, listeners deferred via pending_.disconnect + // Other platforms: just log generic failure message +#ifndef USE_ESP8266 ESP_LOGW(TAG, "Connecting to network failed (callback)"); +#endif this->retry_connect(); return; } diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index f27c522a1b..843e45cad6 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -58,6 +58,12 @@ static constexpr int8_t WIFI_RSSI_DISCONNECTED = -127; /// Buffer size for SSID (IEEE 802.11 max 32 bytes + null terminator) static constexpr size_t SSID_BUFFER_SIZE = 33; +#ifdef USE_ESP8266 +/// Special disconnect reason for authmode downgrade (CVE-2020-12638 mitigation) +/// Not a real WiFi reason code - used internally for deferred logging +static constexpr uint8_t WIFI_DISCONNECT_REASON_AUTHMODE_DOWNGRADE = 254; +#endif + struct SavedWifiSettings { char ssid[33]; char password[65]; @@ -590,6 +596,9 @@ class WiFiComponent : public Component { void connect_soon_(); void wifi_loop_(); +#ifdef USE_ESP8266 + void process_pending_callbacks_(); +#endif bool wifi_mode_(optional sta, optional ap); bool wifi_sta_pre_setup_(); bool wifi_apply_output_power_(float output_power); @@ -704,10 +713,26 @@ class WiFiComponent : public Component { uint8_t num_ipv6_addresses_{0}; #endif /* USE_NETWORK_IPV6 */ + // 0 = no error, non-zero = disconnect reason code from callback + // This serves as both the error flag and stores the reason for deferred logging + uint8_t error_from_callback_{0}; + +#ifdef USE_ESP8266 + // Pending listener callbacks from system context (ESP8266 only) + // ESP8266 callbacks run in SDK system context with ~2KB stack where + // calling arbitrary listener callbacks is unsafe. These flags defer + // listener notifications to wifi_loop_() which runs with full stack. + struct { + bool connect : 1; // STA connected, notify listeners + bool disconnect : 1; // STA disconnected, notify listeners + bool got_ip : 1; // Got IP, notify listeners + bool scan_complete : 1; // Scan complete, notify listeners + } pending_{}; +#endif + // Group all boolean values together bool has_ap_{false}; bool handled_connected_state_{false}; - bool error_from_callback_{false}; bool scan_done_{false}; bool ap_setup_{false}; bool ap_started_{false}; diff --git a/esphome/components/wifi/wifi_component_esp8266.cpp b/esphome/components/wifi/wifi_component_esp8266.cpp index c714afaad3..09ecdf857a 100644 --- a/esphome/components/wifi/wifi_component_esp8266.cpp +++ b/esphome/components/wifi/wifi_component_esp8266.cpp @@ -511,21 +511,8 @@ void WiFiComponent::wifi_event_callback(System_Event_t *event) { it.channel); #endif s_sta_connected = true; -#ifdef USE_WIFI_CONNECT_STATE_LISTENERS - for (auto *listener : global_wifi_component->connect_state_listeners_) { - listener->on_wifi_connect_state(StringRef(it.ssid, it.ssid_len), it.bssid); - } -#endif - // For static IP configurations, GOT_IP event may not fire, so notify IP listeners here -#if defined(USE_WIFI_IP_STATE_LISTENERS) && defined(USE_WIFI_MANUAL_IP) - if (const WiFiAP *config = global_wifi_component->get_selected_sta_(); - config && config->get_manual_ip().has_value()) { - 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 + // Defer listener callbacks to main loop - system context has limited stack + global_wifi_component->pending_.connect = true; break; } case EVENT_STAMODE_DISCONNECTED: { @@ -543,17 +530,9 @@ void WiFiComponent::wifi_event_callback(System_Event_t *event) { } s_sta_connected = false; s_sta_connecting = false; - // IMPORTANT: Set error flag BEFORE notifying listeners. - // This ensures is_connected() returns false during listener callbacks, - // which is critical for proper reconnection logic (e.g., roaming). - global_wifi_component->error_from_callback_ = true; -#ifdef USE_WIFI_CONNECT_STATE_LISTENERS - // Notify listeners AFTER setting error flag so they see correct state - static constexpr uint8_t EMPTY_BSSID[6] = {}; - for (auto *listener : global_wifi_component->connect_state_listeners_) { - listener->on_wifi_connect_state(StringRef(), EMPTY_BSSID); - } -#endif + // Store reason as error flag; defer listener callbacks to main loop + global_wifi_component->error_from_callback_ = it.reason; + global_wifi_component->pending_.disconnect = true; break; } case EVENT_STAMODE_AUTHMODE_CHANGE: { @@ -564,10 +543,8 @@ void WiFiComponent::wifi_event_callback(System_Event_t *event) { // https://lbsfilm.at/blog/wpa2-authenticationmode-downgrade-in-espressif-microprocessors if (it.old_mode != AUTH_OPEN && it.new_mode == AUTH_OPEN) { ESP_LOGW(TAG, "Potential Authmode downgrade detected, disconnecting"); - // we can't call retry_connect() from this context, so disconnect immediately - // and notify main thread with error_from_callback_ wifi_station_disconnect(); - global_wifi_component->error_from_callback_ = true; + global_wifi_component->error_from_callback_ = WIFI_DISCONNECT_REASON_AUTHMODE_DOWNGRADE; } break; } @@ -578,12 +555,8 @@ void WiFiComponent::wifi_event_callback(System_Event_t *event) { ESP_LOGV(TAG, "static_ip=%s gateway=%s netmask=%s", network::IPAddress(&it.ip).str_to(ip_buf), network::IPAddress(&it.gw).str_to(gw_buf), network::IPAddress(&it.mask).str_to(mask_buf)); s_sta_got_ip = true; -#ifdef USE_WIFI_IP_STATE_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 + // Defer listener callbacks to main loop - system context has limited stack + global_wifi_component->pending_.got_ip = true; break; } case EVENT_STAMODE_DHCP_TIMEOUT: { @@ -793,11 +766,7 @@ void WiFiComponent::wifi_scan_done_callback_(void *arg, STATUS status) { ESP_LOGV(TAG, "Scan complete: %zu found, %zu stored%s", total, this->scan_result_.size(), needs_full ? "" : " (filtered)"); this->scan_done_ = true; -#ifdef USE_WIFI_SCAN_RESULTS_LISTENERS - for (auto *listener : global_wifi_component->scan_results_listeners_) { - listener->on_wifi_scan_results(global_wifi_component->scan_result_); - } -#endif + this->pending_.scan_complete = true; // Defer listener callbacks to main loop } #ifdef USE_WIFI_AP @@ -983,7 +952,59 @@ network::IPAddress WiFiComponent::wifi_gateway_ip_() { return network::IPAddress(&ip.gw); } network::IPAddress WiFiComponent::wifi_dns_ip_(int num) { return network::IPAddress(dns_getserver(num)); } -void WiFiComponent::wifi_loop_() {} +void WiFiComponent::wifi_loop_() { this->process_pending_callbacks_(); } + +void WiFiComponent::process_pending_callbacks_() { + // Notify listeners for connect event (logging already done in callback) + if (this->pending_.connect) { + this->pending_.connect = false; +#ifdef USE_WIFI_CONNECT_STATE_LISTENERS + bssid_t bssid = this->wifi_bssid(); + char ssid_buf[SSID_BUFFER_SIZE]; + for (auto *listener : this->connect_state_listeners_) { + listener->on_wifi_connect_state(StringRef(this->wifi_ssid_to(ssid_buf)), bssid); + } +#endif +#if defined(USE_WIFI_IP_STATE_LISTENERS) && defined(USE_WIFI_MANUAL_IP) + if (const WiFiAP *config = this->get_selected_sta_(); config && config->get_manual_ip().has_value()) { + 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 + } + + // Notify listeners for disconnect event (logging already done in callback) + if (this->pending_.disconnect) { + this->pending_.disconnect = false; +#ifdef USE_WIFI_CONNECT_STATE_LISTENERS + static constexpr uint8_t EMPTY_BSSID[6] = {}; + for (auto *listener : this->connect_state_listeners_) { + listener->on_wifi_connect_state(StringRef(), EMPTY_BSSID); + } +#endif + } + + // Notify listeners for got IP event (logging already done in callback) + if (this->pending_.got_ip) { + this->pending_.got_ip = false; +#ifdef USE_WIFI_IP_STATE_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 + } + + // Notify listeners for scan complete (logging already done in callback) + if (this->pending_.scan_complete) { + this->pending_.scan_complete = false; +#ifdef USE_WIFI_SCAN_RESULTS_LISTENERS + for (auto *listener : this->scan_results_listeners_) { + listener->on_wifi_scan_results(this->scan_result_); + } +#endif + } +} } // namespace esphome::wifi #endif diff --git a/esphome/components/wifi/wifi_component_esp_idf.cpp b/esphome/components/wifi/wifi_component_esp_idf.cpp index a32232a758..1c78d962ef 100644 --- a/esphome/components/wifi/wifi_component_esp_idf.cpp +++ b/esphome/components/wifi/wifi_component_esp_idf.cpp @@ -774,7 +774,7 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) { } s_sta_connected = false; s_sta_connecting = false; - error_from_callback_ = true; + error_from_callback_ = 1; #ifdef USE_WIFI_CONNECT_STATE_LISTENERS static constexpr uint8_t EMPTY_BSSID[6] = {}; for (auto *listener : this->connect_state_listeners_) { diff --git a/esphome/components/wifi/wifi_component_libretiny.cpp b/esphome/components/wifi/wifi_component_libretiny.cpp index af2b82c3c6..9af4dfbadb 100644 --- a/esphome/components/wifi/wifi_component_libretiny.cpp +++ b/esphome/components/wifi/wifi_component_libretiny.cpp @@ -494,7 +494,7 @@ void WiFiComponent::wifi_process_event_(LTWiFiEvent *event) { s_ignored_disconnect_count, get_disconnect_reason_str(it.reason)); s_sta_state = LTWiFiSTAState::ERROR_FAILED; WiFi.disconnect(); - this->error_from_callback_ = true; + this->error_from_callback_ = 1; // Don't break - fall through to notify listeners } else { ESP_LOGV(TAG, "Ignoring disconnect event with empty ssid while connecting (reason=%s, count=%u)", @@ -520,7 +520,7 @@ void WiFiComponent::wifi_process_event_(LTWiFiEvent *event) { reason == WIFI_REASON_NO_AP_FOUND || reason == WIFI_REASON_ASSOC_FAIL || reason == WIFI_REASON_HANDSHAKE_TIMEOUT) { WiFi.disconnect(); - this->error_from_callback_ = true; + this->error_from_callback_ = 1; } #ifdef USE_WIFI_CONNECT_STATE_LISTENERS @@ -539,7 +539,7 @@ void WiFiComponent::wifi_process_event_(LTWiFiEvent *event) { if (it.old_mode != WIFI_AUTH_OPEN && it.new_mode == WIFI_AUTH_OPEN) { ESP_LOGW(TAG, "Potential Authmode downgrade detected, disconnecting"); WiFi.disconnect(); - this->error_from_callback_ = true; + this->error_from_callback_ = 1; s_sta_state = LTWiFiSTAState::ERROR_FAILED; } break; From ed4f00d4a3c80c54f6084305a640377f4f575105 Mon Sep 17 00:00:00 2001 From: Marek Beran <95686867+Bercek71@users.noreply.github.com> Date: Fri, 6 Feb 2026 08:11:14 +0100 Subject: [PATCH 02/11] [vbus] Add DeltaSol BS/2 support with sensors and binary sensors (#13762) --- esphome/components/vbus/__init__.py | 1 + .../components/vbus/binary_sensor/__init__.py | 41 +++++++ .../vbus/binary_sensor/vbus_binary_sensor.cpp | 19 +++ .../vbus/binary_sensor/vbus_binary_sensor.h | 17 +++ esphome/components/vbus/sensor/__init__.py | 110 ++++++++++++++++++ .../components/vbus/sensor/vbus_sensor.cpp | 39 +++++++ esphome/components/vbus/sensor/vbus_sensor.h | 30 +++++ tests/components/vbus/common.yaml | 50 ++++++-- 8 files changed, 298 insertions(+), 9 deletions(-) diff --git a/esphome/components/vbus/__init__.py b/esphome/components/vbus/__init__.py index d916d7c064..5790a9cce0 100644 --- a/esphome/components/vbus/__init__.py +++ b/esphome/components/vbus/__init__.py @@ -16,6 +16,7 @@ CONF_VBUS_ID = "vbus_id" CONF_DELTASOL_BS_PLUS = "deltasol_bs_plus" CONF_DELTASOL_BS_2009 = "deltasol_bs_2009" +CONF_DELTASOL_BS2 = "deltasol_bs2" CONF_DELTASOL_C = "deltasol_c" CONF_DELTASOL_CS2 = "deltasol_cs2" CONF_DELTASOL_CS_PLUS = "deltasol_cs_plus" diff --git a/esphome/components/vbus/binary_sensor/__init__.py b/esphome/components/vbus/binary_sensor/__init__.py index ae927656c0..70dda94300 100644 --- a/esphome/components/vbus/binary_sensor/__init__.py +++ b/esphome/components/vbus/binary_sensor/__init__.py @@ -15,6 +15,7 @@ from esphome.const import ( ) from .. import ( + CONF_DELTASOL_BS2, CONF_DELTASOL_BS_2009, CONF_DELTASOL_BS_PLUS, CONF_DELTASOL_C, @@ -27,6 +28,7 @@ from .. import ( DeltaSol_BS_Plus = vbus_ns.class_("DeltaSolBSPlusBSensor", cg.Component) DeltaSol_BS_2009 = vbus_ns.class_("DeltaSolBS2009BSensor", cg.Component) +DeltaSol_BS2 = vbus_ns.class_("DeltaSolBS2BSensor", cg.Component) DeltaSol_C = vbus_ns.class_("DeltaSolCBSensor", cg.Component) DeltaSol_CS2 = vbus_ns.class_("DeltaSolCS2BSensor", cg.Component) DeltaSol_CS_Plus = vbus_ns.class_("DeltaSolCSPlusBSensor", cg.Component) @@ -118,6 +120,28 @@ CONFIG_SCHEMA = cv.typed_schema( ), } ), + CONF_DELTASOL_BS2: cv.COMPONENT_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(DeltaSol_BS2), + cv.GenerateID(CONF_VBUS_ID): cv.use_id(VBus), + cv.Optional(CONF_SENSOR1_ERROR): binary_sensor.binary_sensor_schema( + device_class=DEVICE_CLASS_PROBLEM, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + cv.Optional(CONF_SENSOR2_ERROR): binary_sensor.binary_sensor_schema( + device_class=DEVICE_CLASS_PROBLEM, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + cv.Optional(CONF_SENSOR3_ERROR): binary_sensor.binary_sensor_schema( + device_class=DEVICE_CLASS_PROBLEM, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + cv.Optional(CONF_SENSOR4_ERROR): binary_sensor.binary_sensor_schema( + device_class=DEVICE_CLASS_PROBLEM, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + } + ), CONF_DELTASOL_C: cv.COMPONENT_SCHEMA.extend( { cv.GenerateID(): cv.declare_id(DeltaSol_C), @@ -275,6 +299,23 @@ async def to_code(config): ) cg.add(var.set_frost_protection_active_bsensor(sens)) + elif config[CONF_MODEL] == CONF_DELTASOL_BS2: + cg.add(var.set_command(0x0100)) + cg.add(var.set_source(0x4278)) + cg.add(var.set_dest(0x0010)) + if CONF_SENSOR1_ERROR in config: + sens = await binary_sensor.new_binary_sensor(config[CONF_SENSOR1_ERROR]) + cg.add(var.set_s1_error_bsensor(sens)) + if CONF_SENSOR2_ERROR in config: + sens = await binary_sensor.new_binary_sensor(config[CONF_SENSOR2_ERROR]) + cg.add(var.set_s2_error_bsensor(sens)) + if CONF_SENSOR3_ERROR in config: + sens = await binary_sensor.new_binary_sensor(config[CONF_SENSOR3_ERROR]) + cg.add(var.set_s3_error_bsensor(sens)) + if CONF_SENSOR4_ERROR in config: + sens = await binary_sensor.new_binary_sensor(config[CONF_SENSOR4_ERROR]) + cg.add(var.set_s4_error_bsensor(sens)) + elif config[CONF_MODEL] == CONF_DELTASOL_C: cg.add(var.set_command(0x0100)) cg.add(var.set_source(0x4212)) diff --git a/esphome/components/vbus/binary_sensor/vbus_binary_sensor.cpp b/esphome/components/vbus/binary_sensor/vbus_binary_sensor.cpp index 4ccd149935..c1d7bc1b18 100644 --- a/esphome/components/vbus/binary_sensor/vbus_binary_sensor.cpp +++ b/esphome/components/vbus/binary_sensor/vbus_binary_sensor.cpp @@ -129,6 +129,25 @@ void DeltaSolCSPlusBSensor::handle_message(std::vector &message) { this->s4_error_bsensor_->publish_state(message[20] & 8); } +void DeltaSolBS2BSensor::dump_config() { + ESP_LOGCONFIG(TAG, "DeltaSol BS/2 (DrainBack):"); + LOG_BINARY_SENSOR(" ", "Sensor 1 Error", this->s1_error_bsensor_); + LOG_BINARY_SENSOR(" ", "Sensor 2 Error", this->s2_error_bsensor_); + LOG_BINARY_SENSOR(" ", "Sensor 3 Error", this->s3_error_bsensor_); + LOG_BINARY_SENSOR(" ", "Sensor 4 Error", this->s4_error_bsensor_); +} + +void DeltaSolBS2BSensor::handle_message(std::vector &message) { + if (this->s1_error_bsensor_ != nullptr) + this->s1_error_bsensor_->publish_state(message[10] & 1); + if (this->s2_error_bsensor_ != nullptr) + this->s2_error_bsensor_->publish_state(message[10] & 2); + if (this->s3_error_bsensor_ != nullptr) + this->s3_error_bsensor_->publish_state(message[10] & 4); + if (this->s4_error_bsensor_ != nullptr) + this->s4_error_bsensor_->publish_state(message[10] & 8); +} + void VBusCustomBSensor::dump_config() { ESP_LOGCONFIG(TAG, "VBus Custom Binary Sensor:"); if (this->source_ == 0xffff) { diff --git a/esphome/components/vbus/binary_sensor/vbus_binary_sensor.h b/esphome/components/vbus/binary_sensor/vbus_binary_sensor.h index 146aa1b673..2decdde602 100644 --- a/esphome/components/vbus/binary_sensor/vbus_binary_sensor.h +++ b/esphome/components/vbus/binary_sensor/vbus_binary_sensor.h @@ -111,6 +111,23 @@ class DeltaSolCSPlusBSensor : public VBusListener, public Component { void handle_message(std::vector &message) override; }; +class DeltaSolBS2BSensor : public VBusListener, public Component { + public: + void dump_config() override; + void set_s1_error_bsensor(binary_sensor::BinarySensor *bsensor) { this->s1_error_bsensor_ = bsensor; } + void set_s2_error_bsensor(binary_sensor::BinarySensor *bsensor) { this->s2_error_bsensor_ = bsensor; } + void set_s3_error_bsensor(binary_sensor::BinarySensor *bsensor) { this->s3_error_bsensor_ = bsensor; } + void set_s4_error_bsensor(binary_sensor::BinarySensor *bsensor) { this->s4_error_bsensor_ = bsensor; } + + protected: + binary_sensor::BinarySensor *s1_error_bsensor_{nullptr}; + binary_sensor::BinarySensor *s2_error_bsensor_{nullptr}; + binary_sensor::BinarySensor *s3_error_bsensor_{nullptr}; + binary_sensor::BinarySensor *s4_error_bsensor_{nullptr}; + + void handle_message(std::vector &message) override; +}; + class VBusCustomSubBSensor; class VBusCustomBSensor : public VBusListener, public Component { diff --git a/esphome/components/vbus/sensor/__init__.py b/esphome/components/vbus/sensor/__init__.py index fcff698ac0..ff8ef98a1a 100644 --- a/esphome/components/vbus/sensor/__init__.py +++ b/esphome/components/vbus/sensor/__init__.py @@ -31,6 +31,7 @@ from esphome.const import ( ) from .. import ( + CONF_DELTASOL_BS2, CONF_DELTASOL_BS_2009, CONF_DELTASOL_BS_PLUS, CONF_DELTASOL_C, @@ -43,6 +44,7 @@ from .. import ( DeltaSol_BS_Plus = vbus_ns.class_("DeltaSolBSPlusSensor", cg.Component) DeltaSol_BS_2009 = vbus_ns.class_("DeltaSolBS2009Sensor", cg.Component) +DeltaSol_BS2 = vbus_ns.class_("DeltaSolBS2Sensor", cg.Component) DeltaSol_C = vbus_ns.class_("DeltaSolCSensor", cg.Component) DeltaSol_CS2 = vbus_ns.class_("DeltaSolCS2Sensor", cg.Component) DeltaSol_CS_Plus = vbus_ns.class_("DeltaSolCSPlusSensor", cg.Component) @@ -227,6 +229,79 @@ CONFIG_SCHEMA = cv.typed_schema( ), } ), + CONF_DELTASOL_BS2: cv.COMPONENT_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(DeltaSol_BS2), + cv.GenerateID(CONF_VBUS_ID): cv.use_id(VBus), + cv.Optional(CONF_TEMPERATURE_1): sensor.sensor_schema( + unit_of_measurement=UNIT_CELSIUS, + icon=ICON_THERMOMETER, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_TEMPERATURE_2): sensor.sensor_schema( + unit_of_measurement=UNIT_CELSIUS, + icon=ICON_THERMOMETER, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_TEMPERATURE_3): sensor.sensor_schema( + unit_of_measurement=UNIT_CELSIUS, + icon=ICON_THERMOMETER, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_TEMPERATURE_4): sensor.sensor_schema( + unit_of_measurement=UNIT_CELSIUS, + icon=ICON_THERMOMETER, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_PUMP_SPEED_1): sensor.sensor_schema( + unit_of_measurement=UNIT_PERCENT, + icon=ICON_PERCENT, + accuracy_decimals=0, + device_class=DEVICE_CLASS_EMPTY, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_PUMP_SPEED_2): sensor.sensor_schema( + unit_of_measurement=UNIT_PERCENT, + icon=ICON_PERCENT, + accuracy_decimals=0, + device_class=DEVICE_CLASS_EMPTY, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_OPERATING_HOURS_1): sensor.sensor_schema( + unit_of_measurement=UNIT_HOUR, + icon=ICON_TIMER, + accuracy_decimals=0, + device_class=DEVICE_CLASS_DURATION, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_OPERATING_HOURS_2): sensor.sensor_schema( + unit_of_measurement=UNIT_HOUR, + icon=ICON_TIMER, + accuracy_decimals=0, + device_class=DEVICE_CLASS_DURATION, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_HEAT_QUANTITY): sensor.sensor_schema( + unit_of_measurement=UNIT_WATT_HOURS, + icon=ICON_RADIATOR, + accuracy_decimals=0, + device_class=DEVICE_CLASS_ENERGY, + state_class=STATE_CLASS_TOTAL_INCREASING, + ), + cv.Optional(CONF_VERSION): sensor.sensor_schema( + accuracy_decimals=2, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + } + ), CONF_DELTASOL_C: cv.COMPONENT_SCHEMA.extend( { cv.GenerateID(): cv.declare_id(DeltaSol_C), @@ -560,6 +635,41 @@ async def to_code(config): sens = await sensor.new_sensor(config[CONF_VERSION]) cg.add(var.set_version_sensor(sens)) + elif config[CONF_MODEL] == CONF_DELTASOL_BS2: + cg.add(var.set_command(0x0100)) + cg.add(var.set_source(0x4278)) + cg.add(var.set_dest(0x0010)) + if CONF_TEMPERATURE_1 in config: + sens = await sensor.new_sensor(config[CONF_TEMPERATURE_1]) + cg.add(var.set_temperature1_sensor(sens)) + if CONF_TEMPERATURE_2 in config: + sens = await sensor.new_sensor(config[CONF_TEMPERATURE_2]) + cg.add(var.set_temperature2_sensor(sens)) + if CONF_TEMPERATURE_3 in config: + sens = await sensor.new_sensor(config[CONF_TEMPERATURE_3]) + cg.add(var.set_temperature3_sensor(sens)) + if CONF_TEMPERATURE_4 in config: + sens = await sensor.new_sensor(config[CONF_TEMPERATURE_4]) + cg.add(var.set_temperature4_sensor(sens)) + if CONF_PUMP_SPEED_1 in config: + sens = await sensor.new_sensor(config[CONF_PUMP_SPEED_1]) + cg.add(var.set_pump_speed1_sensor(sens)) + if CONF_PUMP_SPEED_2 in config: + sens = await sensor.new_sensor(config[CONF_PUMP_SPEED_2]) + cg.add(var.set_pump_speed2_sensor(sens)) + if CONF_OPERATING_HOURS_1 in config: + sens = await sensor.new_sensor(config[CONF_OPERATING_HOURS_1]) + cg.add(var.set_operating_hours1_sensor(sens)) + if CONF_OPERATING_HOURS_2 in config: + sens = await sensor.new_sensor(config[CONF_OPERATING_HOURS_2]) + cg.add(var.set_operating_hours2_sensor(sens)) + if CONF_HEAT_QUANTITY in config: + sens = await sensor.new_sensor(config[CONF_HEAT_QUANTITY]) + cg.add(var.set_heat_quantity_sensor(sens)) + if CONF_VERSION in config: + sens = await sensor.new_sensor(config[CONF_VERSION]) + cg.add(var.set_version_sensor(sens)) + elif config[CONF_MODEL] == CONF_DELTASOL_C: cg.add(var.set_command(0x0100)) cg.add(var.set_source(0x4212)) diff --git a/esphome/components/vbus/sensor/vbus_sensor.cpp b/esphome/components/vbus/sensor/vbus_sensor.cpp index e81c0486d4..75c9ea1aee 100644 --- a/esphome/components/vbus/sensor/vbus_sensor.cpp +++ b/esphome/components/vbus/sensor/vbus_sensor.cpp @@ -214,6 +214,45 @@ void DeltaSolCSPlusSensor::handle_message(std::vector &message) { this->flow_rate_sensor_->publish_state(get_u16(message, 38)); } +void DeltaSolBS2Sensor::dump_config() { + ESP_LOGCONFIG(TAG, "DeltaSol BS/2 (DrainBack):"); + LOG_SENSOR(" ", "Temperature 1", this->temperature1_sensor_); + LOG_SENSOR(" ", "Temperature 2", this->temperature2_sensor_); + LOG_SENSOR(" ", "Temperature 3", this->temperature3_sensor_); + LOG_SENSOR(" ", "Temperature 4", this->temperature4_sensor_); + LOG_SENSOR(" ", "Pump Speed 1", this->pump_speed1_sensor_); + LOG_SENSOR(" ", "Pump Speed 2", this->pump_speed2_sensor_); + LOG_SENSOR(" ", "Operating Hours 1", this->operating_hours1_sensor_); + LOG_SENSOR(" ", "Operating Hours 2", this->operating_hours2_sensor_); + LOG_SENSOR(" ", "Heat Quantity", this->heat_quantity_sensor_); + LOG_SENSOR(" ", "FW Version", this->version_sensor_); +} + +void DeltaSolBS2Sensor::handle_message(std::vector &message) { + if (this->temperature1_sensor_ != nullptr) + this->temperature1_sensor_->publish_state(get_i16(message, 0) * 0.1f); + if (this->temperature2_sensor_ != nullptr) + this->temperature2_sensor_->publish_state(get_i16(message, 2) * 0.1f); + if (this->temperature3_sensor_ != nullptr) + this->temperature3_sensor_->publish_state(get_i16(message, 4) * 0.1f); + if (this->temperature4_sensor_ != nullptr) + this->temperature4_sensor_->publish_state(get_i16(message, 6) * 0.1f); + if (this->pump_speed1_sensor_ != nullptr) + this->pump_speed1_sensor_->publish_state(message[8]); + if (this->pump_speed2_sensor_ != nullptr) + this->pump_speed2_sensor_->publish_state(message[9]); + if (this->operating_hours1_sensor_ != nullptr) + this->operating_hours1_sensor_->publish_state(get_u16(message, 12)); + if (this->operating_hours2_sensor_ != nullptr) + this->operating_hours2_sensor_->publish_state(get_u16(message, 14)); + if (this->heat_quantity_sensor_ != nullptr) { + float heat_wh = get_u16(message, 16) + get_u16(message, 18) * 1000.0f + get_u16(message, 20) * 1000000.0f; + this->heat_quantity_sensor_->publish_state(heat_wh); + } + if (this->version_sensor_ != nullptr) + this->version_sensor_->publish_state(get_u16(message, 24) * 0.01f); +} + void VBusCustomSensor::dump_config() { ESP_LOGCONFIG(TAG, "VBus Custom Sensor:"); if (this->source_ == 0xffff) { diff --git a/esphome/components/vbus/sensor/vbus_sensor.h b/esphome/components/vbus/sensor/vbus_sensor.h index d5535b2019..cea2ee1c86 100644 --- a/esphome/components/vbus/sensor/vbus_sensor.h +++ b/esphome/components/vbus/sensor/vbus_sensor.h @@ -157,6 +157,36 @@ class DeltaSolCSPlusSensor : public VBusListener, public Component { void handle_message(std::vector &message) override; }; +class DeltaSolBS2Sensor : public VBusListener, public Component { + public: + void dump_config() override; + + void set_temperature1_sensor(sensor::Sensor *sensor) { this->temperature1_sensor_ = sensor; } + void set_temperature2_sensor(sensor::Sensor *sensor) { this->temperature2_sensor_ = sensor; } + void set_temperature3_sensor(sensor::Sensor *sensor) { this->temperature3_sensor_ = sensor; } + void set_temperature4_sensor(sensor::Sensor *sensor) { this->temperature4_sensor_ = sensor; } + void set_pump_speed1_sensor(sensor::Sensor *sensor) { this->pump_speed1_sensor_ = sensor; } + void set_pump_speed2_sensor(sensor::Sensor *sensor) { this->pump_speed2_sensor_ = sensor; } + void set_operating_hours1_sensor(sensor::Sensor *sensor) { this->operating_hours1_sensor_ = sensor; } + void set_operating_hours2_sensor(sensor::Sensor *sensor) { this->operating_hours2_sensor_ = sensor; } + void set_heat_quantity_sensor(sensor::Sensor *sensor) { this->heat_quantity_sensor_ = sensor; } + void set_version_sensor(sensor::Sensor *sensor) { this->version_sensor_ = sensor; } + + protected: + sensor::Sensor *temperature1_sensor_{nullptr}; + sensor::Sensor *temperature2_sensor_{nullptr}; + sensor::Sensor *temperature3_sensor_{nullptr}; + sensor::Sensor *temperature4_sensor_{nullptr}; + sensor::Sensor *pump_speed1_sensor_{nullptr}; + sensor::Sensor *pump_speed2_sensor_{nullptr}; + sensor::Sensor *operating_hours1_sensor_{nullptr}; + sensor::Sensor *operating_hours2_sensor_{nullptr}; + sensor::Sensor *heat_quantity_sensor_{nullptr}; + sensor::Sensor *version_sensor_{nullptr}; + + void handle_message(std::vector &message) override; +}; + class VBusCustomSubSensor; class VBusCustomSensor : public VBusListener, public Component { diff --git a/tests/components/vbus/common.yaml b/tests/components/vbus/common.yaml index 33d9e2935d..5c771be922 100644 --- a/tests/components/vbus/common.yaml +++ b/tests/components/vbus/common.yaml @@ -4,11 +4,21 @@ binary_sensor: - platform: vbus model: deltasol_bs_plus relay1: - name: Relay 1 On + name: BS Plus Relay 1 On relay2: - name: Relay 2 On + name: BS Plus Relay 2 On sensor1_error: - name: Sensor 1 Error + name: BS Plus Sensor 1 Error + - platform: vbus + model: deltasol_bs2 + sensor1_error: + name: BS2 Sensor 1 Error + sensor2_error: + name: BS2 Sensor 2 Error + sensor3_error: + name: BS2 Sensor 3 Error + sensor4_error: + name: BS2 Sensor 4 Error - platform: vbus model: custom command: 0x100 @@ -23,14 +33,36 @@ sensor: - platform: vbus model: deltasol c temperature_1: - name: Temperature 1 + name: DeltaSol C Temperature 1 temperature_2: - name: Temperature 2 + name: DeltaSol C Temperature 2 temperature_3: - name: Temperature 3 + name: DeltaSol C Temperature 3 operating_hours_1: - name: Operating Hours 1 + name: DeltaSol C Operating Hours 1 heat_quantity: - name: Heat Quantity + name: DeltaSol C Heat Quantity time: - name: System Time + name: DeltaSol C System Time + - platform: vbus + model: deltasol_bs2 + temperature_1: + name: BS2 Temperature 1 + temperature_2: + name: BS2 Temperature 2 + temperature_3: + name: BS2 Temperature 3 + temperature_4: + name: BS2 Temperature 4 + pump_speed_1: + name: BS2 Pump Speed 1 + pump_speed_2: + name: BS2 Pump Speed 2 + operating_hours_1: + name: BS2 Operating Hours 1 + operating_hours_2: + name: BS2 Operating Hours 2 + heat_quantity: + name: BS2 Heat Quantity + version: + name: BS2 Firmware Version From 8fa94dbdf3eaa2cc2f0353e7caf11916f2bc78f6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 6 Feb 2026 09:25:12 +0100 Subject: [PATCH 03/11] merge --- esphome/components/wifi/wifi_component.cpp | 7 +++--- esphome/components/wifi/wifi_component.h | 29 +++++++++------------- 2 files changed, 16 insertions(+), 20 deletions(-) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index 44c92fcaa4..8a60da9f44 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -1464,9 +1464,10 @@ void WiFiComponent::check_connecting_finished(uint32_t now) { this->release_scan_results_(); -#ifdef USE_WIFI_CONNECT_STATE_LISTENERS +#if defined(USE_WIFI_CONNECT_STATE_LISTENERS) && !defined(USE_ESP8266) // Notify listeners now that state machine has reached STA_CONNECTED // This ensures wifi.connected condition returns true in listener automations + // On ESP8266, this is handled by process_pending_callbacks_() instead. this->notify_connect_state_listeners_(); #endif @@ -2193,7 +2194,7 @@ void WiFiComponent::release_scan_results_() { } } -#ifdef USE_WIFI_CONNECT_STATE_LISTENERS +#if defined(USE_WIFI_CONNECT_STATE_LISTENERS) && !defined(USE_ESP8266) void WiFiComponent::notify_connect_state_listeners_() { if (!this->pending_.connect_state) return; @@ -2206,7 +2207,7 @@ void WiFiComponent::notify_connect_state_listeners_() { listener->on_wifi_connect_state(StringRef(ssid, strlen(ssid)), bssid); } } -#endif // USE_WIFI_CONNECT_STATE_LISTENERS +#endif // USE_WIFI_CONNECT_STATE_LISTENERS && !USE_ESP8266 void WiFiComponent::check_roaming_(uint32_t now) { // Guard: not for hidden networks (may not appear in scan) diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index e910da2cbb..e6499e9c58 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -641,8 +641,9 @@ class WiFiComponent : public Component { /// Free scan results memory unless a component needs them void release_scan_results_(); -#ifdef USE_WIFI_CONNECT_STATE_LISTENERS +#if defined(USE_WIFI_CONNECT_STATE_LISTENERS) && !defined(USE_ESP8266) /// Notify connect state listeners (called after state machine reaches STA_CONNECTED) + /// On ESP8266, this is handled by process_pending_callbacks_() instead. void notify_connect_state_listeners_(); #endif @@ -726,18 +727,22 @@ class WiFiComponent : public Component { // This serves as both the error flag and stores the reason for deferred logging uint8_t error_from_callback_{0}; -#ifdef USE_ESP8266 - // Pending listener callbacks from system context (ESP8266 only) - // ESP8266 callbacks run in SDK system context with ~2KB stack where - // calling arbitrary listener callbacks is unsafe. These flags defer - // listener notifications to wifi_loop_() which runs with full stack. + // Pending listener callbacks deferred from platform callbacks to main loop. struct { +#ifdef USE_ESP8266 + // ESP8266 callbacks run in SDK system context with ~2KB stack where + // calling arbitrary listener callbacks is unsafe. These flags defer + // listener notifications to wifi_loop_() which runs with full stack. bool connect : 1; // STA connected, notify listeners bool disconnect : 1; // STA disconnected, notify listeners bool got_ip : 1; // Got IP, notify listeners bool scan_complete : 1; // Scan complete, notify listeners - } pending_{}; +#elif defined(USE_WIFI_CONNECT_STATE_LISTENERS) + // Non-ESP8266 platforms: deferred until state machine reaches STA_CONNECTED + // so wifi.connected condition returns true in listener automations. + bool connect_state : 1; #endif + } pending_{}; // Group all boolean values together bool has_ap_{false}; @@ -769,16 +774,6 @@ class WiFiComponent : public Component { SemaphoreHandle_t high_performance_semaphore_{nullptr}; #endif -#ifdef USE_WIFI_CONNECT_STATE_LISTENERS - // Pending listener notifications deferred until state machine reaches appropriate state. - // Listeners are notified after state transitions complete so conditions like - // wifi.connected return correct values in automations. - // Uses bitfields to minimize memory; more flags may be added as needed. - struct { - bool connect_state : 1; // Notify connect state listeners after STA_CONNECTED - } pending_{}; -#endif - #ifdef USE_WIFI_CONNECT_TRIGGER Trigger<> connect_trigger_; #endif From 2573863d827def717e28fd6978f4ebb12a441f8f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 6 Feb 2026 09:31:06 +0100 Subject: [PATCH 04/11] adjust --- esphome/components/wifi/wifi_component.cpp | 17 ++++++++--- esphome/components/wifi/wifi_component.h | 13 ++++----- .../wifi/wifi_component_esp8266.cpp | 28 ++++++------------- 3 files changed, 27 insertions(+), 31 deletions(-) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index 8a60da9f44..14129a1f6e 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -1464,13 +1464,22 @@ void WiFiComponent::check_connecting_finished(uint32_t now) { this->release_scan_results_(); -#if defined(USE_WIFI_CONNECT_STATE_LISTENERS) && !defined(USE_ESP8266) +#ifdef USE_WIFI_CONNECT_STATE_LISTENERS // Notify listeners now that state machine has reached STA_CONNECTED // This ensures wifi.connected condition returns true in listener automations - // On ESP8266, this is handled by process_pending_callbacks_() instead. this->notify_connect_state_listeners_(); #endif +#if defined(USE_ESP8266) && defined(USE_WIFI_IP_STATE_LISTENERS) && defined(USE_WIFI_MANUAL_IP) + // On ESP8266, GOT_IP event may not fire for static IP configurations, + // so notify IP state listeners here as a fallback. + if (const WiFiAP *config = this->get_selected_sta_(); config && config->get_manual_ip().has_value()) { + 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 + return; } @@ -2194,7 +2203,7 @@ void WiFiComponent::release_scan_results_() { } } -#if defined(USE_WIFI_CONNECT_STATE_LISTENERS) && !defined(USE_ESP8266) +#ifdef USE_WIFI_CONNECT_STATE_LISTENERS void WiFiComponent::notify_connect_state_listeners_() { if (!this->pending_.connect_state) return; @@ -2207,7 +2216,7 @@ void WiFiComponent::notify_connect_state_listeners_() { listener->on_wifi_connect_state(StringRef(ssid, strlen(ssid)), bssid); } } -#endif // USE_WIFI_CONNECT_STATE_LISTENERS && !USE_ESP8266 +#endif // USE_WIFI_CONNECT_STATE_LISTENERS void WiFiComponent::check_roaming_(uint32_t now) { // Guard: not for hidden networks (may not appear in scan) diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index e6499e9c58..fd705ab86e 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -641,9 +641,8 @@ class WiFiComponent : public Component { /// Free scan results memory unless a component needs them void release_scan_results_(); -#if defined(USE_WIFI_CONNECT_STATE_LISTENERS) && !defined(USE_ESP8266) +#ifdef USE_WIFI_CONNECT_STATE_LISTENERS /// Notify connect state listeners (called after state machine reaches STA_CONNECTED) - /// On ESP8266, this is handled by process_pending_callbacks_() instead. void notify_connect_state_listeners_(); #endif @@ -729,18 +728,18 @@ class WiFiComponent : public Component { // Pending listener callbacks deferred from platform callbacks to main loop. struct { +#ifdef USE_WIFI_CONNECT_STATE_LISTENERS + // Deferred until state machine reaches STA_CONNECTED so wifi.connected + // condition returns true in listener automations. + bool connect_state : 1; +#endif #ifdef USE_ESP8266 // ESP8266 callbacks run in SDK system context with ~2KB stack where // calling arbitrary listener callbacks is unsafe. These flags defer // listener notifications to wifi_loop_() which runs with full stack. - bool connect : 1; // STA connected, notify listeners bool disconnect : 1; // STA disconnected, notify listeners bool got_ip : 1; // Got IP, notify listeners bool scan_complete : 1; // Scan complete, notify listeners -#elif defined(USE_WIFI_CONNECT_STATE_LISTENERS) - // Non-ESP8266 platforms: deferred until state machine reaches STA_CONNECTED - // so wifi.connected condition returns true in listener automations. - bool connect_state : 1; #endif } pending_{}; diff --git a/esphome/components/wifi/wifi_component_esp8266.cpp b/esphome/components/wifi/wifi_component_esp8266.cpp index 222212c8e2..935f039efb 100644 --- a/esphome/components/wifi/wifi_component_esp8266.cpp +++ b/esphome/components/wifi/wifi_component_esp8266.cpp @@ -515,8 +515,11 @@ void WiFiComponent::wifi_event_callback(System_Event_t *event) { it.channel); #endif s_sta_connected = true; - // Defer listener callbacks to main loop - system context has limited stack - global_wifi_component->pending_.connect = true; +#ifdef USE_WIFI_CONNECT_STATE_LISTENERS + // Defer listener notification until state machine reaches STA_CONNECTED + // This ensures wifi.connected condition returns true in listener automations + global_wifi_component->pending_.connect_state = true; +#endif break; } case EVENT_STAMODE_DISCONNECTED: { @@ -959,24 +962,9 @@ network::IPAddress WiFiComponent::wifi_dns_ip_(int num) { return network::IPAddr void WiFiComponent::wifi_loop_() { this->process_pending_callbacks_(); } void WiFiComponent::process_pending_callbacks_() { - // Notify listeners for connect event (logging already done in callback) - if (this->pending_.connect) { - this->pending_.connect = false; -#ifdef USE_WIFI_CONNECT_STATE_LISTENERS - bssid_t bssid = this->wifi_bssid(); - char ssid_buf[SSID_BUFFER_SIZE]; - for (auto *listener : this->connect_state_listeners_) { - listener->on_wifi_connect_state(StringRef(this->wifi_ssid_to(ssid_buf)), bssid); - } -#endif -#if defined(USE_WIFI_IP_STATE_LISTENERS) && defined(USE_WIFI_MANUAL_IP) - if (const WiFiAP *config = this->get_selected_sta_(); config && config->get_manual_ip().has_value()) { - 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 - } + // Process callbacks deferred from ESP8266 SDK system context (~2KB stack) + // to main loop context (full stack). Connect state listeners are handled + // by notify_connect_state_listeners_() in the shared state machine code. // Notify listeners for disconnect event (logging already done in callback) if (this->pending_.disconnect) { From 2a74dd27e11469a3a5464414895e57609121d07f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 6 Feb 2026 09:33:21 +0100 Subject: [PATCH 05/11] adjust --- esphome/components/wifi/wifi_component.cpp | 12 +++++++++--- esphome/components/wifi/wifi_component.h | 6 ++++++ esphome/components/wifi/wifi_component_esp8266.cpp | 4 +--- esphome/components/wifi/wifi_component_esp_idf.cpp | 12 +++--------- esphome/components/wifi/wifi_component_libretiny.cpp | 12 +++--------- esphome/components/wifi/wifi_component_pico_w.cpp | 8 ++------ 6 files changed, 24 insertions(+), 30 deletions(-) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index 14129a1f6e..bf5145d13f 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -1474,9 +1474,7 @@ void WiFiComponent::check_connecting_finished(uint32_t now) { // On ESP8266, GOT_IP event may not fire for static IP configurations, // so notify IP state listeners here as a fallback. if (const WiFiAP *config = this->get_selected_sta_(); config && config->get_manual_ip().has_value()) { - 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)); - } + this->notify_ip_state_listeners_(); } #endif @@ -2218,6 +2216,14 @@ void WiFiComponent::notify_connect_state_listeners_() { } #endif // USE_WIFI_CONNECT_STATE_LISTENERS +#ifdef USE_WIFI_IP_STATE_LISTENERS +void WiFiComponent::notify_ip_state_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 // USE_WIFI_IP_STATE_LISTENERS + void WiFiComponent::check_roaming_(uint32_t now) { // Guard: not for hidden networks (may not appear in scan) const WiFiAP *selected = this->get_selected_sta_(); diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index fd705ab86e..2eb83f6bf4 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -645,6 +645,10 @@ class WiFiComponent : public Component { /// Notify connect state listeners (called after state machine reaches STA_CONNECTED) void notify_connect_state_listeners_(); #endif +#ifdef USE_WIFI_IP_STATE_LISTENERS + /// Notify IP state listeners with current addresses + void notify_ip_state_listeners_(); +#endif #ifdef USE_ESP8266 static void wifi_event_callback(System_Event_t *event); @@ -726,6 +730,7 @@ class WiFiComponent : public Component { // This serves as both the error flag and stores the reason for deferred logging uint8_t error_from_callback_{0}; +#if defined(USE_WIFI_CONNECT_STATE_LISTENERS) || defined(USE_ESP8266) // Pending listener callbacks deferred from platform callbacks to main loop. struct { #ifdef USE_WIFI_CONNECT_STATE_LISTENERS @@ -742,6 +747,7 @@ class WiFiComponent : public Component { bool scan_complete : 1; // Scan complete, notify listeners #endif } pending_{}; +#endif // Group all boolean values together bool has_ap_{false}; diff --git a/esphome/components/wifi/wifi_component_esp8266.cpp b/esphome/components/wifi/wifi_component_esp8266.cpp index 935f039efb..d3f7f45a7c 100644 --- a/esphome/components/wifi/wifi_component_esp8266.cpp +++ b/esphome/components/wifi/wifi_component_esp8266.cpp @@ -981,9 +981,7 @@ void WiFiComponent::process_pending_callbacks_() { if (this->pending_.got_ip) { this->pending_.got_ip = false; #ifdef USE_WIFI_IP_STATE_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)); - } + this->notify_ip_state_listeners_(); #endif } diff --git a/esphome/components/wifi/wifi_component_esp_idf.cpp b/esphome/components/wifi/wifi_component_esp_idf.cpp index 3c4e78d926..ad260221f3 100644 --- a/esphome/components/wifi/wifi_component_esp_idf.cpp +++ b/esphome/components/wifi/wifi_component_esp_idf.cpp @@ -753,9 +753,7 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) { // For static IP configurations, GOT_IP event may not fire, so notify IP listeners here #if defined(USE_WIFI_IP_STATE_LISTENERS) && defined(USE_WIFI_MANUAL_IP) if (const WiFiAP *config = this->get_selected_sta_(); config && config->get_manual_ip().has_value()) { - 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)); - } + this->notify_ip_state_listeners_(); } #endif @@ -793,9 +791,7 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) { 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_IP_STATE_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)); - } + this->notify_ip_state_listeners_(); #endif #if USE_NETWORK_IPV6 @@ -804,9 +800,7 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) { ESP_LOGV(TAG, "IPv6 address=" IPV6STR, IPV62STR(it.ip6_info.ip)); this->num_ipv6_addresses_++; #ifdef USE_WIFI_IP_STATE_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)); - } + this->notify_ip_state_listeners_(); #endif #endif /* USE_NETWORK_IPV6 */ diff --git a/esphome/components/wifi/wifi_component_libretiny.cpp b/esphome/components/wifi/wifi_component_libretiny.cpp index 4f9e2c48e9..39eca029a9 100644 --- a/esphome/components/wifi/wifi_component_libretiny.cpp +++ b/esphome/components/wifi/wifi_component_libretiny.cpp @@ -468,9 +468,7 @@ void WiFiComponent::wifi_process_event_(LTWiFiEvent *event) { if (const WiFiAP *config = this->get_selected_sta_(); config && config->get_manual_ip().has_value()) { s_sta_state = LTWiFiSTAState::CONNECTED; #ifdef USE_WIFI_IP_STATE_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)); - } + this->notify_ip_state_listeners_(); #endif } #endif @@ -553,18 +551,14 @@ void WiFiComponent::wifi_process_event_(LTWiFiEvent *event) { network::IPAddress(WiFi.gatewayIP()).str_to(gw_buf)); s_sta_state = LTWiFiSTAState::CONNECTED; #ifdef USE_WIFI_IP_STATE_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)); - } + this->notify_ip_state_listeners_(); #endif break; } case ESPHOME_EVENT_ID_WIFI_STA_GOT_IP6: { ESP_LOGV(TAG, "Got IPv6"); #ifdef USE_WIFI_IP_STATE_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)); - } + this->notify_ip_state_listeners_(); #endif break; } diff --git a/esphome/components/wifi/wifi_component_pico_w.cpp b/esphome/components/wifi/wifi_component_pico_w.cpp index 1ce36c2d93..146578a311 100644 --- a/esphome/components/wifi/wifi_component_pico_w.cpp +++ b/esphome/components/wifi/wifi_component_pico_w.cpp @@ -290,9 +290,7 @@ void WiFiComponent::wifi_loop_() { #if defined(USE_WIFI_IP_STATE_LISTENERS) && defined(USE_WIFI_MANUAL_IP) if (const WiFiAP *config = this->get_selected_sta_(); config && config->get_manual_ip().has_value()) { s_sta_had_ip = true; - 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)); - } + this->notify_ip_state_listeners_(); } #endif } else if (!is_connected && s_sta_was_connected) { @@ -322,9 +320,7 @@ void WiFiComponent::wifi_loop_() { s_sta_had_ip = true; ESP_LOGV(TAG, "Got IP address"); #ifdef USE_WIFI_IP_STATE_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)); - } + this->notify_ip_state_listeners_(); #endif } } From 3173283166a2f3006d52b0baf545edb17ea13ed7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 6 Feb 2026 09:36:01 +0100 Subject: [PATCH 06/11] de-dupe --- esphome/components/wifi/wifi_component.cpp | 15 +++++++++++++++ esphome/components/wifi/wifi_component.h | 6 ++++++ .../components/wifi/wifi_component_esp8266.cpp | 9 ++------- .../components/wifi/wifi_component_esp_idf.cpp | 9 ++------- .../components/wifi/wifi_component_libretiny.cpp | 9 ++------- esphome/components/wifi/wifi_component_pico_w.cpp | 9 ++------- 6 files changed, 29 insertions(+), 28 deletions(-) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index bf5145d13f..1d07af206b 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -2214,6 +2214,13 @@ void WiFiComponent::notify_connect_state_listeners_() { listener->on_wifi_connect_state(StringRef(ssid, strlen(ssid)), bssid); } } + +void WiFiComponent::notify_disconnect_state_listeners_() { + static constexpr uint8_t EMPTY_BSSID[6] = {}; + for (auto *listener : this->connect_state_listeners_) { + listener->on_wifi_connect_state(StringRef(), EMPTY_BSSID); + } +} #endif // USE_WIFI_CONNECT_STATE_LISTENERS #ifdef USE_WIFI_IP_STATE_LISTENERS @@ -2224,6 +2231,14 @@ void WiFiComponent::notify_ip_state_listeners_() { } #endif // USE_WIFI_IP_STATE_LISTENERS +#ifdef USE_WIFI_SCAN_RESULTS_LISTENERS +void WiFiComponent::notify_scan_results_listeners_() { + for (auto *listener : this->scan_results_listeners_) { + listener->on_wifi_scan_results(this->scan_result_); + } +} +#endif // USE_WIFI_SCAN_RESULTS_LISTENERS + void WiFiComponent::check_roaming_(uint32_t now) { // Guard: not for hidden networks (may not appear in scan) const WiFiAP *selected = this->get_selected_sta_(); diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index 2eb83f6bf4..e48c0db47c 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -644,11 +644,17 @@ class WiFiComponent : public Component { #ifdef USE_WIFI_CONNECT_STATE_LISTENERS /// Notify connect state listeners (called after state machine reaches STA_CONNECTED) void notify_connect_state_listeners_(); + /// Notify connect state listeners of disconnection + void notify_disconnect_state_listeners_(); #endif #ifdef USE_WIFI_IP_STATE_LISTENERS /// Notify IP state listeners with current addresses void notify_ip_state_listeners_(); #endif +#ifdef USE_WIFI_SCAN_RESULTS_LISTENERS + /// Notify scan results listeners with current scan results + void notify_scan_results_listeners_(); +#endif #ifdef USE_ESP8266 static void wifi_event_callback(System_Event_t *event); diff --git a/esphome/components/wifi/wifi_component_esp8266.cpp b/esphome/components/wifi/wifi_component_esp8266.cpp index d3f7f45a7c..99b66a76d6 100644 --- a/esphome/components/wifi/wifi_component_esp8266.cpp +++ b/esphome/components/wifi/wifi_component_esp8266.cpp @@ -970,10 +970,7 @@ void WiFiComponent::process_pending_callbacks_() { if (this->pending_.disconnect) { this->pending_.disconnect = false; #ifdef USE_WIFI_CONNECT_STATE_LISTENERS - static constexpr uint8_t EMPTY_BSSID[6] = {}; - for (auto *listener : this->connect_state_listeners_) { - listener->on_wifi_connect_state(StringRef(), EMPTY_BSSID); - } + this->notify_disconnect_state_listeners_(); #endif } @@ -989,9 +986,7 @@ void WiFiComponent::process_pending_callbacks_() { if (this->pending_.scan_complete) { this->pending_.scan_complete = false; #ifdef USE_WIFI_SCAN_RESULTS_LISTENERS - for (auto *listener : this->scan_results_listeners_) { - listener->on_wifi_scan_results(this->scan_result_); - } + this->notify_scan_results_listeners_(); #endif } } diff --git a/esphome/components/wifi/wifi_component_esp_idf.cpp b/esphome/components/wifi/wifi_component_esp_idf.cpp index ad260221f3..62bb2ad162 100644 --- a/esphome/components/wifi/wifi_component_esp_idf.cpp +++ b/esphome/components/wifi/wifi_component_esp_idf.cpp @@ -777,10 +777,7 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) { s_sta_connecting = false; error_from_callback_ = 1; #ifdef USE_WIFI_CONNECT_STATE_LISTENERS - static constexpr uint8_t EMPTY_BSSID[6] = {}; - for (auto *listener : this->connect_state_listeners_) { - listener->on_wifi_connect_state(StringRef(), EMPTY_BSSID); - } + this->notify_disconnect_state_listeners_(); #endif } else if (data->event_base == IP_EVENT && data->event_id == IP_EVENT_STA_GOT_IP) { @@ -877,9 +874,7 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) { ESP_LOGV(TAG, "Scan complete: %u found, %zu stored%s", number, this->scan_result_.size(), needs_full ? "" : " (filtered)"); #ifdef USE_WIFI_SCAN_RESULTS_LISTENERS - for (auto *listener : this->scan_results_listeners_) { - listener->on_wifi_scan_results(this->scan_result_); - } + this->notify_scan_results_listeners_(); #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 39eca029a9..a69f1c530a 100644 --- a/esphome/components/wifi/wifi_component_libretiny.cpp +++ b/esphome/components/wifi/wifi_component_libretiny.cpp @@ -525,10 +525,7 @@ void WiFiComponent::wifi_process_event_(LTWiFiEvent *event) { } #ifdef USE_WIFI_CONNECT_STATE_LISTENERS - static constexpr uint8_t EMPTY_BSSID[6] = {}; - for (auto *listener : this->connect_state_listeners_) { - listener->on_wifi_connect_state(StringRef(), EMPTY_BSSID); - } + this->notify_disconnect_state_listeners_(); #endif break; } @@ -702,9 +699,7 @@ void WiFiComponent::wifi_scan_done_callback_() { needs_full ? "" : " (filtered)"); WiFi.scanDelete(); #ifdef USE_WIFI_SCAN_RESULTS_LISTENERS - for (auto *listener : this->scan_results_listeners_) { - listener->on_wifi_scan_results(this->scan_result_); - } + this->notify_scan_results_listeners_(); #endif } diff --git a/esphome/components/wifi/wifi_component_pico_w.cpp b/esphome/components/wifi/wifi_component_pico_w.cpp index 146578a311..818ad1059c 100644 --- a/esphome/components/wifi/wifi_component_pico_w.cpp +++ b/esphome/components/wifi/wifi_component_pico_w.cpp @@ -264,9 +264,7 @@ void WiFiComponent::wifi_loop_() { ESP_LOGV(TAG, "Scan complete: %zu found, %zu stored%s", s_scan_result_count, this->scan_result_.size(), needs_full ? "" : " (filtered)"); #ifdef USE_WIFI_SCAN_RESULTS_LISTENERS - for (auto *listener : this->scan_results_listeners_) { - listener->on_wifi_scan_results(this->scan_result_); - } + this->notify_scan_results_listeners_(); #endif } @@ -299,10 +297,7 @@ void WiFiComponent::wifi_loop_() { s_sta_had_ip = false; ESP_LOGV(TAG, "Disconnected"); #ifdef USE_WIFI_CONNECT_STATE_LISTENERS - static constexpr uint8_t EMPTY_BSSID[6] = {}; - for (auto *listener : this->connect_state_listeners_) { - listener->on_wifi_connect_state(StringRef(), EMPTY_BSSID); - } + this->notify_disconnect_state_listeners_(); #endif } From 107e470410288d0a570bf3f265d54c413588c5cd Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 6 Feb 2026 09:37:07 +0100 Subject: [PATCH 07/11] de-dupe --- esphome/components/wifi/wifi_component.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index 1d07af206b..93b2bd9d6a 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -2216,7 +2216,7 @@ void WiFiComponent::notify_connect_state_listeners_() { } void WiFiComponent::notify_disconnect_state_listeners_() { - static constexpr uint8_t EMPTY_BSSID[6] = {}; + constexpr uint8_t EMPTY_BSSID[6] = {}; for (auto *listener : this->connect_state_listeners_) { listener->on_wifi_connect_state(StringRef(), EMPTY_BSSID); } From 7a7cc66141adb683e952bf2ab7b2a3aa09a489ff Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 6 Feb 2026 09:39:22 +0100 Subject: [PATCH 08/11] tweak members --- esphome/components/wifi/wifi_component.h | 38 ++++++++++++------------ 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index e48c0db47c..130fcb8291 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -677,13 +677,13 @@ class WiFiComponent : public Component { void wifi_scan_done_callback_(); #endif + // Large/pointer-aligned members first FixedVector sta_; std::vector sta_priorities_; wifi_scan_vector_t scan_result_; #ifdef USE_WIFI_AP WiFiAP ap_; #endif - float output_power_{NAN}; #ifdef USE_WIFI_IP_STATE_LISTENERS StaticVector ip_state_listeners_; #endif @@ -700,6 +700,15 @@ class WiFiComponent : public Component { #ifdef USE_WIFI_FAST_CONNECT ESPPreferenceObject fast_connect_pref_; #endif +#ifdef USE_WIFI_CONNECT_TRIGGER + Trigger<> connect_trigger_; +#endif +#ifdef USE_WIFI_DISCONNECT_TRIGGER + Trigger<> disconnect_trigger_; +#endif +#if defined(USE_ESP32) && defined(USE_WIFI_RUNTIME_POWER_SAVE) + SemaphoreHandle_t high_performance_semaphore_{nullptr}; +#endif // Post-connect roaming constants static constexpr uint32_t ROAMING_CHECK_INTERVAL = 5 * 60 * 1000; // 5 minutes @@ -707,7 +716,8 @@ class WiFiComponent : public Component { static constexpr int8_t ROAMING_GOOD_RSSI = -49; // Skip scan if signal is excellent static constexpr uint8_t ROAMING_MAX_ATTEMPTS = 3; - // Group all 32-bit integers together + // 4-byte members + float output_power_{NAN}; uint32_t action_started_; uint32_t last_connected_{0}; uint32_t reboot_timeout_{}; @@ -716,7 +726,7 @@ class WiFiComponent : public Component { uint32_t ap_timeout_{}; #endif - // Group all 8-bit values together + // 1-byte enums and integers WiFiComponentState state_{WIFI_COMPONENT_STATE_OFF}; WiFiPowerSaveMode power_save_{WIFI_POWER_SAVE_NONE}; WifiMinAuthMode min_auth_mode_{WIFI_MIN_AUTH_MODE_WPA2}; @@ -727,15 +737,19 @@ class WiFiComponent : public Component { // int8_t limits to 127 APs (enforced in __init__.py via MAX_WIFI_NETWORKS) int8_t selected_sta_index_{-1}; uint8_t roaming_attempts_{0}; - #if USE_NETWORK_IPV6 uint8_t num_ipv6_addresses_{0}; #endif /* USE_NETWORK_IPV6 */ - // 0 = no error, non-zero = disconnect reason code from callback // This serves as both the error flag and stores the reason for deferred logging uint8_t error_from_callback_{0}; + RetryHiddenMode retry_hidden_mode_{RetryHiddenMode::BLIND_RETRY}; + RoamingState roaming_state_{RoamingState::IDLE}; +#if defined(USE_ESP32) && defined(USE_WIFI_RUNTIME_POWER_SAVE) + WiFiPowerSaveMode configured_power_save_{WIFI_POWER_SAVE_NONE}; +#endif + // Bools and bitfields #if defined(USE_WIFI_CONNECT_STATE_LISTENERS) || defined(USE_ESP8266) // Pending listener callbacks deferred from platform callbacks to main loop. struct { @@ -754,8 +768,6 @@ class WiFiComponent : public Component { #endif } pending_{}; #endif - - // Group all boolean values together bool has_ap_{false}; #if defined(USE_WIFI_CONNECT_TRIGGER) || defined(USE_WIFI_DISCONNECT_TRIGGER) bool handled_connected_state_{false}; @@ -774,22 +786,10 @@ class WiFiComponent : public Component { bool keep_scan_results_{false}; bool has_completed_scan_after_captive_portal_start_{ false}; // Tracks if we've completed a scan after captive portal started - RetryHiddenMode retry_hidden_mode_{RetryHiddenMode::BLIND_RETRY}; bool skip_cooldown_next_cycle_{false}; bool post_connect_roaming_{true}; // Enabled by default - 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}; - - SemaphoreHandle_t high_performance_semaphore_{nullptr}; -#endif - -#ifdef USE_WIFI_CONNECT_TRIGGER - Trigger<> connect_trigger_; -#endif -#ifdef USE_WIFI_DISCONNECT_TRIGGER - Trigger<> disconnect_trigger_; #endif private: From c80c06cabd5d05704902411aaacc68f694e5466b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 6 Feb 2026 09:45:59 +0100 Subject: [PATCH 09/11] cleanup guards --- esphome/components/wifi/wifi_component.h | 18 ++++++++-------- .../wifi/wifi_component_esp8266.cpp | 21 +++++++++++-------- 2 files changed, 21 insertions(+), 18 deletions(-) diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index 130fcb8291..d92a7faeb3 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -750,24 +750,24 @@ class WiFiComponent : public Component { #endif // Bools and bitfields -#if defined(USE_WIFI_CONNECT_STATE_LISTENERS) || defined(USE_ESP8266) // Pending listener callbacks deferred from platform callbacks to main loop. struct { #ifdef USE_WIFI_CONNECT_STATE_LISTENERS // Deferred until state machine reaches STA_CONNECTED so wifi.connected // condition returns true in listener automations. bool connect_state : 1; -#endif #ifdef USE_ESP8266 - // ESP8266 callbacks run in SDK system context with ~2KB stack where - // calling arbitrary listener callbacks is unsafe. These flags defer - // listener notifications to wifi_loop_() which runs with full stack. - bool disconnect : 1; // STA disconnected, notify listeners - bool got_ip : 1; // Got IP, notify listeners - bool scan_complete : 1; // Scan complete, notify listeners + // ESP8266: also defer disconnect notification to main loop + bool disconnect : 1; +#endif +#endif +#if defined(USE_ESP8266) && defined(USE_WIFI_IP_STATE_LISTENERS) + bool got_ip : 1; +#endif +#if defined(USE_ESP8266) && defined(USE_WIFI_SCAN_RESULTS_LISTENERS) + bool scan_complete : 1; #endif } pending_{}; -#endif bool has_ap_{false}; #if defined(USE_WIFI_CONNECT_TRIGGER) || defined(USE_WIFI_DISCONNECT_TRIGGER) bool handled_connected_state_{false}; diff --git a/esphome/components/wifi/wifi_component_esp8266.cpp b/esphome/components/wifi/wifi_component_esp8266.cpp index 99b66a76d6..5026fbcb94 100644 --- a/esphome/components/wifi/wifi_component_esp8266.cpp +++ b/esphome/components/wifi/wifi_component_esp8266.cpp @@ -539,7 +539,9 @@ void WiFiComponent::wifi_event_callback(System_Event_t *event) { s_sta_connecting = false; // Store reason as error flag; defer listener callbacks to main loop global_wifi_component->error_from_callback_ = it.reason; +#ifdef USE_WIFI_CONNECT_STATE_LISTENERS global_wifi_component->pending_.disconnect = true; +#endif break; } case EVENT_STAMODE_AUTHMODE_CHANGE: { @@ -562,8 +564,10 @@ void WiFiComponent::wifi_event_callback(System_Event_t *event) { ESP_LOGV(TAG, "static_ip=%s gateway=%s netmask=%s", network::IPAddress(&it.ip).str_to(ip_buf), network::IPAddress(&it.gw).str_to(gw_buf), network::IPAddress(&it.mask).str_to(mask_buf)); s_sta_got_ip = true; +#ifdef USE_WIFI_IP_STATE_LISTENERS // Defer listener callbacks to main loop - system context has limited stack global_wifi_component->pending_.got_ip = true; +#endif break; } case EVENT_STAMODE_DHCP_TIMEOUT: { @@ -773,7 +777,9 @@ void WiFiComponent::wifi_scan_done_callback_(void *arg, STATUS status) { ESP_LOGV(TAG, "Scan complete: %zu found, %zu stored%s", total, this->scan_result_.size(), needs_full ? "" : " (filtered)"); this->scan_done_ = true; +#ifdef USE_WIFI_SCAN_RESULTS_LISTENERS this->pending_.scan_complete = true; // Defer listener callbacks to main loop +#endif } #ifdef USE_WIFI_AP @@ -966,29 +972,26 @@ void WiFiComponent::process_pending_callbacks_() { // to main loop context (full stack). Connect state listeners are handled // by notify_connect_state_listeners_() in the shared state machine code. - // Notify listeners for disconnect event (logging already done in callback) +#ifdef USE_WIFI_CONNECT_STATE_LISTENERS if (this->pending_.disconnect) { this->pending_.disconnect = false; -#ifdef USE_WIFI_CONNECT_STATE_LISTENERS this->notify_disconnect_state_listeners_(); -#endif } +#endif - // Notify listeners for got IP event (logging already done in callback) +#ifdef USE_WIFI_IP_STATE_LISTENERS if (this->pending_.got_ip) { this->pending_.got_ip = false; -#ifdef USE_WIFI_IP_STATE_LISTENERS this->notify_ip_state_listeners_(); -#endif } +#endif - // Notify listeners for scan complete (logging already done in callback) +#ifdef USE_WIFI_SCAN_RESULTS_LISTENERS if (this->pending_.scan_complete) { this->pending_.scan_complete = false; -#ifdef USE_WIFI_SCAN_RESULTS_LISTENERS this->notify_scan_results_listeners_(); -#endif } +#endif } } // namespace esphome::wifi From 8d8bd21cdaa3ab83cfe9b27d069a41a5d099c393 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 6 Feb 2026 09:46:58 +0100 Subject: [PATCH 10/11] cleanup guards --- esphome/components/wifi/wifi_component.h | 6 ------ esphome/components/wifi/wifi_component_esp8266.cpp | 4 ++++ 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index d92a7faeb3..2ef53f6946 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -58,12 +58,6 @@ static constexpr int8_t WIFI_RSSI_DISCONNECTED = -127; /// Buffer size for SSID (IEEE 802.11 max 32 bytes + null terminator) static constexpr size_t SSID_BUFFER_SIZE = 33; -#ifdef USE_ESP8266 -/// Special disconnect reason for authmode downgrade (CVE-2020-12638 mitigation) -/// Not a real WiFi reason code - used internally for deferred logging -static constexpr uint8_t WIFI_DISCONNECT_REASON_AUTHMODE_DOWNGRADE = 254; -#endif - struct SavedWifiSettings { char ssid[33]; char password[65]; diff --git a/esphome/components/wifi/wifi_component_esp8266.cpp b/esphome/components/wifi/wifi_component_esp8266.cpp index 5026fbcb94..3ae3b69174 100644 --- a/esphome/components/wifi/wifi_component_esp8266.cpp +++ b/esphome/components/wifi/wifi_component_esp8266.cpp @@ -42,6 +42,10 @@ namespace esphome::wifi { static const char *const TAG = "wifi_esp8266"; +/// Special disconnect reason for authmode downgrade (CVE-2020-12638 mitigation) +/// Not a real WiFi reason code - used internally for deferred logging +static constexpr uint8_t WIFI_DISCONNECT_REASON_AUTHMODE_DOWNGRADE = 254; + static bool s_sta_connected = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) static bool s_sta_got_ip = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) static bool s_sta_connect_not_found = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) From c4a4a86cff6edf4840dfd4e24ff7ff3be6437951 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 6 Feb 2026 09:56:42 +0100 Subject: [PATCH 11/11] tidy --- 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 93b2bd9d6a..906aa848cb 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -2216,9 +2216,9 @@ void WiFiComponent::notify_connect_state_listeners_() { } void WiFiComponent::notify_disconnect_state_listeners_() { - constexpr uint8_t EMPTY_BSSID[6] = {}; + constexpr uint8_t empty_bssid[6] = {}; for (auto *listener : this->connect_state_listeners_) { - listener->on_wifi_connect_state(StringRef(), EMPTY_BSSID); + listener->on_wifi_connect_state(StringRef(), empty_bssid); } } #endif // USE_WIFI_CONNECT_STATE_LISTENERS