diff --git a/esphome/components/esp32_ble_tracker/__init__.py b/esphome/components/esp32_ble_tracker/__init__.py index e916625971..c6e34f37ca 100644 --- a/esphome/components/esp32_ble_tracker/__init__.py +++ b/esphome/components/esp32_ble_tracker/__init__.py @@ -203,7 +203,8 @@ def _raise_defaulted_scan_window(config: ConfigType) -> ConfigType: # A larger value would widen the scan during connections. raise cv.Invalid( f"{CONF_CONNECTION_SCAN_WINDOW} ({connection_window}) needs to be " - f"smaller than the scan window ({params[CONF_WINDOW]})" + f"smaller than the scan window ({params[CONF_WINDOW]})", + path=[CONF_SCAN_PARAMETERS, CONF_CONNECTION_SCAN_WINDOW], ) return config diff --git a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp index ceb0fc8eff..f31d5e6c55 100644 --- a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp +++ b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp @@ -154,7 +154,8 @@ void ESP32BLETracker::loop() { // scan would not restart); !disconnecting matches the restart gate below. if (this->using_connection_window_ && !counts.active && !counts.disconnecting && this->scan_continuous_ && this->scanner_state_ == ScannerState::RUNNING) { - // Same logical scan period continues: no extra on_scan_end for listeners. + // Same logical scan period continues: cleanup_scan_state_ and start_scan_ + // both skip their on_scan_end sweep. this->skip_next_scan_end_ = true; this->stop_scan_(); } @@ -528,14 +529,22 @@ void ESP32BLETracker::cleanup_scan_state_(bool is_stop_complete) { // Reset timeout state machine instead of cancelling scheduler timeout this->scan_timeout_state_ = ScanTimeoutState::INACTIVE; + bool notify_scan_end = true; +#ifdef ESPHOME_ESP32_BLE_TRACKER_CLIENT_COUNT + // Window-change restart continues the scan period: no on_scan_end here. + // The flag stays set so start_scan_ skips its sweep too. + notify_scan_end = !this->skip_next_scan_end_; +#endif + if (notify_scan_end) { #ifdef ESPHOME_ESP32_BLE_TRACKER_LISTENER_COUNT - for (auto *listener : this->listeners_) - listener->on_scan_end(); + for (auto *listener : this->listeners_) + listener->on_scan_end(); #endif #ifdef ESPHOME_BLE_DEVICE_BASE_LISTENER_COUNT - for (auto *listener : this->neutral_listeners_) - listener->on_scan_end(); + for (auto *listener : this->neutral_listeners_) + listener->on_scan_end(); #endif + } this->set_scanner_state_(ScannerState::IDLE); } diff --git a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h index 1ac3211c92..aa7728010c 100644 --- a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h +++ b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h @@ -350,7 +350,7 @@ class ESP32BLETracker final : public Component, /// The running scan was started with connection_scan_window_; lets loop() /// restart the scan at the configured window when the last connection drops. bool using_connection_window_ : 1 {false}; - /// Skip one on_scan_end sweep: the window-change restart continues the period. + /// Suppress the window-change restart's on_scan_end sweeps (stop and start). bool skip_next_scan_end_ : 1 {false}; #endif #ifdef USE_ESP32_BLE_SOFTWARE_COEXISTENCE diff --git a/tests/component_tests/esp32_ble_tracker/config/scan_window_user_set_scan_only.yaml b/tests/component_tests/esp32_ble_tracker/config/scan_window_user_set_scan_only.yaml new file mode 100644 index 0000000000..2b9093164f --- /dev/null +++ b/tests/component_tests/esp32_ble_tracker/config/scan_window_user_set_scan_only.yaml @@ -0,0 +1,14 @@ +esphome: + name: scan-window-user-scan-only + +esp32: + board: esp32dev + framework: + type: esp-idf + +wifi: + ssid: MySSID + +esp32_ble_tracker: + scan_parameters: + connection_scan_window: 20ms diff --git a/tests/component_tests/esp32_ble_tracker/test_scan_window_default.py b/tests/component_tests/esp32_ble_tracker/test_scan_window_default.py index 8a3680d021..bd577248fb 100644 --- a/tests/component_tests/esp32_ble_tracker/test_scan_window_default.py +++ b/tests/component_tests/esp32_ble_tracker/test_scan_window_default.py @@ -222,3 +222,17 @@ def test_codegen_no_connection_window_without_gatt_clients( main_cpp = generate_main(component_config_path("scan_window_scan_only.yaml")) assert "set_scan_window(512)" in main_cpp assert "set_connection_scan_window" not in main_cpp + + +def test_user_set_connection_window_warns_without_gatt_clients( + generate_main: Callable[[str | Path], str], + component_config_path: Callable[[str], Path], + caplog: pytest.LogCaptureFixture, +) -> None: + """A user-set value that cannot take effect warns; the injected default + (previous test) is dropped silently.""" + main_cpp = generate_main( + component_config_path("scan_window_user_set_scan_only.yaml") + ) + assert "set_connection_scan_window" not in main_cpp + assert "'connection_scan_window' has no effect" in caplog.text