[bk72xx_ble] Keep wifi power save off while BLE is compiled in (#19317)

This commit is contained in:
Edvard Filistovič
2026-09-15 08:15:06 -05:00
committed by GitHub
parent e737dc8dac
commit e163ae5299
6 changed files with 128 additions and 2 deletions
+10 -1
View File
@@ -23,7 +23,7 @@ public ble_api.h.
import logging
import esphome.codegen as cg
from esphome.components import libretiny
from esphome.components import libretiny, wifi
from esphome.components.libretiny.const import (
FAMILY_BK7231N,
FAMILY_BK7231Q,
@@ -84,6 +84,15 @@ def _final_validate(config: ConfigType) -> None:
# which run on a BLE 4.2 board. The hard error is raised at codegen.
if msg := _unsupported_family_message(libretiny.get_libretiny_family()):
_LOGGER.warning("%s (this configuration cannot compile)", msg)
# Any wifi power_save_mode other than NONE also arms the Beken SDK's MCU
# sleep. With the BLE controller running, that sleep never wakes up once the
# station is stopped (adapter restart after failed roams, wifi.disable): the
# device is dead until a power cycle (esphome#18592). Keep power save off
# until LibreTiny ships the SDK-side fix (libretiny-eu/libretiny#414).
wifi.force_power_save_off(
"with BLE running, the Beken SDK's MCU sleep halts the device once the "
"station is stopped (https://github.com/esphome/esphome/issues/18592)"
)
FINAL_VALIDATE_SCHEMA = _final_validate
+31 -1
View File
@@ -690,7 +690,16 @@ async def to_code(config):
):
cg.add(var.set_reboot_timeout(reboot_timeout))
if (power_save_mode := config[CONF_POWER_SAVE_MODE]) != "NONE":
cg.add(var.set_power_save_mode(power_save_mode))
if reasons := CORE.data.get(POWER_SAVE_OFF_REASONS_KEY):
_LOGGER.warning(
"power_save_mode %s is not applied: %s",
power_save_mode,
"; ".join(reasons),
)
else:
cg.add(var.set_power_save_mode(power_save_mode))
# From here on force_power_save_off() can no longer take effect
CORE.data[POWER_SAVE_APPLIED_KEY] = True
if (
min_auth_mode := config.get(CONF_MIN_AUTH_MODE)
) is not None and min_auth_mode != "WPA2":
@@ -864,6 +873,8 @@ async def wifi_roam_to_code(
KEEP_SCAN_RESULTS_KEY = "wifi_keep_scan_results"
RUNTIME_POWER_SAVE_KEY = "wifi_runtime_power_save"
POWER_SAVE_OFF_REASONS_KEY = "wifi_power_save_off_reasons"
POWER_SAVE_APPLIED_KEY = "wifi_power_save_applied"
RUNTIME_ROAMING_SUPPRESSION_KEY = "wifi_runtime_roaming_suppression"
# Keys for listener counts
IP_STATE_LISTENERS_KEY = "wifi_ip_state_listeners"
@@ -896,6 +907,25 @@ def request_wifi_scan_results_lock() -> None:
CORE.data[SCAN_RESULTS_LOCK_KEY] = True
def force_power_save_off(reason: str) -> None:
"""Keep the station out of WiFi power save regardless of power_save_mode.
Components whose platform cannot run power save safely call this from their
final validation (FINAL_VALIDATE_SCHEMA), which always runs before any code
generation. Every distinct reason is kept; when the configured mode is not
NONE, wifi's code generation logs them and skips the mode. Calling it once
wifi has generated its code is too late and raises.
"""
if POWER_SAVE_APPLIED_KEY in CORE.data:
raise EsphomeError(
"wifi.force_power_save_off() must be called from final validation, "
"before wifi generates its code"
)
reasons: list[str] = CORE.data.setdefault(POWER_SAVE_OFF_REASONS_KEY, [])
if reason not in reasons:
reasons.append(reason)
def enable_runtime_power_save_control():
"""Enable runtime WiFi power save control.
@@ -0,0 +1,12 @@
esphome:
name: bk-power-save
bk72xx:
board: cb2s
wifi:
ssid: test
password: testtest
power_save_mode: high
bk72xx_ble:
@@ -0,0 +1,20 @@
"""bk72xx_ble keeps WiFi power save off: the Beken SDK's MCU sleep does not
wake up once the station is stopped while the BLE controller runs."""
from collections.abc import Callable
from pathlib import Path
import pytest
def test_power_save_mode_is_not_applied_with_ble(
generate_main: Callable[[str | Path], str],
component_config_path: Callable[[str], Path],
caplog: pytest.LogCaptureFixture,
) -> None:
main_cpp = generate_main(component_config_path("test_power_save.yaml"))
assert "bk72xx_ble::BK72xxBLE" in main_cpp
assert "set_power_save_mode(" not in main_cpp
assert "power_save_mode HIGH is not applied" in caplog.text
assert "issues/18592" in caplog.text
@@ -0,0 +1,46 @@
"""Tests for wifi.force_power_save_off(), the hook platforms use to keep the
station out of power save."""
from collections.abc import Callable
from pathlib import Path
import pytest
from esphome.components import wifi
from esphome.core import CORE, EsphomeError
def test_reasons_accumulate_without_duplicates() -> None:
"""Every caller's reason is kept once; a repeated reason is not duplicated."""
wifi.force_power_save_off("first")
wifi.force_power_save_off("first")
wifi.force_power_save_off("second")
assert CORE.data[wifi.POWER_SAVE_OFF_REASONS_KEY] == ["first", "second"]
def test_forced_off_skips_the_setter_and_warns(
generate_main: Callable[[str | Path], str],
component_config_path: Callable[[str], Path],
caplog: pytest.LogCaptureFixture,
) -> None:
"""With a reason recorded, power_save_mode is reported and not applied."""
wifi.force_power_save_off("the platform cannot sleep")
main_cpp = generate_main(component_config_path("custom.yaml"))
assert "set_power_save_mode(" not in main_cpp
assert (
"power_save_mode LIGHT is not applied: the platform cannot sleep" in caplog.text
)
def test_call_after_wifi_codegen_raises(
generate_main: Callable[[str | Path], str],
component_config_path: Callable[[str], Path],
) -> None:
"""Once wifi has generated its code the hook cannot take effect any more."""
generate_main(component_config_path("custom.yaml"))
with pytest.raises(EsphomeError, match="before wifi generates its code"):
wifi.force_power_save_off("too late")
@@ -0,0 +1,9 @@
# A wifi power_save_mode other than NONE is forced off with a warning while
# bk72xx_ble is configured (esphome#18592); this config must still validate.
packages:
bk72xx_ble: !include common.yaml
wifi:
ssid: MySSID
password: password1
power_save_mode: high