mirror of
https://github.com/esphome/esphome.git
synced 2026-10-14 18:22:48 +00:00
Compare commits
9
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9d530f5c56 | ||
|
|
5a78f636b9 | ||
|
|
53d504137b | ||
|
|
bb0546fc0b | ||
|
|
b43630cf8e | ||
|
|
c9664d4e42 | ||
|
|
39ea6a3220 | ||
|
|
f4a6d6f18b | ||
|
|
6c24d4aa43 |
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -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 ¶ms = 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();
|
||||
|
||||
@@ -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
@@ -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 = (
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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
@@ -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"]
|
||||
|
||||
|
||||
Reference in New Issue
Block a user