[esp32_ble] Reference count BLE advertising

Advertising was started when the BLE server came up and never stopped, so a
device that auto-loads esp32_ble_server for esp32_improv advertised over BLE
forever - even with WiFi connected and Improv never started.

Components now request advertising with advertising_start() and release it
with advertising_stop(); the radio only advertises while at least one request
is held. The BLE server only requests it when the configuration defines
services or manufacturer data of its own, Improv holds a request for as long
as provisioning is possible, and a beacon holds one permanently.

Fixes https://github.com/esphome/esphome/issues/18938
This commit is contained in:
Jesse Hills
2026-09-02 20:39:07 +12:00
parent b8480b8424
commit 3fa9cd5abf
12 changed files with 166 additions and 9 deletions
@@ -0,0 +1,13 @@
esphome:
name: test
esp32:
variant: esp32
wifi:
ssid: MySSID
password: password1
# esp32_ble_server is only auto-loaded here, so it has no services of its own.
esp32_improv:
authorizer: none
@@ -0,0 +1,9 @@
esphome:
name: test
esp32:
variant: esp32
esp32_ble_server:
id: ble_server
manufacturer_data: [0x72, 0x04, 0x00, 0x23]
@@ -0,0 +1,14 @@
esphome:
name: test
esp32:
variant: esp32
esp32_ble_server:
id: ble_server
services:
- uuid: 2a24b789-7aab-4535-af3e-ee76a35cc12d
characteristics:
- uuid: cad48e28-7fbe-41cf-bae9-d77a6c233423
read: true
value: [1, 2, 3, 4]
@@ -1,5 +1,10 @@
"""Tests for esp32_ble_server configuration helpers."""
from __future__ import annotations
from collections.abc import Callable
from pathlib import Path
import pytest
from esphome.components.esp32_ble_server import (
@@ -45,3 +50,26 @@ def test_uuid_is_matches_descriptor_short_strings(uuid16) -> None:
assert uuid_is(uuid16, uuid16)
assert uuid_is(f"{uuid16:04X}", uuid16)
assert uuid_is(f"{uuid16:08X}", uuid16)
@pytest.mark.parametrize(
("config_file", "required"),
[
# Auto-loaded by esp32_improv only: nothing to find until Improv asks for it
("improv_only.yaml", False),
# The configuration defines a service clients are meant to connect to
("own_service.yaml", True),
# Manufacturer data is only useful if it is actually broadcast
("manufacturer_data_only.yaml", True),
],
)
def test_advertising_required(
generate_main: Callable[[str | Path], str],
component_config_path: Callable[[str], Path],
config_file: str,
required: bool,
) -> None:
"""The server only requests advertising when the configuration needs it."""
main_cpp = generate_main(component_config_path(config_file))
assert f"set_advertising_required({str(required).lower()})" in main_cpp