Merge remote-tracking branch 'upstream/dev' into remove_posix_tz_parser

This commit is contained in:
J. Nick Koston
2026-03-01 17:02:06 -10:00
152 changed files with 3462 additions and 1964 deletions
@@ -8,7 +8,7 @@ def test_deep_sleep_setup(generate_main):
main_cpp = generate_main("tests/component_tests/deep_sleep/test_deep_sleep1.yaml")
assert "deepsleep = new deep_sleep::DeepSleepComponent();" in main_cpp
assert "App.register_component(deepsleep);" in main_cpp
assert "App.register_component_(deepsleep);" in main_cpp
def test_deep_sleep_sleep_duration(generate_main):
@@ -0,0 +1,10 @@
esphome:
name: test
esp32:
variant: esp32s3
framework:
type: esp-idf
psram:
mode: octal
@@ -0,0 +1,11 @@
esphome:
name: test
esp32:
variant: esp32p4
framework:
type: esp-idf
advanced:
execute_from_psram: true
psram:
@@ -0,0 +1,12 @@
esphome:
name: test
esp32:
variant: esp32s3
framework:
type: esp-idf
advanced:
execute_from_psram: true
psram:
mode: octal
+53 -2
View File
@@ -2,13 +2,17 @@
Test ESP32 configuration
"""
from collections.abc import Callable
from pathlib import Path
from typing import Any
import pytest
from esphome.components.esp32 import VARIANTS
from esphome.components.esp32.const import KEY_ESP32, KEY_SDKCONFIG_OPTIONS
import esphome.config_validation as cv
from esphome.const import CONF_ESPHOME, PlatformFramework
from esphome.core import CORE
from tests.component_tests.types import SetCoreConfigCallable
@@ -70,7 +74,7 @@ def test_esp32_config(
"advanced": {"execute_from_psram": True},
},
},
r"'execute_from_psram' is only supported on ESP32S3 variant @ data\['framework'\]\['advanced'\]\['execute_from_psram'\]",
r"'execute_from_psram' is not available on this esp32 variant @ data\['framework'\]\['advanced'\]\['execute_from_psram'\]",
id="execute_from_psram_invalid_for_variant_config",
),
pytest.param(
@@ -82,7 +86,18 @@ def test_esp32_config(
},
},
r"'execute_from_psram' requires PSRAM to be configured @ data\['framework'\]\['advanced'\]\['execute_from_psram'\]",
id="execute_from_psram_requires_psram_config",
id="execute_from_psram_requires_psram_s3_config",
),
pytest.param(
{
"variant": "esp32p4",
"framework": {
"type": "esp-idf",
"advanced": {"execute_from_psram": True},
},
},
r"'execute_from_psram' requires PSRAM to be configured @ data\['framework'\]\['advanced'\]\['execute_from_psram'\]",
id="execute_from_psram_requires_psram_p4_config",
),
pytest.param(
{
@@ -108,3 +123,39 @@ def test_esp32_configuration_errors(
with pytest.raises(cv.Invalid, match=error_match):
FINAL_VALIDATE_SCHEMA(CONFIG_SCHEMA(config))
def test_execute_from_psram_s3_sdkconfig(
generate_main: Callable[[str | Path], str],
component_config_path: Callable[[str], Path],
) -> None:
"""Test that execute_from_psram on ESP32-S3 sets the correct sdkconfig options."""
generate_main(component_config_path("execute_from_psram_s3.yaml"))
sdkconfig = CORE.data[KEY_ESP32][KEY_SDKCONFIG_OPTIONS]
assert sdkconfig.get("CONFIG_SPIRAM_FETCH_INSTRUCTIONS") is True
assert sdkconfig.get("CONFIG_SPIRAM_RODATA") is True
assert "CONFIG_SPIRAM_XIP_FROM_PSRAM" not in sdkconfig
def test_execute_from_psram_p4_sdkconfig(
generate_main: Callable[[str | Path], str],
component_config_path: Callable[[str], Path],
) -> None:
"""Test that execute_from_psram on ESP32-P4 sets the correct sdkconfig options."""
generate_main(component_config_path("execute_from_psram_p4.yaml"))
sdkconfig = CORE.data[KEY_ESP32][KEY_SDKCONFIG_OPTIONS]
assert sdkconfig.get("CONFIG_SPIRAM_XIP_FROM_PSRAM") is True
assert "CONFIG_SPIRAM_FETCH_INSTRUCTIONS" not in sdkconfig
assert "CONFIG_SPIRAM_RODATA" not in sdkconfig
def test_execute_from_psram_disabled_sdkconfig(
generate_main: Callable[[str | Path], str],
component_config_path: Callable[[str], Path],
) -> None:
"""Test that without execute_from_psram, no XIP sdkconfig options are set."""
generate_main(component_config_path("execute_from_psram_disabled.yaml"))
sdkconfig = CORE.data[KEY_ESP32][KEY_SDKCONFIG_OPTIONS]
assert "CONFIG_SPIRAM_FETCH_INSTRUCTIONS" not in sdkconfig
assert "CONFIG_SPIRAM_RODATA" not in sdkconfig
assert "CONFIG_SPIRAM_XIP_FROM_PSRAM" not in sdkconfig
@@ -27,7 +27,7 @@ def test_web_server_ota_generated(generate_main: Callable[[str], str]) -> None:
assert "global_web_server_base" in main_cpp
# Check component is registered
assert "App.register_component(web_server_webserverotacomponent_id)" in main_cpp
assert "App.register_component_(web_server_webserverotacomponent_id)" in main_cpp
def test_web_server_ota_with_callbacks(generate_main: Callable[[str], str]) -> None:
@@ -10,6 +10,7 @@ esp32:
use_full_certificate_bundle: false # Test CMN bundle (default)
include_builtin_idf_components:
- freertos # Test escape hatch (freertos is always included anyway)
enable_full_printf: false
disable_debug_stubs: true
disable_ocd_aware: true
disable_usb_serial_jtag_secondary: true
@@ -0,0 +1,6 @@
esp32_ble:
io_capability: keyboard_display
# Explicitly not setting some parameters to test ifdef selection
# max_key_size: 16
# min_key_size: 7
auth_req_mode: sc_mitm_bond
@@ -0,0 +1,5 @@
esp32_ble:
io_capability: keyboard_display
max_key_size: 16
min_key_size: 7
auth_req_mode: sc_mitm_bond
@@ -0,0 +1,16 @@
esp32_hosted:
variant: ESP32C6
slot: 1
active_high: true
reset_pin: GPIO15
cmd_pin: GPIO13
clk_pin: GPIO12
d0_pin: GPIO11
d1_pin: GPIO10
d2_pin: GPIO9
d3_pin: GPIO8
sdio_frequency: 8MHz
wifi:
ssid: MySSID
password: password1
@@ -4,7 +4,6 @@ esp32_touch:
measurement_duration: 8ms
low_voltage_reference: 0.5V
high_voltage_reference: 2.7V
voltage_attenuation: 1.5V
binary_sensor:
- platform: esp32_touch
@@ -0,0 +1,5 @@
substitutions:
pin: GPIO5
<<: !include common-variants.yaml
<<: !include common-get-value.yaml
+7
View File
@@ -1,8 +1,15 @@
gps:
latitude:
name: "Latitude"
id: gps_lat
on_value:
then:
- logger.log:
format: "%.6f, %.6f"
args: [id(gps_lat).state, id(gps_long).state]
longitude:
name: "Longitude"
id: gps_long
altitude:
name: "Altitude"
speed:
@@ -16,3 +16,7 @@ button:
switch:
- platform: safe_mode
name: Safe Mode Switch
esphome:
on_boot:
- safe_mode.mark_successful
+37
View File
@@ -0,0 +1,37 @@
sensor:
# Test with explicit type parameter
- platform: sen6x
id: sen6x_sensor
type: SEN69C
i2c_id: i2c_bus
temperature:
name: Temperature
accuracy_decimals: 1
humidity:
name: Humidity
accuracy_decimals: 0
pm_1_0:
name: PM <1µm Weight concentration
id: sen6x_pm_1_0
accuracy_decimals: 1
pm_2_5:
name: PM <2.5µm Weight concentration
id: sen6x_pm_2_5
accuracy_decimals: 1
pm_4_0:
name: PM <4µm Weight concentration
id: sen6x_pm_4_0
accuracy_decimals: 1
pm_10_0:
name: PM <10µm Weight concentration
id: sen6x_pm_10_0
accuracy_decimals: 1
nox:
name: NOx
voc:
name: VOC
co2:
name: Carbon Dioxide
formaldehyde:
name: Formaldehyde
address: 0x6B
@@ -0,0 +1,4 @@
packages:
i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml
<<: !include common.yaml
@@ -0,0 +1,4 @@
packages:
i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml
<<: !include common.yaml
@@ -0,0 +1,4 @@
packages:
i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml
<<: !include common.yaml
@@ -1,9 +1,14 @@
import pytest
from esphome.components import socket
from esphome.const import (
KEY_CORE,
KEY_TARGET_PLATFORM,
PLATFORM_BK72XX,
PLATFORM_ESP32,
PLATFORM_ESP8266,
PLATFORM_LN882X,
PLATFORM_RTL87XX,
)
from esphome.core import CORE
@@ -90,9 +95,15 @@ def test_require_wake_loop_threadsafe__no_networking_does_not_consume_socket() -
assert udp_consumers == initial_udp
def test_require_wake_loop_threadsafe__esp32_no_udp_socket() -> None:
"""Test that ESP32 uses task notifications instead of UDP socket."""
_setup_platform(PLATFORM_ESP32)
@pytest.mark.parametrize(
"platform",
[PLATFORM_ESP32, PLATFORM_BK72XX, PLATFORM_RTL87XX, PLATFORM_LN882X],
)
def test_require_wake_loop_threadsafe__fast_select_no_udp_socket(
platform: str,
) -> None:
"""Test that fast select platforms use task notifications instead of UDP socket."""
_setup_platform(platform)
CORE.config = {"wifi": True}
socket.require_wake_loop_threadsafe()
@@ -100,13 +111,13 @@ def test_require_wake_loop_threadsafe__esp32_no_udp_socket() -> None:
assert CORE.data[socket.KEY_WAKE_LOOP_THREADSAFE_REQUIRED] is True
assert any(d.name == "USE_WAKE_LOOP_THREADSAFE" for d in CORE.defines)
# Verify no UDP socket was consumed (ESP32 uses FreeRTOS task notifications)
# Verify no UDP socket was consumed (fast select platforms use FreeRTOS task notifications)
udp_consumers = CORE.data.get(socket.KEY_SOCKET_CONSUMERS_UDP, {})
assert "socket.wake_loop_threadsafe" not in udp_consumers
def test_require_wake_loop_threadsafe__non_esp32_consumes_udp_socket() -> None:
"""Test that non-ESP32 platforms consume a UDP socket for wake notifications."""
def test_require_wake_loop_threadsafe__non_fast_select_consumes_udp_socket() -> None:
"""Test that platforms without fast select consume a UDP socket for wake notifications."""
_setup_platform(PLATFORM_ESP8266)
CORE.config = {"wifi": True}
socket.require_wake_loop_threadsafe()
@@ -0,0 +1 @@
<<: !include common.yaml
@@ -0,0 +1 @@
<<: !include common.yaml
@@ -0,0 +1 @@
<<: !include common.yaml
+2 -2
View File
@@ -16,10 +16,10 @@ void setup() {
auto *log = new logger::Logger(115200); // NOLINT
log->pre_setup();
log->set_uart_selection(logger::UART_SELECTION_UART0);
App.register_component(log);
App.register_component_(log);
auto *wifi = new wifi::WiFiComponent(); // NOLINT
App.register_component(wifi);
App.register_component_(wifi);
wifi::WiFiAP ap;
ap.set_ssid("Test SSID");
ap.set_password("password1");
+177
View File
@@ -0,0 +1,177 @@
"""Tests for esphome.automation module."""
from collections.abc import Generator
from unittest.mock import patch
import pytest
from esphome.automation import has_non_synchronous_actions
from esphome.util import RegistryEntry
def _make_registry(non_synchronous_actions: set[str]) -> dict[str, RegistryEntry]:
"""Create a mock ACTION_REGISTRY with specified non-synchronous actions.
Uses the default synchronous=False, matching the real registry behavior.
"""
registry: dict[str, RegistryEntry] = {}
for name in non_synchronous_actions:
registry[name] = RegistryEntry(name, lambda: None, None, None)
return registry
@pytest.fixture
def mock_registry() -> Generator[dict[str, RegistryEntry]]:
"""Fixture that patches ACTION_REGISTRY with delay, wait_until, script.wait as non-synchronous."""
registry: dict[str, RegistryEntry] = _make_registry(
{"delay", "wait_until", "script.wait"}
)
registry["logger.log"] = RegistryEntry(
"logger.log", lambda: None, None, None, synchronous=True
)
with patch("esphome.automation.ACTION_REGISTRY", registry):
yield registry
def test_has_non_synchronous_actions_empty_list(
mock_registry: dict[str, RegistryEntry],
) -> None:
assert has_non_synchronous_actions([]) is False
def test_has_non_synchronous_actions_empty_dict(
mock_registry: dict[str, RegistryEntry],
) -> None:
assert has_non_synchronous_actions({}) is False
def test_has_non_synchronous_actions_non_dict_non_list(
mock_registry: dict[str, RegistryEntry],
) -> None:
assert has_non_synchronous_actions("string") is False
assert has_non_synchronous_actions(42) is False
assert has_non_synchronous_actions(None) is False
def test_has_non_synchronous_actions_delay(
mock_registry: dict[str, RegistryEntry],
) -> None:
assert has_non_synchronous_actions([{"delay": "1s"}]) is True
def test_has_non_synchronous_actions_wait_until(
mock_registry: dict[str, RegistryEntry],
) -> None:
assert has_non_synchronous_actions([{"wait_until": {"condition": {}}}]) is True
def test_has_non_synchronous_actions_script_wait(
mock_registry: dict[str, RegistryEntry],
) -> None:
assert has_non_synchronous_actions([{"script.wait": "script_id"}]) is True
def test_has_non_synchronous_actions_synchronous(
mock_registry: dict[str, RegistryEntry],
) -> None:
assert has_non_synchronous_actions([{"logger.log": "hello"}]) is False
def test_has_non_synchronous_actions_unknown_not_in_registry(
mock_registry: dict[str, RegistryEntry],
) -> None:
"""Unknown actions not in registry are not flagged (only registered actions count)."""
assert has_non_synchronous_actions([{"unknown.action": "value"}]) is False
def test_has_non_synchronous_actions_default_non_synchronous(
mock_registry: dict[str, RegistryEntry],
) -> None:
"""Actions registered without explicit synchronous=True default to non-synchronous."""
mock_registry["some.action"] = RegistryEntry(
"some.action", lambda: None, None, None
)
assert has_non_synchronous_actions([{"some.action": "value"}]) is True
def test_has_non_synchronous_actions_nested_in_then(
mock_registry: dict[str, RegistryEntry],
) -> None:
"""Non-synchronous action nested inside a synchronous action's then block."""
actions: list[dict[str, object]] = [
{
"logger.log": "first",
"then": [{"delay": "1s"}],
}
]
assert has_non_synchronous_actions(actions) is True
def test_has_non_synchronous_actions_deeply_nested(
mock_registry: dict[str, RegistryEntry],
) -> None:
"""Non-synchronous action deeply nested in action structure."""
actions: list[dict[str, object]] = [
{
"if": {
"then": [
{"logger.log": "hello"},
{"delay": "500ms"},
]
}
}
]
assert has_non_synchronous_actions(actions) is True
def test_has_non_synchronous_actions_none_in_nested(
mock_registry: dict[str, RegistryEntry],
) -> None:
"""No non-synchronous actions even with nesting."""
actions: list[dict[str, object]] = [
{
"if": {
"then": [
{"logger.log": "hello"},
]
}
}
]
assert has_non_synchronous_actions(actions) is False
def test_has_non_synchronous_actions_multiple_one_non_synchronous(
mock_registry: dict[str, RegistryEntry],
) -> None:
assert (
has_non_synchronous_actions(
[
{"logger.log": "first"},
{"delay": "1s"},
{"logger.log": "second"},
]
)
is True
)
def test_has_non_synchronous_actions_multiple_all_synchronous(
mock_registry: dict[str, RegistryEntry],
) -> None:
assert (
has_non_synchronous_actions(
[
{"logger.log": "first"},
{"logger.log": "second"},
]
)
is False
)
def test_has_non_synchronous_actions_dict_input(
mock_registry: dict[str, RegistryEntry],
) -> None:
"""Direct dict input (single action)."""
assert has_non_synchronous_actions({"delay": "1s"}) is True
assert has_non_synchronous_actions({"logger.log": "hello"}) is False
+4 -4
View File
@@ -16,7 +16,7 @@ async def test_gpio_pin_expression__conf_is_none(monkeypatch):
async def test_register_component(monkeypatch):
var = Mock(base="foo.bar")
app_mock = Mock(register_component=Mock(return_value=var))
app_mock = Mock(register_component_=Mock(return_value=var))
monkeypatch.setattr(ch, "App", app_mock)
core_mock = Mock(component_ids=["foo.bar"])
@@ -29,7 +29,7 @@ async def test_register_component(monkeypatch):
assert actual is var
assert add_mock.call_count == 2
app_mock.register_component.assert_called_with(var)
app_mock.register_component_.assert_called_with(var)
assert core_mock.component_ids == []
@@ -48,7 +48,7 @@ async def test_register_component__no_component_id(monkeypatch):
async def test_register_component__with_setup_priority(monkeypatch):
var = Mock(base="foo.bar")
app_mock = Mock(register_component=Mock(return_value=var))
app_mock = Mock(register_component_=Mock(return_value=var))
monkeypatch.setattr(ch, "App", app_mock)
core_mock = Mock(component_ids=["foo.bar"])
@@ -68,5 +68,5 @@ async def test_register_component__with_setup_priority(monkeypatch):
assert actual is var
add_mock.assert_called()
assert add_mock.call_count == 4
app_mock.register_component.assert_called_with(var)
app_mock.register_component_.assert_called_with(var)
assert core_mock.component_ids == []