Merge pull request #20680 from esphome/bump-2026.10.0b4

2026.10.0b4
This commit is contained in:
Jesse Hills
2026-10-14 11:14:49 +13:00
committed by GitHub
19 changed files with 252 additions and 18 deletions
+1 -1
View File
@@ -48,7 +48,7 @@ PROJECT_NAME = ESPHome
# could be handy for archiving the generated documentation or if some version
# control system is used.
PROJECT_NUMBER = 2026.10.0b3
PROJECT_NUMBER = 2026.10.0b4
# Using the PROJECT_BRIEF tag one can provide an optional one line description
# for a project that appears at the top of each page and should give viewer a
+1 -1
View File
@@ -391,7 +391,7 @@ async def to_code(config: ConfigType) -> None:
# Adds a define and IDF component for legacy `audio_decoder.cpp`.
if data.flac_support:
cg.add_define("USE_AUDIO_FLAC_SUPPORT")
add_idf_component(name="esphome/micro-flac", ref="0.2.0")
add_idf_component(name="esphome/micro-flac", ref="0.3.0")
_emit_memory_pair(
data.flac.buffer_memory,
"CONFIG_MICRO_FLAC_PREFER_PSRAM",
+4 -2
View File
@@ -1039,11 +1039,13 @@ def run_compile(args, config: ConfigType) -> bool:
framework_ver = CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION]
bootloader = zephyr_data()[KEY_BOOTLOADER]
# (dev_type, sd_req) per bootloader — values from Nordic SoftDevice release notes
# (dev_type, sd_req) per bootloader — values from Nordic SoftDevice release notes.
# The bootloader resets on a package whose sd_req does not list its SoftDevice,
# so v7 accepts S140 7.0.1, 7.2.0 and 7.3.0 (e.g. Seeed XIAO nRF52840).
_GENPKG_PARAMS = {
BOOTLOADER_ADAFRUIT_NRF52_SD132: ("0x0051", "0x009D"),
BOOTLOADER_ADAFRUIT_NRF52_SD140_V6: ("0x0052", "0x00B6"),
BOOTLOADER_ADAFRUIT_NRF52_SD140_V7: ("0x0052", "0x00CA"),
BOOTLOADER_ADAFRUIT_NRF52_SD140_V7: ("0x0052", "0x00CA,0x0100,0x0123"),
}
# UF2 family IDs — nRF52832 vs nRF52840 per SoftDevice variant
_UF2_FAMILY_IDS = {
+2
View File
@@ -194,6 +194,8 @@ def _parse_platform_version(value: Any) -> str:
# * The new version needs to be thoroughly validated before changing the
# recommended version as otherwise a bunch of devices could be bricked
# * For all constants below, update platformio.ini (in this repo)
# * rp2_pio pins its own pioasm build (PIOASM_REPO_VERSION in rp2_pio/__init__.py); check
# https://github.com/earlephilhower/pico-quick-toolchain/releases for a matching release
# The default/recommended arduino framework version
# - https://github.com/earlephilhower/arduino-pico/releases
+4 -4
View File
@@ -7,9 +7,9 @@ from esphome.types import ConfigType
DEPENDENCIES = ["rp2"]
PIOASM_REPO_VERSION = "1.5.0-b"
PIOASM_REPO_VERSION = "4.1.0"
PIOASM_REPO_BASE = f"https://github.com/earlephilhower/pico-quick-toolchain/releases/download/{PIOASM_REPO_VERSION}"
PIOASM_VERSION = "pioasm-2e6142b.230216"
PIOASM_VERSION = "pioasm-9fdfe11.250530"
PIOASM_DOWNLOADS = {
"linux": {
"aarch64": f"aarch64-linux-gnu.{PIOASM_VERSION}.tar.gz",
@@ -20,8 +20,8 @@ PIOASM_DOWNLOADS = {
"amd64": f"x86_64-w64-mingw32.{PIOASM_VERSION}.zip",
},
"darwin": {
"x86_64": f"x86_64-apple-darwin14.{PIOASM_VERSION}.tar.gz",
"arm64": f"x86_64-apple-darwin14.{PIOASM_VERSION}.tar.gz",
"x86_64": f"x86_64-apple-darwin20.4.{PIOASM_VERSION}.tar.gz",
"arm64": f"aarch64-apple-darwin20.4.{PIOASM_VERSION}.tar.gz",
},
}
+7 -3
View File
@@ -295,8 +295,11 @@ template<class C, typename... Ts> class ScriptWaitAction final : public Action<T
}
void loop() override {
if (this->num_running_ == 0)
if (this->num_running_ == 0) {
// Nothing left to wait for, e.g. once the last queued item has run
this->disable_loop();
return;
}
if (this->script_->is_running())
return;
@@ -305,9 +308,10 @@ template<class C, typename... Ts> class ScriptWaitAction final : public Action<T
// Processing all items in a while loop causes infinite loops because
// play_next_() can trigger more items to be queued
if (!this->param_queue_.empty()) {
auto &params = this->param_queue_.front();
std::apply([this](auto &...args) { this->play_next_(args...); }, params);
// Take the item out first: the chain may call stop(), which clears the queue
auto params = std::move(this->param_queue_.front());
this->param_queue_.pop_front();
std::apply([this](auto &...args) { this->play_next_(args...); }, params);
} else {
// Queue is now empty - disable loop until next play_complex
this->disable_loop();
+7
View File
@@ -344,6 +344,12 @@ def validate_variant(_):
)
def _validate_min_auth_mode(value: str) -> str:
if value == "WPA3" and CORE.is_esp8266:
raise cv.Invalid("ESP8266 does not support WPA3; use WPA or WPA2 instead")
return value
def _apply_min_auth_mode_default(config):
"""Apply platform-specific default for min_auth_mode."""
if CONF_MIN_AUTH_MODE not in config and (CORE.is_esp8266 or CORE.is_esp32):
@@ -522,6 +528,7 @@ CONFIG_SCHEMA = cv.All(
cv.Optional(CONF_MIN_AUTH_MODE): cv.All(
VALIDATE_WIFI_MIN_AUTH_MODE,
cv.only_on([Platform.ESP32, Platform.ESP8266]),
_validate_min_auth_mode,
),
cv.SplitDefault(CONF_OUTPUT_POWER, esp8266=20.0): cv.All(
cv.decibel, cv.float_range(min=8.5, max=20.5)
+1 -1
View File
@@ -4,7 +4,7 @@ from enum import Enum
from esphome.enum import StrEnum
__version__ = "2026.10.0b3"
__version__ = "2026.10.0b4"
ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_"
VALID_SUBSTITUTIONS_CHARACTERS = (
+1 -1
View File
@@ -10,7 +10,7 @@ dependencies:
esphome/micro-decoder:
version: 0.4.0
esphome/micro-flac:
version: 0.2.0
version: 0.3.0
esphome/micro-mp3:
version: 0.4.0
esphome/micro-opus:
+3 -4
View File
@@ -620,12 +620,11 @@ def _make_registry_client() -> Any:
elsewhere, not by the PlatformIO registry.
"""
from platformio.package.manager._registry import PackageManagerRegistryMixin
from platformio.project.helpers import get_project_cache_dir
from platformio.registry.client import RegistryClient
# PlatformIO creates its HTTP cache dir without exist_ok, so two builds
# making their first registry lookup at once race on it
(Path(get_project_cache_dir()) / "http").mkdir(parents=True, exist_ok=True)
from esphome.platformio.runner import ensure_http_cache_dir
ensure_http_cache_dir()
class _Registry(PackageManagerRegistryMixin):
def __init__(self) -> None:
+4
View File
@@ -841,6 +841,10 @@ def _prefetch(build_dir: Path, env: str) -> None:
from platformio.package.meta import PackageCompatibility, PackageSpec
from platformio.platform.factory import PlatformFactory
from esphome.platformio.runner import ensure_http_cache_dir
# The resolve and install pools each build their own registry client
ensure_http_cache_dir()
platform_spec, config = _project_platform_and_config(
build_dir / "platformio.ini", env
)
+11
View File
@@ -106,6 +106,17 @@ def patch_file_downloader() -> None:
FileDownloader.__init__ = patched_init
def ensure_http_cache_dir() -> None:
"""Create PlatformIO's HTTP cache dir before anything resolves a package.
PlatformIO creates it without exist_ok on the first registry lookup, so
concurrent lookups (the prefetch's resolve threads, or two builds) race.
"""
from platformio.project.helpers import get_project_cache_dir
(Path(get_project_cache_dir()) / "http").mkdir(parents=True, exist_ok=True)
def patch_registry_private_packages() -> None:
"""Skip PlatformIO's private-package probe; it sleeps ~500 ms per lookup.
+1 -1
View File
@@ -12,7 +12,7 @@ pyserial==3.5
platformio==6.2.0
esptool==5.4.0
click==8.5.0
aioesphomeapi==46.9.0
aioesphomeapi==46.10.0
aiohappyeyeballs==2.7.1 # Happy Eyeballs for requests downloads; already pulled in by aioesphomeapi
zeroconf==0.151.5
puremagic==2.2.0
@@ -0,0 +1,79 @@
"""Platform restrictions for minimum Wi-Fi authentication modes."""
import pytest
from esphome import config_validation as cv
from esphome.components.esp32 import KEY_VARIANT
from esphome.components.esp32.const import VARIANT_ESP32
from esphome.components.wifi import CONF_MIN_AUTH_MODE, CONFIG_SCHEMA
from esphome.const import PlatformFramework
from tests.component_tests.types import SetCoreConfigCallable
def _validate(mode: str | None = None) -> dict:
config = {"ssid": "net", "password": "secret12", "use_address": "test.local"}
if mode is not None:
config[CONF_MIN_AUTH_MODE] = mode
return CONFIG_SCHEMA(config)
@pytest.mark.parametrize("mode", ["WPA3", "wpa3"])
def test_esp8266_rejects_wpa3(
set_core_config: SetCoreConfigCallable, mode: str
) -> None:
set_core_config(PlatformFramework.ESP8266_ARDUINO)
with pytest.raises(cv.Invalid, match="ESP8266 does not support WPA3") as exc:
_validate(mode)
assert exc.value.path == [CONF_MIN_AUTH_MODE]
@pytest.mark.parametrize(
("platform", "mode"),
[
(PlatformFramework.ESP8266_ARDUINO, "WPA"),
(PlatformFramework.ESP8266_ARDUINO, "WPA2"),
(PlatformFramework.ESP32_IDF, "WPA"),
(PlatformFramework.ESP32_IDF, "WPA2"),
(PlatformFramework.ESP32_IDF, "WPA3"),
(PlatformFramework.ESP32_IDF, "wpa3"),
(PlatformFramework.ESP32_ARDUINO, "WPA"),
(PlatformFramework.ESP32_ARDUINO, "WPA2"),
(PlatformFramework.ESP32_ARDUINO, "WPA3"),
],
)
def test_supported_modes(
set_core_config: SetCoreConfigCallable, platform: PlatformFramework, mode: str
) -> None:
set_core_config(
platform,
platform_data=(
{KEY_VARIANT: VARIANT_ESP32}
if platform
in (PlatformFramework.ESP32_IDF, PlatformFramework.ESP32_ARDUINO)
else None
),
)
assert _validate(mode)[CONF_MIN_AUTH_MODE] == mode.upper()
@pytest.mark.parametrize(
"platform",
[
PlatformFramework.ESP8266_ARDUINO,
PlatformFramework.ESP32_IDF,
PlatformFramework.ESP32_ARDUINO,
],
)
def test_default_remains_wpa2(
set_core_config: SetCoreConfigCallable, platform: PlatformFramework
) -> None:
set_core_config(
platform,
platform_data=(
{KEY_VARIANT: VARIANT_ESP32}
if platform
in (PlatformFramework.ESP32_IDF, PlatformFramework.ESP32_ARDUINO)
else None
),
)
assert _validate()[CONF_MIN_AUTH_MODE] == "WPA2"
@@ -0,0 +1,2 @@
packages:
rp2040_pio_led_strip: !include common.yaml
@@ -2,6 +2,7 @@
#include <memory>
#include <string>
#include <vector>
#include "esphome/components/script/script.h"
@@ -93,4 +94,89 @@ TEST(ScriptWaitActionTest, KeepsReferenceToNonCopyableArguments) {
EXPECT_EQ(entity.value, 1);
}
class StopWaitAction : public Action<const std::string &> {
public:
void play(const std::string &value) override {
this->wait->stop_complex();
this->seen = value;
}
Action<const std::string &> *wait{nullptr};
std::string seen;
};
// The chain after the wait may stop it, e.g. a script with mode: restart
TEST(ScriptWaitActionTest, StopDuringChainKeepsArguments) {
FakeScript script;
ScriptWaitAction<FakeScript, const std::string &> wait(&script);
StopWaitAction stop;
stop.wait = &wait;
ActionList<const std::string &> actions;
actions.add_actions({&wait, &stop});
// With a second item waiting, stop() clears the queue while the first item's chain runs
actions.play(std::string("argument longer than the small string buffer"));
actions.play(std::string("second argument, also longer than the buffer"));
script.running = false;
wait.loop();
EXPECT_EQ(stop.seen, "argument longer than the small string buffer");
wait.loop();
EXPECT_EQ(stop.seen, "argument longer than the small string buffer");
}
class RestartAction : public Action<const std::string &> {
public:
void play(const std::string &value) override {
this->seen.push_back(value);
if (this->seen.size() > 1)
return;
// What a script with mode: restart does: stop, then run again while the awaited script is busy
this->wait->stop_complex();
this->script->running = true;
this->actions->play(std::string("restarted, longer than the small string buffer"));
}
Action<const std::string &> *wait{nullptr};
FakeScript *script{nullptr};
ActionList<const std::string &> *actions{nullptr};
std::vector<std::string> seen;
};
// The old pop_front() after the chain removed the item queued by the restart
TEST(ScriptWaitActionTest, RestartDuringChainKeepsNewItem) {
FakeScript script;
ScriptWaitAction<FakeScript, const std::string &> wait(&script);
RestartAction restart;
ActionList<const std::string &> actions;
actions.add_actions({&wait, &restart});
restart.wait = &wait;
restart.script = &script;
restart.actions = &actions;
// With a second item waiting, the stop really clears the queue; the restart then queues a new item
actions.play(std::string("first argument, longer than the small string buffer"));
actions.play(std::string("second argument, dropped by the stop"));
script.running = false;
wait.loop();
ASSERT_EQ(restart.seen.size(), 1u);
script.running = false;
wait.loop();
ASSERT_EQ(restart.seen.size(), 2u);
EXPECT_EQ(restart.seen[1], "restarted, longer than the small string buffer");
}
TEST(ScriptWaitActionTest, LoopDisabledOnceNothingWaits) {
FakeScript script;
ScriptWaitAction<FakeScript, const std::string &> wait(&script);
RecordAction record;
ActionList<const std::string &> actions;
actions.add_actions({&wait, &record});
actions.play(std::string("argument"));
script.running = false;
wait.loop();
EXPECT_EQ(record.seen, "argument");
wait.loop();
EXPECT_TRUE(wait.is_idle());
}
} // namespace esphome::script::testing
@@ -128,3 +128,13 @@ def test_stale_merged_hex_is_removed_before_building(
app_hex = str(build_dir / "zephyr" / "zephyr.hex")
assert _uf2_hex(run_cmd) == app_hex
assert _dfu_hex(run_cmd) == app_hex
def test_sd140_v7_dfu_accepts_all_s140_7_softdevices(
build_dir: Path, run_cmd: Mock
) -> None:
"""Boards such as the Seeed XIAO nRF52840 ship S140 7.3.0, not 7.0.1."""
_build(build_dir, cv.Version(3, 4, 0), ["zephyr/zephyr/zephyr.hex"], run_cmd)
cmd = next(c.args[0] for c in run_cmd.call_args_list if "genpkg" in c.args[0])
sd_req = cmd[cmd.index("--sd-req") + 1].split(",")
assert sd_req == ["0x00CA", "0x0100", "0x0123"]
@@ -561,6 +561,19 @@ def test_registry_lock_deadline_skips_registration(tmp_path: Path) -> None:
manager.set_download_utime.assert_not_called()
def test_prefetch_creates_http_cache_dir_first(
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
) -> None:
"""Eight resolve threads each build a registry client whose cache
init has no exist_ok; the dir must exist before any of them run."""
from platformio.project import helpers
monkeypatch.setattr(helpers, "get_project_cache_dir", lambda: str(tmp_path))
with patch.object(pf, "_project_platform_and_config", return_value=(None, None)):
pf._prefetch(tmp_path, "testenv")
assert (tmp_path / "http").is_dir()
def test_main_interrupt_exits_quietly(tmp_path: Path) -> None:
"""Ctrl-C reaches the child via the shared process group; it must exit
without a traceback."""
@@ -4,6 +4,7 @@ from __future__ import annotations
from collections.abc import Callable
import io
from pathlib import Path
import sys
from types import ModuleType
from unittest.mock import Mock
@@ -110,6 +111,20 @@ def test_main_applies_registry_private_packages_patch(
assert order == ["patch", "pio"]
def test_ensure_http_cache_dir_is_idempotent(
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
) -> None:
"""The dir exists before PlatformIO's non-exist_ok makedirs, and a
second caller (another thread or build) does not trip over it."""
from platformio.project import helpers
monkeypatch.setattr(helpers, "get_project_cache_dir", lambda: str(tmp_path / "c"))
runner.ensure_http_cache_dir()
assert (tmp_path / "c" / "http").is_dir()
runner.ensure_http_cache_dir()
# Snapshot PlatformIO's own probe at import, before any test can patch it
_PIO_PROBE = RegistryClient.__dict__["allowed_private_packages"]