Merge branch 'dev' into enhancement/ethernet-spi-interface

This commit is contained in:
Ross Tyler
2026-01-28 19:15:42 -08:00
committed by GitHub
2589 changed files with 83152 additions and 29891 deletions
@@ -29,7 +29,7 @@ def test_binary_sensor_sets_mandatory_fields(generate_main):
)
# Then
assert 'bs_1->set_name("test bs1");' in main_cpp
assert 'bs_1->set_name("test bs1",' in main_cpp
assert "bs_1->set_pin(" in main_cpp
+1 -1
View File
@@ -26,7 +26,7 @@ def test_button_sets_mandatory_fields(generate_main):
main_cpp = generate_main("tests/component_tests/button/test_button.yaml")
# Then
assert 'wol_1->set_name("wol_test_1");' in main_cpp
assert 'wol_1->set_name("wol_test_1",' in main_cpp
assert "wol_2->set_macaddr(18, 52, 86, 120, 144, 171);" in main_cpp
@@ -0,0 +1,291 @@
"""Tests for epaper_spi configuration validation."""
from collections.abc import Callable
from typing import Any
import pytest
from esphome import config_validation as cv
from esphome.components.epaper_spi.display import (
CONFIG_SCHEMA,
FINAL_VALIDATE_SCHEMA,
MODELS,
)
from esphome.components.esp32 import (
KEY_BOARD,
KEY_VARIANT,
VARIANT_ESP32,
VARIANT_ESP32S3,
)
from esphome.const import (
CONF_BUSY_PIN,
CONF_CS_PIN,
CONF_DC_PIN,
CONF_DIMENSIONS,
CONF_HEIGHT,
CONF_INIT_SEQUENCE,
CONF_RESET_PIN,
CONF_WIDTH,
PlatformFramework,
)
from esphome.types import ConfigType
from tests.component_tests.types import SetCoreConfigCallable
def run_schema_validation(
config: ConfigType, with_final_validate: bool = False
) -> None:
"""Run schema validation on a configuration.
Args:
config: The configuration to validate
with_final_validate: If True, also run final validation (requires full config setup)
"""
result = CONFIG_SCHEMA(config)
if with_final_validate:
FINAL_VALIDATE_SCHEMA(result)
return result
@pytest.mark.parametrize(
("config", "error_match"),
[
pytest.param(
"a string",
"expected a dictionary",
id="invalid_string_config",
),
pytest.param(
{"id": "display_id"},
r"required key not provided @ data\['model'\]",
id="missing_model",
),
pytest.param(
{
"id": "display_id",
"model": "ssd1677",
"dimensions": {"width": 200, "height": 200},
},
r"required key not provided @ data\['dc_pin'\]",
id="missing_dc_pin",
),
],
)
def test_basic_configuration_errors(
config: str | ConfigType,
error_match: str,
set_core_config: SetCoreConfigCallable,
) -> None:
"""Test basic configuration validation errors"""
set_core_config(
PlatformFramework.ESP32_IDF,
platform_data={KEY_BOARD: "esp32dev", KEY_VARIANT: VARIANT_ESP32},
)
with pytest.raises(cv.Invalid, match=error_match):
CONFIG_SCHEMA(config)
def test_all_predefined_models(
set_core_config: SetCoreConfigCallable,
set_component_config: Callable[[str, Any], None],
) -> None:
"""Test all predefined epaper models validate successfully with appropriate defaults."""
# Test all models, providing default values where necessary
for name, model in MODELS.items():
# SEEED models are designed for ESP32-S3 hardware
if name in ("SEEED-EE04-MONO-4.26", "SEEED-RETERMINAL-E1002"):
set_core_config(
PlatformFramework.ESP32_IDF,
platform_data={
KEY_BOARD: "esp32-s3-devkitc-1",
KEY_VARIANT: VARIANT_ESP32S3,
},
)
else:
set_core_config(
PlatformFramework.ESP32_IDF,
platform_data={KEY_BOARD: "esp32dev", KEY_VARIANT: VARIANT_ESP32},
)
# Configure SPI component which is required by epaper_spi
set_component_config("spi", {"id": "spi_bus", "clk_pin": 18, "mosi_pin": 19})
config = {"model": name}
# Add ID field
config["id"] = "test_display"
# Add required fields that don't have defaults
# Use safe GPIO pins that work on ESP32 (avoiding flash pins 6-11)
if not model.get_default(CONF_DC_PIN):
config[CONF_DC_PIN] = 21
# Add dimensions if not provided by model
if not model.get_default(CONF_WIDTH):
config[CONF_DIMENSIONS] = {CONF_HEIGHT: 240, CONF_WIDTH: 320}
# Add init sequence if model doesn't provide one
if model.initsequence is None:
config[CONF_INIT_SEQUENCE] = [[0xA0, 0x01]]
# Add other optional pins that some models might require
if not model.get_default(CONF_BUSY_PIN):
config[CONF_BUSY_PIN] = 22
if not model.get_default(CONF_RESET_PIN):
config[CONF_RESET_PIN] = 23
if not model.get_default(CONF_CS_PIN):
config[CONF_CS_PIN] = 5
run_schema_validation(config)
@pytest.mark.parametrize(
"model_name",
[pytest.param(name, id=name.lower()) for name in sorted(MODELS.keys())],
)
def test_individual_models(
model_name: str,
set_core_config: SetCoreConfigCallable,
set_component_config: Callable[[str, Any], None],
) -> None:
"""Test each epaper model individually to ensure it validates correctly."""
# SEEED models are designed for ESP32-S3 hardware
if model_name in ("SEEED-EE04-MONO-4.26", "SEEED-RETERMINAL-E1002"):
set_core_config(
PlatformFramework.ESP32_IDF,
platform_data={
KEY_BOARD: "esp32-s3-devkitc-1",
KEY_VARIANT: VARIANT_ESP32S3,
},
)
else:
set_core_config(
PlatformFramework.ESP32_IDF,
platform_data={KEY_BOARD: "esp32dev", KEY_VARIANT: VARIANT_ESP32},
)
# Configure SPI component which is required by epaper_spi
set_component_config("spi", {"id": "spi_bus", "clk_pin": 18, "mosi_pin": 19})
model = MODELS[model_name]
config: dict[str, Any] = {"model": model_name, "id": "test_display"}
# Add required fields based on model defaults
# Use safe GPIO pins that work on ESP32
if not model.get_default(CONF_DC_PIN):
config[CONF_DC_PIN] = 21
if not model.get_default(CONF_WIDTH):
config[CONF_DIMENSIONS] = {CONF_HEIGHT: 240, CONF_WIDTH: 320}
if model.initsequence is None:
config[CONF_INIT_SEQUENCE] = [[0xA0, 0x01]]
if not model.get_default(CONF_BUSY_PIN):
config[CONF_BUSY_PIN] = 22
if not model.get_default(CONF_RESET_PIN):
config[CONF_RESET_PIN] = 23
if not model.get_default(CONF_CS_PIN):
config[CONF_CS_PIN] = 5
# This should not raise any exceptions
run_schema_validation(config)
def test_model_with_explicit_dimensions(
set_core_config: SetCoreConfigCallable,
set_component_config: Callable[[str, Any], None],
) -> None:
"""Test model configuration with explicitly provided dimensions."""
set_core_config(
PlatformFramework.ESP32_IDF,
platform_data={KEY_BOARD: "esp32dev", KEY_VARIANT: VARIANT_ESP32},
)
# Configure SPI component which is required by epaper_spi
set_component_config("spi", {"id": "spi_bus", "clk_pin": 18, "mosi_pin": 19})
run_schema_validation(
{
"id": "test_display",
"model": "ssd1677",
"dc_pin": 21,
"busy_pin": 22,
"reset_pin": 23,
"cs_pin": 5,
"dimensions": {
"width": 200,
"height": 200,
},
}
)
def test_model_with_transform(
set_core_config: SetCoreConfigCallable,
set_component_config: Callable[[str, Any], None],
) -> None:
"""Test model configuration with transform options."""
set_core_config(
PlatformFramework.ESP32_IDF,
platform_data={KEY_BOARD: "esp32dev", KEY_VARIANT: VARIANT_ESP32},
)
# Configure SPI component which is required by epaper_spi
set_component_config("spi", {"id": "spi_bus", "clk_pin": 18, "mosi_pin": 19})
run_schema_validation(
{
"id": "test_display",
"model": "ssd1677",
"dc_pin": 21,
"busy_pin": 22,
"reset_pin": 23,
"cs_pin": 5,
"dimensions": {
"width": 200,
"height": 200,
},
"transform": {
"mirror_x": True,
"mirror_y": False,
},
}
)
def test_model_with_full_update_every(
set_core_config: SetCoreConfigCallable,
set_component_config: Callable[[str, Any], None],
) -> None:
"""Test model configuration with full_update_every option."""
set_core_config(
PlatformFramework.ESP32_IDF,
platform_data={KEY_BOARD: "esp32dev", KEY_VARIANT: VARIANT_ESP32},
)
# Configure SPI component which is required by epaper_spi
set_component_config("spi", {"id": "spi_bus", "clk_pin": 18, "mosi_pin": 19})
run_schema_validation(
{
"id": "test_display",
"model": "ssd1677",
"dc_pin": 21,
"busy_pin": 22,
"reset_pin": 23,
"cs_pin": 5,
"dimensions": {
"width": 200,
"height": 200,
},
"full_update_every": 10,
}
)
+1 -2
View File
@@ -17,8 +17,7 @@ def test_esp32_config(
) -> None:
set_core_config(PlatformFramework.ESP32_IDF)
from esphome.components.esp32 import CONFIG_SCHEMA
from esphome.components.esp32.const import VARIANT_ESP32, VARIANT_FRIENDLY
from esphome.components.esp32 import CONFIG_SCHEMA, VARIANT_ESP32, VARIANT_FRIENDLY
# Example ESP32 configuration
config = {
@@ -18,7 +18,8 @@ def test_gpio_binary_sensor_basic_setup(
assert "new gpio::GPIOBinarySensor();" in main_cpp
assert "App.register_binary_sensor" in main_cpp
assert "bs_gpio->set_use_interrupt(true);" in main_cpp
# set_use_interrupt(true) should NOT be generated (uses C++ default)
assert "bs_gpio->set_use_interrupt(true);" not in main_cpp
assert "bs_gpio->set_interrupt_type(gpio::INTERRUPT_ANY_EDGE);" in main_cpp
@@ -51,8 +52,8 @@ def test_gpio_binary_sensor_esp8266_other_pins_use_interrupt(
"tests/component_tests/gpio/test_gpio_binary_sensor_esp8266.yaml"
)
# GPIO5 should still use interrupts
assert "bs_gpio5->set_use_interrupt(true);" in main_cpp
# GPIO5 should still use interrupts (default, so no setter call)
assert "bs_gpio5->set_use_interrupt(true);" not in main_cpp
assert "bs_gpio5->set_interrupt_type(gpio::INTERRUPT_ANY_EDGE);" in main_cpp
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" width="10mm" height="10mm" viewBox="0 0 100 100">
<rect x="0" y="0" width="100" height="100" fill="#00FF00"/>
<circle cx="50" cy="50" r="30" fill="#0000FF"/>
</svg>

After

Width:  |  Height:  |  Size: 248 B

+170 -2
View File
@@ -5,12 +5,22 @@ from __future__ import annotations
from collections.abc import Callable
from pathlib import Path
from typing import Any
from unittest.mock import MagicMock, patch
import pytest
from esphome import config_validation as cv
from esphome.components.image import CONF_TRANSPARENCY, CONFIG_SCHEMA
from esphome.const import CONF_ID, CONF_RAW_DATA_ID, CONF_TYPE
from esphome.components.image import (
CONF_INVERT_ALPHA,
CONF_OPAQUE,
CONF_TRANSPARENCY,
CONFIG_SCHEMA,
get_all_image_metadata,
get_image_metadata,
write_image,
)
from esphome.const import CONF_DITHER, CONF_FILE, CONF_ID, CONF_RAW_DATA_ID, CONF_TYPE
from esphome.core import CORE
@pytest.mark.parametrize(
@@ -235,3 +245,161 @@ def test_image_generation(
"cat_img = new image::Image(uint8_t_id, 32, 24, image::IMAGE_TYPE_RGB565, image::TRANSPARENCY_OPAQUE);"
in main_cpp
)
def test_image_to_code_defines_and_core_data(
generate_main: Callable[[str | Path], str],
component_config_path: Callable[[str], Path],
) -> None:
"""Test that to_code() sets USE_IMAGE define and stores image metadata."""
# Generate the main cpp which will call to_code
generate_main(component_config_path("image_test.yaml"))
# Verify USE_IMAGE define was added
assert any(d.name == "USE_IMAGE" for d in CORE.defines), (
"USE_IMAGE define should be set when images are configured"
)
# Use the public API to get image metadata
# The test config has an image with id 'cat_img'
cat_img_metadata = get_image_metadata("cat_img")
assert cat_img_metadata is not None, (
"Image metadata should be retrievable via get_image_metadata()"
)
# Verify the metadata has the expected attributes
assert hasattr(cat_img_metadata, "width"), "Metadata should have width attribute"
assert hasattr(cat_img_metadata, "height"), "Metadata should have height attribute"
assert hasattr(cat_img_metadata, "image_type"), (
"Metadata should have image_type attribute"
)
assert hasattr(cat_img_metadata, "transparency"), (
"Metadata should have transparency attribute"
)
# Verify the values are correct (from the test image)
assert cat_img_metadata.width == 32, "Width should be 32"
assert cat_img_metadata.height == 24, "Height should be 24"
assert cat_img_metadata.image_type == "RGB565", "Type should be RGB565"
assert cat_img_metadata.transparency == "opaque", "Transparency should be opaque"
def test_image_to_code_multiple_images(
generate_main: Callable[[str | Path], str],
component_config_path: Callable[[str], Path],
) -> None:
"""Test that to_code() stores metadata for multiple images."""
generate_main(component_config_path("image_test.yaml"))
# Use the public API to get all image metadata
all_metadata = get_all_image_metadata()
assert isinstance(all_metadata, dict), (
"get_all_image_metadata() should return a dictionary"
)
# Verify that at least one image is present
assert len(all_metadata) > 0, "Should have at least one image metadata entry"
# Each image ID should map to an ImageMetaData object
for image_id, metadata in all_metadata.items():
assert isinstance(image_id, str), "Image IDs should be strings"
# Verify it's an ImageMetaData object with all required attributes
assert hasattr(metadata, "width"), (
f"Metadata for '{image_id}' should have width"
)
assert hasattr(metadata, "height"), (
f"Metadata for '{image_id}' should have height"
)
assert hasattr(metadata, "image_type"), (
f"Metadata for '{image_id}' should have image_type"
)
assert hasattr(metadata, "transparency"), (
f"Metadata for '{image_id}' should have transparency"
)
# Verify values are valid
assert isinstance(metadata.width, int), (
f"Width for '{image_id}' should be an integer"
)
assert isinstance(metadata.height, int), (
f"Height for '{image_id}' should be an integer"
)
assert isinstance(metadata.image_type, str), (
f"Type for '{image_id}' should be a string"
)
assert isinstance(metadata.transparency, str), (
f"Transparency for '{image_id}' should be a string"
)
assert metadata.width > 0, f"Width for '{image_id}' should be positive"
assert metadata.height > 0, f"Height for '{image_id}' should be positive"
def test_get_image_metadata_nonexistent() -> None:
"""Test that get_image_metadata returns None for non-existent image IDs."""
# This should return None when no images are configured or ID doesn't exist
metadata = get_image_metadata("nonexistent_image_id")
assert metadata is None, (
"get_image_metadata should return None for non-existent IDs"
)
def test_get_all_image_metadata_empty() -> None:
"""Test that get_all_image_metadata returns empty dict when no images configured."""
# When CORE hasn't been initialized with images, should return empty dict
all_metadata = get_all_image_metadata()
assert isinstance(all_metadata, dict), (
"get_all_image_metadata should always return a dict"
)
# Length could be 0 or more depending on what's in CORE at test time
@pytest.fixture
def mock_progmem_array():
"""Mock progmem_array to avoid needing a proper ID object in tests."""
with patch("esphome.components.image.cg.progmem_array") as mock_progmem:
mock_progmem.return_value = MagicMock()
yield mock_progmem
@pytest.mark.asyncio
async def test_svg_with_mm_dimensions_succeeds(
component_config_path: Callable[[str], Path],
mock_progmem_array: MagicMock,
) -> None:
"""Test that SVG files with dimensions in mm are successfully processed."""
# Create a config for write_image without CONF_RESIZE
config = {
CONF_FILE: component_config_path("mm_dimensions.svg"),
CONF_TYPE: "BINARY",
CONF_TRANSPARENCY: CONF_OPAQUE,
CONF_DITHER: "NONE",
CONF_INVERT_ALPHA: False,
CONF_RAW_DATA_ID: "test_raw_data_id",
}
# This should succeed without raising an error
result = await write_image(config)
# Verify that write_image returns the expected tuple
assert isinstance(result, tuple), "write_image should return a tuple"
assert len(result) == 6, "write_image should return 6 values"
prog_arr, width, height, image_type, trans_value, frame_count = result
# Verify the dimensions are positive integers
# At 100 DPI, 10mm = ~39 pixels (10mm * 100dpi / 25.4mm_per_inch)
assert isinstance(width, int), "Width should be an integer"
assert isinstance(height, int), "Height should be an integer"
assert width > 0, "Width should be positive"
assert height > 0, "Height should be positive"
assert frame_count == 1, "Single image should have frame_count of 1"
# Verify we got reasonable dimensions from the mm-based SVG
assert 30 < width < 50, (
f"Width should be around 39 pixels for 10mm at 100dpi, got {width}"
)
assert 30 < height < 50, (
f"Height should be around 39 pixels for 10mm at 100dpi, got {height}"
)
+2 -2
View File
@@ -20,9 +20,9 @@ def choose_variant_with_pins() -> Generator[Callable[[list], None]]:
"""
def chooser(pins: list) -> None:
for v in VARIANTS:
for variant in VARIANTS:
try:
CORE.data[KEY_ESP32][KEY_VARIANT] = v
CORE.data[KEY_ESP32][KEY_VARIANT] = variant
for pin in pins:
if pin is not None:
pin = gpio_pin_schema(
+12 -29
View File
@@ -1,4 +1,4 @@
"""Tests for mpip_spi configuration validation."""
"""Tests for mipi_spi configuration validation."""
from collections.abc import Callable
from pathlib import Path
@@ -220,37 +220,20 @@ def test_esp32s3_specific_errors(
set_core_config(
PlatformFramework.ESP32_IDF,
platform_data={KEY_BOARD: "esp32dev", KEY_VARIANT: VARIANT_ESP32S3},
platform_data={KEY_BOARD: "esp32-s3-devkitc-1", KEY_VARIANT: VARIANT_ESP32S3},
)
with pytest.raises(cv.Invalid, match=error_match):
run_schema_validation(config)
def test_framework_specific_errors(
set_core_config: SetCoreConfigCallable,
) -> None:
"""Test framework-specific configuration errors"""
set_core_config(
PlatformFramework.ESP32_ARDUINO,
platform_data={KEY_BOARD: "esp32dev", KEY_VARIANT: VARIANT_ESP32},
)
with pytest.raises(
cv.Invalid,
match=r"This feature is only available with framework\(s\) esp-idf",
):
run_schema_validation({"model": "wt32-sc01-plus"})
def test_custom_model_with_all_options(
set_core_config: SetCoreConfigCallable,
) -> None:
"""Test custom model configuration with all available options."""
set_core_config(
PlatformFramework.ESP32_IDF,
platform_data={KEY_BOARD: "esp32dev", KEY_VARIANT: VARIANT_ESP32S3},
platform_data={KEY_BOARD: "esp32-s3-devkitc-1", KEY_VARIANT: VARIANT_ESP32S3},
)
run_schema_validation(
@@ -293,7 +276,7 @@ def test_all_predefined_models(
"""Test all predefined display models validate successfully with appropriate defaults."""
set_core_config(
PlatformFramework.ESP32_IDF,
platform_data={KEY_BOARD: "esp32dev", KEY_VARIANT: VARIANT_ESP32S3},
platform_data={KEY_BOARD: "esp32-s3-devkitc-1", KEY_VARIANT: VARIANT_ESP32S3},
)
# Enable PSRAM which is required for some models
@@ -304,14 +287,14 @@ def test_all_predefined_models(
config = {"model": name}
# Get the pins required by this model and find a compatible variant
pins = [
pin
for pin in [
model.get_default(pin, None)
for pin in ("dc_pin", "reset_pin", "cs_pin")
]
if pin is not None
]
pins = []
for pin_name in ("dc_pin", "reset_pin", "cs_pin", "enable_pin"):
pin_value = model.get_default(pin_name, None)
if pin_value is not None:
if isinstance(pin_value, list):
pins.extend(pin_value)
else:
pins.append(pin_value)
choose_variant_with_pins(pins)
# Add required fields that don't have defaults
@@ -1,6 +1,18 @@
"""Tests for the web_server OTA platform."""
from __future__ import annotations
from collections.abc import Callable
import logging
from typing import Any
import pytest
from esphome import config_validation as cv
from esphome.components.web_server.ota import _web_server_ota_final_validate
from esphome.const import CONF_ID, CONF_OTA, CONF_PLATFORM, CONF_WEB_SERVER
from esphome.core import ID
import esphome.final_validate as fv
def test_web_server_ota_generated(generate_main: Callable[[str], str]) -> None:
@@ -100,3 +112,144 @@ def test_web_server_ota_esp8266(generate_main: Callable[[str], str]) -> None:
# Check web server OTA component is present
assert "WebServerOTAComponent" in main_cpp
assert "web_server::WebServerOTAComponent" in main_cpp
@pytest.mark.parametrize(
("ota_configs", "expected_count", "warning_expected"),
[
pytest.param(
[
{
CONF_PLATFORM: CONF_WEB_SERVER,
CONF_ID: ID("ota_web", is_manual=False),
}
],
1,
False,
id="single_instance_no_merge",
),
pytest.param(
[
{
CONF_PLATFORM: CONF_WEB_SERVER,
CONF_ID: ID("ota_web_1", is_manual=False),
},
{
CONF_PLATFORM: CONF_WEB_SERVER,
CONF_ID: ID("ota_web_2", is_manual=False),
},
],
1,
True,
id="two_instances_merged",
),
pytest.param(
[
{
CONF_PLATFORM: CONF_WEB_SERVER,
CONF_ID: ID("ota_web_1", is_manual=False),
},
{
CONF_PLATFORM: "esphome",
CONF_ID: ID("ota_esphome", is_manual=False),
},
{
CONF_PLATFORM: CONF_WEB_SERVER,
CONF_ID: ID("ota_web_2", is_manual=False),
},
],
2,
True,
id="mixed_platforms_web_server_merged",
),
],
)
def test_web_server_ota_instance_merging(
ota_configs: list[dict[str, Any]],
expected_count: int,
warning_expected: bool,
caplog: pytest.LogCaptureFixture,
) -> None:
"""Test web_server OTA instance merging behavior."""
full_conf = {CONF_OTA: ota_configs.copy()}
token = fv.full_config.set(full_conf)
try:
with caplog.at_level(logging.WARNING):
_web_server_ota_final_validate({})
updated_conf = fv.full_config.get()
# Verify total number of OTA platforms
assert len(updated_conf[CONF_OTA]) == expected_count
# Verify warning
if warning_expected:
assert any(
"Found and merged" in record.message
and "web_server OTA" in record.message
for record in caplog.records
), "Expected merge warning not found in log"
else:
assert len(caplog.records) == 0, "Unexpected warnings logged"
finally:
fv.full_config.reset(token)
def test_web_server_ota_consistent_manual_ids(
caplog: pytest.LogCaptureFixture,
) -> None:
"""Test that consistent manual IDs can be merged successfully."""
ota_configs = [
{
CONF_PLATFORM: CONF_WEB_SERVER,
CONF_ID: ID("ota_web", is_manual=True),
},
{
CONF_PLATFORM: CONF_WEB_SERVER,
CONF_ID: ID("ota_web", is_manual=True),
},
]
full_conf = {CONF_OTA: ota_configs}
token = fv.full_config.set(full_conf)
try:
with caplog.at_level(logging.WARNING):
_web_server_ota_final_validate({})
updated_conf = fv.full_config.get()
assert len(updated_conf[CONF_OTA]) == 1
assert updated_conf[CONF_OTA][0][CONF_ID].id == "ota_web"
assert any(
"Found and merged" in record.message and "web_server OTA" in record.message
for record in caplog.records
)
finally:
fv.full_config.reset(token)
def test_web_server_ota_inconsistent_manual_ids() -> None:
"""Test that inconsistent manual IDs raise an error."""
ota_configs = [
{
CONF_PLATFORM: CONF_WEB_SERVER,
CONF_ID: ID("ota_web_1", is_manual=True),
},
{
CONF_PLATFORM: CONF_WEB_SERVER,
CONF_ID: ID("ota_web_2", is_manual=True),
},
]
full_conf = {CONF_OTA: ota_configs}
token = fv.full_config.set(full_conf)
try:
with pytest.raises(
cv.Invalid,
match="Found multiple web_server OTA configurations but id is inconsistent",
):
_web_server_ota_final_validate({})
finally:
fv.full_config.reset(token)
+279 -23
View File
@@ -5,7 +5,8 @@ from unittest.mock import MagicMock, patch
import pytest
from esphome.components.packages import CONFIG_SCHEMA, do_packages_pass
from esphome.components.packages import CONFIG_SCHEMA, do_packages_pass, merge_packages
import esphome.config as config_module
from esphome.config import resolve_extend_remove
from esphome.config_helpers import Extend, Remove
import esphome.config_validation as cv
@@ -27,11 +28,13 @@ from esphome.const import (
CONF_REFRESH,
CONF_SENSOR,
CONF_SSID,
CONF_SUBSTITUTIONS,
CONF_UPDATE_INTERVAL,
CONF_URL,
CONF_VARS,
CONF_WIFI,
)
from esphome.core import CORE
from esphome.util import OrderedDict
# Test strings
@@ -68,11 +71,12 @@ def fixture_basic_esphome():
def packages_pass(config):
"""Wrapper around packages_pass that also resolves Extend and Remove."""
config = do_packages_pass(config)
config = merge_packages(config)
resolve_extend_remove(config)
return config
def test_package_unused(basic_esphome, basic_wifi):
def test_package_unused(basic_esphome, basic_wifi) -> None:
"""
Ensures do_package_pass does not change a config if packages aren't used.
"""
@@ -82,7 +86,7 @@ def test_package_unused(basic_esphome, basic_wifi):
assert actual == config
def test_package_invalid_dict(basic_esphome, basic_wifi):
def test_package_invalid_dict(basic_esphome, basic_wifi) -> None:
"""
If a url: key is present, it's expected to be well-formed remote package spec. Ensure an error is raised if not.
Any other simple dict passed as a package will be merged as usual but may fail later validation.
@@ -95,7 +99,7 @@ def test_package_invalid_dict(basic_esphome, basic_wifi):
@pytest.mark.parametrize(
"package",
"packages",
[
{"package1": "github://esphome/non-existant-repo/file1.yml@main"},
{"package2": "github://esphome/non-existant-repo/file1.yml"},
@@ -107,12 +111,12 @@ def test_package_invalid_dict(basic_esphome, basic_wifi):
],
],
)
def test_package_shorthand(package):
CONFIG_SCHEMA(package)
def test_package_shorthand(packages) -> None:
CONFIG_SCHEMA(packages)
@pytest.mark.parametrize(
"package",
"packages",
[
# not github
{"package1": "someplace://esphome/non-existant-repo/file1.yml@main"},
@@ -133,12 +137,12 @@ def test_package_shorthand(package):
[3],
],
)
def test_package_invalid(package):
def test_package_invalid(packages) -> None:
with pytest.raises(cv.Invalid):
CONFIG_SCHEMA(package)
CONFIG_SCHEMA(packages)
def test_package_include(basic_wifi, basic_esphome):
def test_package_include(basic_wifi, basic_esphome) -> None:
"""
Tests the simple case where an independent config present in a package is added to the top-level config as is.
@@ -155,7 +159,31 @@ def test_package_include(basic_wifi, basic_esphome):
assert actual == expected
def test_package_append(basic_wifi, basic_esphome):
def test_single_package(
basic_esphome,
basic_wifi,
caplog: pytest.LogCaptureFixture,
) -> None:
"""
Tests the simple case where a single package is added to the top-level config as is.
In this test, the CONF_WIFI config is expected to be simply added to the top-level config.
This tests the case where the user just put packages: !include package.yaml, not
part of a list or mapping of packages.
This behavior is deprecated, the test also checks if a warning is issued.
"""
config = {CONF_ESPHOME: basic_esphome, CONF_PACKAGES: {CONF_WIFI: basic_wifi}}
expected = {CONF_ESPHOME: basic_esphome, CONF_WIFI: basic_wifi}
with caplog.at_level("WARNING"):
actual = packages_pass(config)
assert actual == expected
assert "This method for including packages will go away in 2026.7.0" in caplog.text
def test_package_append(basic_wifi, basic_esphome) -> None:
"""
Tests the case where a key is present in both a package and top-level config.
@@ -180,7 +208,7 @@ def test_package_append(basic_wifi, basic_esphome):
assert actual == expected
def test_package_override(basic_wifi, basic_esphome):
def test_package_override(basic_wifi, basic_esphome) -> None:
"""
Ensures that the top-level configuration takes precedence over duplicate keys defined in a package.
@@ -204,7 +232,7 @@ def test_package_override(basic_wifi, basic_esphome):
assert actual == expected
def test_multiple_package_order():
def test_multiple_package_order() -> None:
"""
Ensures that mutiple packages are merged in order.
"""
@@ -233,7 +261,7 @@ def test_multiple_package_order():
assert actual == expected
def test_package_list_merge():
def test_package_list_merge() -> None:
"""
Ensures lists defined in both a package and the top-level config are merged correctly
"""
@@ -289,7 +317,7 @@ def test_package_list_merge():
assert actual == expected
def test_package_list_merge_by_id():
def test_package_list_merge_by_id() -> None:
"""
Ensures that components with matching IDs are merged correctly.
@@ -367,7 +395,7 @@ def test_package_list_merge_by_id():
assert actual == expected
def test_package_merge_by_id_with_list():
def test_package_merge_by_id_with_list() -> None:
"""
Ensures that components with matching IDs are merged correctly when their configuration contains lists.
@@ -406,7 +434,7 @@ def test_package_merge_by_id_with_list():
assert actual == expected
def test_package_merge_by_missing_id():
def test_package_merge_by_missing_id() -> None:
"""
Ensures that a validation error is thrown when trying to extend a missing ID.
"""
@@ -442,7 +470,7 @@ def test_package_merge_by_missing_id():
assert error_raised
def test_package_list_remove_by_id():
def test_package_list_remove_by_id() -> None:
"""
Ensures that components with matching IDs are removed correctly.
@@ -493,7 +521,7 @@ def test_package_list_remove_by_id():
assert actual == expected
def test_multiple_package_list_remove_by_id():
def test_multiple_package_list_remove_by_id() -> None:
"""
Ensures that components with matching IDs are removed correctly.
@@ -539,7 +567,7 @@ def test_multiple_package_list_remove_by_id():
assert actual == expected
def test_package_dict_remove_by_id(basic_wifi, basic_esphome):
def test_package_dict_remove_by_id(basic_wifi, basic_esphome) -> None:
"""
Ensures that components with missing IDs are removed from dict.
Ensures that the top-level configuration takes precedence over duplicate keys defined in a package.
@@ -560,7 +588,7 @@ def test_package_dict_remove_by_id(basic_wifi, basic_esphome):
assert actual == expected
def test_package_remove_by_missing_id():
def test_package_remove_by_missing_id() -> None:
"""
Ensures that components with missing IDs are not merged.
"""
@@ -608,7 +636,7 @@ def test_package_remove_by_missing_id():
@patch("esphome.git.clone_or_update")
def test_remote_packages_with_files_list(
mock_clone_or_update, mock_is_file, mock_load_yaml
):
) -> None:
"""
Ensures that packages are loaded as mixed list of dictionary and strings
"""
@@ -680,7 +708,7 @@ def test_remote_packages_with_files_list(
@patch("esphome.git.clone_or_update")
def test_remote_packages_with_files_and_vars(
mock_clone_or_update, mock_is_file, mock_load_yaml
):
) -> None:
"""
Ensures that packages are loaded as mixed list of dictionary and strings with vars
"""
@@ -769,3 +797,231 @@ def test_remote_packages_with_files_and_vars(
actual = packages_pass(config)
assert actual == expected
def test_packages_merge_substitutions() -> None:
"""
Tests that substitutions from packages in a complex package hierarchy
are extracted and merged into the top-level config.
"""
config = {
CONF_SUBSTITUTIONS: {
"a": 1,
"b": 2,
"c": 3,
},
CONF_PACKAGES: {
"package1": {
"logger": {
"level": "DEBUG",
},
CONF_PACKAGES: [
{
CONF_SUBSTITUTIONS: {
"a": 10,
"e": 5,
},
"sensor": [
{"platform": "template", "id": "sensor1"},
],
},
],
"sensor": [
{"platform": "template", "id": "sensor2"},
],
},
"package2": {
"logger": {
"level": "VERBOSE",
},
},
"package3": {
CONF_PACKAGES: [
{
CONF_PACKAGES: [
{
CONF_SUBSTITUTIONS: {
"b": 20,
"d": 4,
},
"sensor": [
{"platform": "template", "id": "sensor3"},
],
},
],
CONF_SUBSTITUTIONS: {
"b": 20,
"d": 6,
},
"sensor": [
{"platform": "template", "id": "sensor4"},
],
},
],
},
},
}
expected = {
CONF_SUBSTITUTIONS: {"a": 1, "e": 5, "b": 2, "d": 6, "c": 3},
CONF_PACKAGES: {
"package1": {
"logger": {
"level": "DEBUG",
},
CONF_PACKAGES: [
{
"sensor": [
{"platform": "template", "id": "sensor1"},
],
},
],
"sensor": [
{"platform": "template", "id": "sensor2"},
],
},
"package2": {
"logger": {
"level": "VERBOSE",
},
},
"package3": {
CONF_PACKAGES: [
{
CONF_PACKAGES: [
{
"sensor": [
{"platform": "template", "id": "sensor3"},
],
},
],
"sensor": [
{"platform": "template", "id": "sensor4"},
],
},
],
},
},
}
actual = do_packages_pass(config)
assert actual == expected
def test_package_merge() -> None:
"""
Tests that all packages are merged into the top-level config.
"""
config = {
CONF_SUBSTITUTIONS: {"a": 1, "e": 5, "b": 2, "d": 6, "c": 3},
CONF_PACKAGES: {
"package1": {
"logger": {
"level": "DEBUG",
},
CONF_PACKAGES: [
{
"sensor": [
{"platform": "template", "id": "sensor1"},
],
},
],
"sensor": [
{"platform": "template", "id": "sensor2"},
],
},
"package2": {
"logger": {
"level": "VERBOSE",
},
},
"package3": {
CONF_PACKAGES: [
{
CONF_PACKAGES: [
{
"sensor": [
{"platform": "template", "id": "sensor3"},
],
},
],
"sensor": [
{"platform": "template", "id": "sensor4"},
],
},
],
},
},
}
expected = {
"sensor": [
{"platform": "template", "id": "sensor1"},
{"platform": "template", "id": "sensor2"},
{"platform": "template", "id": "sensor3"},
{"platform": "template", "id": "sensor4"},
],
"logger": {"level": "VERBOSE"},
CONF_SUBSTITUTIONS: {"a": 1, "e": 5, "b": 2, "d": 6, "c": 3},
}
actual = merge_packages(config)
assert actual == expected
@pytest.mark.parametrize(
"invalid_package",
[
6,
"some string",
["some string"],
None,
True,
{"some_component": 8},
{3: 2},
{"some_component": r"${unevaluated expression}"},
],
)
def test_package_merge_invalid(invalid_package) -> None:
"""
Tests that trying to merge an invalid package raises an error.
"""
config = {
CONF_PACKAGES: {
"some_package": invalid_package,
},
}
with pytest.raises(cv.Invalid):
merge_packages(config)
def test_raw_config_contains_merged_esphome_from_package(tmp_path) -> None:
"""Test that CORE.raw_config contains esphome section from merged package.
This is a regression test for the bug where CORE.raw_config was set before
packages were merged, causing KeyError when components accessed
CORE.raw_config[CONF_ESPHOME] and the esphome section came from a package.
"""
# Create a config where esphome section comes from a package
test_config = OrderedDict()
test_config[CONF_PACKAGES] = {
"base": {
CONF_ESPHOME: {CONF_NAME: TEST_DEVICE_NAME},
}
}
test_config["esp32"] = {"board": "esp32dev"}
# Set up CORE for the test
test_yaml = tmp_path / "test.yaml"
test_yaml.write_text("# test config")
CORE.reset()
CORE.config_path = test_yaml
# Call validate_config - this should merge packages and set CORE.raw_config
config_module.validate_config(test_config, {})
# Verify that CORE.raw_config contains the esphome section from the package
assert CONF_ESPHOME in CORE.raw_config, (
"CORE.raw_config should contain esphome section after package merge"
)
assert CORE.raw_config[CONF_ESPHOME][CONF_NAME] == TEST_DEVICE_NAME
+13 -6
View File
@@ -4,7 +4,7 @@ from typing import Any
import pytest
from esphome.components.esp32.const import (
from esphome.components.esp32 import (
KEY_VARIANT,
VARIANT_ESP32,
VARIANT_ESP32C2,
@@ -23,17 +23,24 @@ from tests.component_tests.types import SetCoreConfigCallable
UNSUPPORTED_PSRAM_VARIANTS = [
VARIANT_ESP32C2,
VARIANT_ESP32C3,
VARIANT_ESP32C5,
VARIANT_ESP32C6,
VARIANT_ESP32H2,
]
SUPPORTED_PSRAM_VARIANTS = [
VARIANT_ESP32,
VARIANT_ESP32C5,
VARIANT_ESP32P4,
VARIANT_ESP32S2,
VARIANT_ESP32S3,
VARIANT_ESP32P4,
]
SUPPORTED_PSRAM_MODES = {
VARIANT_ESP32: ["quad"],
VARIANT_ESP32C5: ["quad"],
VARIANT_ESP32P4: ["hex"],
VARIANT_ESP32S2: ["quad"],
VARIANT_ESP32S3: ["quad", "octal"],
}
@pytest.mark.parametrize(
@@ -86,7 +93,7 @@ def test_psram_configuration_valid_supported_variants(
from esphome.components.psram import CONFIG_SCHEMA, FINAL_VALIDATE_SCHEMA
# This should not raise an exception
config = CONFIG_SCHEMA({})
config = CONFIG_SCHEMA({"mode": SUPPORTED_PSRAM_MODES[variant][0]})
FINAL_VALIDATE_SCHEMA(config)
@@ -122,7 +129,7 @@ def _setup_psram_final_validation_test(
("config", "esp32_config", "expect_error", "error_match"),
[
pytest.param(
{"speed": "120MHz"},
{"mode": "quad", "speed": "120MHz"},
{"cpu_frequency": "160MHz"},
True,
r"PSRAM 120MHz requires 240MHz CPU frequency",
@@ -143,7 +150,7 @@ def _setup_psram_final_validation_test(
id="ecc_only_in_octal_mode",
),
pytest.param(
{"speed": "120MHZ"},
{"mode": "quad", "speed": "120MHZ"},
{"cpu_frequency": "240MHZ"},
False,
None,
+1
View File
@@ -0,0 +1 @@
"""Tests for SNTP component."""
@@ -0,0 +1,22 @@
esphome:
name: sntp-test
esp32:
board: esp32dev
framework:
type: esp-idf
wifi:
ssid: "testssid"
password: "testpassword"
# Test multiple SNTP instances that should be merged
time:
- platform: sntp
servers:
- 192.168.1.1
- pool.ntp.org
- platform: sntp
servers:
- pool.ntp.org
- 192.168.1.2
+238
View File
@@ -0,0 +1,238 @@
"""Tests for SNTP time configuration validation."""
from __future__ import annotations
import logging
from typing import Any
import pytest
from esphome import config_validation as cv
from esphome.components.sntp.time import CONF_SNTP, _sntp_final_validate
from esphome.const import CONF_ID, CONF_PLATFORM, CONF_SERVERS, CONF_TIME
from esphome.core import ID
import esphome.final_validate as fv
@pytest.mark.parametrize(
("time_configs", "expected_count", "expected_servers", "warning_messages"),
[
pytest.param(
[
{
CONF_PLATFORM: CONF_SNTP,
CONF_ID: ID("sntp_time", is_manual=False),
CONF_SERVERS: ["192.168.1.1", "pool.ntp.org"],
}
],
1,
["192.168.1.1", "pool.ntp.org"],
[],
id="single_instance_no_merge",
),
pytest.param(
[
{
CONF_PLATFORM: CONF_SNTP,
CONF_ID: ID("sntp_time_1", is_manual=False),
CONF_SERVERS: ["192.168.1.1", "pool.ntp.org"],
},
{
CONF_PLATFORM: CONF_SNTP,
CONF_ID: ID("sntp_time_2", is_manual=False),
CONF_SERVERS: ["192.168.1.2"],
},
],
1,
["192.168.1.1", "pool.ntp.org", "192.168.1.2"],
["Found and merged 2 SNTP time configurations into one instance"],
id="two_instances_merged",
),
pytest.param(
[
{
CONF_PLATFORM: CONF_SNTP,
CONF_ID: ID("sntp_time_1", is_manual=False),
CONF_SERVERS: ["192.168.1.1", "pool.ntp.org"],
},
{
CONF_PLATFORM: CONF_SNTP,
CONF_ID: ID("sntp_time_2", is_manual=False),
CONF_SERVERS: ["pool.ntp.org", "192.168.1.2"],
},
],
1,
["192.168.1.1", "pool.ntp.org", "192.168.1.2"],
["Found and merged 2 SNTP time configurations into one instance"],
id="deduplication_preserves_order",
),
pytest.param(
[
{
CONF_PLATFORM: CONF_SNTP,
CONF_ID: ID("sntp_time_1", is_manual=False),
CONF_SERVERS: ["192.168.1.1", "pool.ntp.org"],
},
{
CONF_PLATFORM: CONF_SNTP,
CONF_ID: ID("sntp_time_2", is_manual=False),
CONF_SERVERS: ["192.168.1.2", "pool2.ntp.org"],
},
{
CONF_PLATFORM: CONF_SNTP,
CONF_ID: ID("sntp_time_3", is_manual=False),
CONF_SERVERS: ["pool3.ntp.org"],
},
],
1,
["192.168.1.1", "pool.ntp.org", "192.168.1.2"],
[
"SNTP supports maximum 3 servers. Dropped excess server(s): ['pool2.ntp.org', 'pool3.ntp.org']",
"Found and merged 3 SNTP time configurations into one instance",
],
id="three_instances_drops_excess_servers",
),
pytest.param(
[
{
CONF_PLATFORM: CONF_SNTP,
CONF_ID: ID("sntp_time_1", is_manual=False),
CONF_SERVERS: [
"192.168.1.1",
"pool.ntp.org",
"pool.ntp.org",
"192.168.1.1",
],
},
{
CONF_PLATFORM: CONF_SNTP,
CONF_ID: ID("sntp_time_2", is_manual=False),
CONF_SERVERS: ["pool.ntp.org", "192.168.1.2"],
},
],
1,
["192.168.1.1", "pool.ntp.org", "192.168.1.2"],
["Found and merged 2 SNTP time configurations into one instance"],
id="deduplication_multiple_duplicates",
),
],
)
def test_sntp_instance_merging(
time_configs: list[dict[str, Any]],
expected_count: int,
expected_servers: list[str],
warning_messages: list[str],
caplog: pytest.LogCaptureFixture,
) -> None:
"""Test SNTP instance merging behavior."""
# Create a mock full config with time configs
full_conf = {CONF_TIME: time_configs.copy()}
# Set the context var
token = fv.full_config.set(full_conf)
try:
with caplog.at_level(logging.WARNING):
_sntp_final_validate({})
# Get the updated config
updated_conf = fv.full_config.get()
# Check if merging occurred
if len(time_configs) > 1:
# Verify only one SNTP instance remains
sntp_instances = [
tc
for tc in updated_conf[CONF_TIME]
if tc.get(CONF_PLATFORM) == CONF_SNTP
]
assert len(sntp_instances) == expected_count
# Verify server list
assert sntp_instances[0][CONF_SERVERS] == expected_servers
# Verify warnings
for expected_msg in warning_messages:
assert any(
expected_msg in record.message for record in caplog.records
), f"Expected warning message '{expected_msg}' not found in log"
else:
# Single instance should not trigger merging or warnings
assert len(caplog.records) == 0
# Config should be unchanged
assert updated_conf[CONF_TIME] == time_configs
finally:
fv.full_config.reset(token)
def test_sntp_inconsistent_manual_ids() -> None:
"""Test that inconsistent manual IDs raise an error."""
# Create configs with manual IDs that are inconsistent
time_configs = [
{
CONF_PLATFORM: CONF_SNTP,
CONF_ID: ID("sntp_time_1", is_manual=True),
CONF_SERVERS: ["192.168.1.1"],
},
{
CONF_PLATFORM: CONF_SNTP,
CONF_ID: ID("sntp_time_2", is_manual=True),
CONF_SERVERS: ["192.168.1.2"],
},
]
full_conf = {CONF_TIME: time_configs}
token = fv.full_config.set(full_conf)
try:
with pytest.raises(
cv.Invalid,
match="Found multiple SNTP configurations but id is inconsistent",
):
_sntp_final_validate({})
finally:
fv.full_config.reset(token)
def test_sntp_with_other_time_platforms(caplog: pytest.LogCaptureFixture) -> None:
"""Test that SNTP merging doesn't affect other time platforms."""
time_configs = [
{
CONF_PLATFORM: CONF_SNTP,
CONF_ID: ID("sntp_time_1", is_manual=False),
CONF_SERVERS: ["192.168.1.1"],
},
{
CONF_PLATFORM: "homeassistant",
CONF_ID: ID("homeassistant_time", is_manual=False),
},
{
CONF_PLATFORM: CONF_SNTP,
CONF_ID: ID("sntp_time_2", is_manual=False),
CONF_SERVERS: ["192.168.1.2"],
},
]
full_conf = {CONF_TIME: time_configs.copy()}
token = fv.full_config.set(full_conf)
try:
with caplog.at_level(logging.WARNING):
_sntp_final_validate({})
updated_conf = fv.full_config.get()
# Should have 2 time platforms: 1 merged SNTP + 1 homeassistant
assert len(updated_conf[CONF_TIME]) == 2
# Find the platforms
platforms = {tc[CONF_PLATFORM] for tc in updated_conf[CONF_TIME]}
assert platforms == {CONF_SNTP, "homeassistant"}
# Verify SNTP was merged
sntp_instances = [
tc for tc in updated_conf[CONF_TIME] if tc[CONF_PLATFORM] == CONF_SNTP
]
assert len(sntp_instances) == 1
assert sntp_instances[0][CONF_SERVERS] == ["192.168.1.1", "192.168.1.2"]
finally:
fv.full_config.reset(token)
+1 -1
View File
@@ -25,7 +25,7 @@ def test_text_sets_mandatory_fields(generate_main):
main_cpp = generate_main("tests/component_tests/text/test_text.yaml")
# Then
assert 'it_1->set_name("test 1 text");' in main_cpp
assert 'it_1->set_name("test 1 text",' in main_cpp
def test_text_config_value_internal_set(generate_main):
@@ -25,9 +25,9 @@ def test_text_sensor_sets_mandatory_fields(generate_main):
main_cpp = generate_main("tests/component_tests/text_sensor/test_text_sensor.yaml")
# Then
assert 'ts_1->set_name("Template Text Sensor 1");' in main_cpp
assert 'ts_2->set_name("Template Text Sensor 2");' in main_cpp
assert 'ts_3->set_name("Template Text Sensor 3");' in main_cpp
assert 'ts_1->set_name("Template Text Sensor 1",' in main_cpp
assert 'ts_2->set_name("Template Text Sensor 2",' in main_cpp
assert 'ts_3->set_name("Template Text Sensor 3",' in main_cpp
def test_text_sensor_config_value_internal_set(generate_main):
@@ -1,8 +0,0 @@
packages:
uart: !include ../../test_build_components/common/uart/esp32-c3-idf.yaml
substitutions:
tx_pin: GPIO4
rx_pin: GPIO5
<<: !include common.yaml
@@ -1,8 +0,0 @@
packages:
uart: !include ../../test_build_components/common/uart/esp32-c3-idf.yaml
substitutions:
tx_pin: GPIO4
rx_pin: GPIO5
<<: !include common.yaml
@@ -1,6 +0,0 @@
substitutions:
step_pin: GPIO2
dir_pin: GPIO3
sleep_pin: GPIO5
<<: !include common.yaml
@@ -1,5 +0,0 @@
substitutions:
gate_pin: GPIO4
zero_cross_pin: GPIO5
<<: !include common.yaml
@@ -0,0 +1,5 @@
substitutions:
gate_pin: GPIO18
zero_cross_pin: GPIO19
<<: !include common.yaml
@@ -1,7 +0,0 @@
substitutions:
cs_pin: GPIO2
packages:
spi: !include ../../test_build_components/common/spi/esp32-c3-idf.yaml
<<: !include common.yaml
@@ -1,9 +0,0 @@
substitutions:
irq0_pin: GPIO6
irq1_pin: GPIO7
reset_pin: GPIO9
packages:
i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml
<<: !include common.yaml
@@ -1,7 +0,0 @@
substitutions:
irq_pin: GPIO6
packages:
i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml
<<: !include common.yaml
@@ -1,7 +0,0 @@
substitutions:
irq_pin: GPIO9
cs_pin: GPIO8
packages:
spi: !include ../../test_build_components/common/spi/esp32-c3-idf.yaml
<<: !include common.yaml
@@ -0,0 +1,4 @@
packages:
i2c: !include ../../test_build_components/common/i2c/nrf52.yaml
<<: !include common.yaml
@@ -0,0 +1,4 @@
packages:
i2c: !include ../../test_build_components/common/i2c/nrf52.yaml
<<: !include common.yaml
@@ -1,4 +0,0 @@
packages:
i2c_low_freq: !include ../../test_build_components/common/i2c_low_freq/esp32-c3-idf.yaml
<<: !include common.yaml
@@ -1,4 +0,0 @@
packages:
i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml
<<: !include common.yaml
@@ -0,0 +1,4 @@
packages:
i2c: !include ../../test_build_components/common/i2c/nrf52.yaml
<<: !include common.yaml
@@ -1,4 +0,0 @@
packages:
i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml
<<: !include common.yaml
@@ -1,4 +0,0 @@
packages:
ble: !include ../../test_build_components/common/ble/esp32-c3-idf.yaml
<<: !include common.yaml
@@ -1,4 +0,0 @@
packages:
ble: !include ../../test_build_components/common/ble/esp32-c3-idf.yaml
<<: !include common.yaml
@@ -9,6 +9,8 @@ alarm_control_panel:
name: Alarm Panel
codes:
- "1234"
- "5678"
- "0000"
requires_code_to_arm: true
arming_home_time: 1s
arming_night_time: 1s
@@ -29,6 +31,7 @@ alarm_control_panel:
name: Alarm Panel 2
codes:
- "1234"
- "9999"
requires_code_to_arm: true
arming_home_time: 1s
arming_night_time: 1s
@@ -1,4 +0,0 @@
packages:
ble: !include ../../test_build_components/common/ble/esp32-c3-idf.yaml
<<: !include common.yaml
@@ -1,4 +0,0 @@
packages:
i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml
<<: !include common.yaml
@@ -1,4 +0,0 @@
packages:
i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml
<<: !include common.yaml
@@ -1,4 +0,0 @@
packages:
ble: !include ../../test_build_components/common/ble/esp32-c3-idf.yaml
<<: !include common.yaml
@@ -1,13 +0,0 @@
packages:
spi: !include ../../test_build_components/common/spi/esp32-c3-idf.yaml
animation: !include common.yaml
display:
- platform: ili9xxx
id: main_lcd
spi_id: spi_bus
model: ili9342
cs_pin: 8
dc_pin: 9
reset_pin: 10
invert_colors: false
@@ -11,3 +11,4 @@ display:
dc_pin: 21
reset_pin: 22
invert_colors: false
data_rate: 10MHz
@@ -1,4 +0,0 @@
packages:
ble: !include ../../test_build_components/common/ble/esp32-c3-idf.yaml
<<: !include common.yaml
@@ -1,4 +0,0 @@
packages:
i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml
<<: !include common.yaml
@@ -1,4 +0,0 @@
packages:
i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml
<<: !include common.yaml
+201
View File
@@ -1,6 +1,10 @@
esphome:
on_boot:
then:
- wait_until:
condition:
api.connected:
state_subscription_only: true
- homeassistant.event:
event: esphome.button_pressed
data:
@@ -87,3 +91,200 @@ api:
- float_arr.size()
- string_arr[0].c_str()
- string_arr.size()
# Test ContinuationAction (IfAction with then/else branches)
- action: test_if_action
variables:
condition: bool
value: int
then:
- if:
condition:
lambda: 'return condition;'
then:
- logger.log:
format: "Condition true, value: %d"
args: ['value']
else:
- logger.log:
format: "Condition false, value: %d"
args: ['value']
- logger.log: "After if/else"
# Test nested IfAction (multiple ContinuationAction instances)
- action: test_nested_if
variables:
outer: bool
inner: bool
then:
- if:
condition:
lambda: 'return outer;'
then:
- if:
condition:
lambda: 'return inner;'
then:
- logger.log: "Both true"
else:
- logger.log: "Outer true, inner false"
else:
- logger.log: "Outer false"
- logger.log: "After nested if"
# Test WhileLoopContinuation (WhileAction)
- action: test_while_action
variables:
max_count: int
then:
- lambda: 'id(api_continuation_test_counter) = 0;'
- while:
condition:
lambda: 'return id(api_continuation_test_counter) < max_count;'
then:
- logger.log:
format: "While loop iteration: %d"
args: ['id(api_continuation_test_counter)']
- lambda: 'id(api_continuation_test_counter)++;'
- logger.log: "After while loop"
# Test RepeatLoopContinuation (RepeatAction)
- action: test_repeat_action
variables:
count: int
then:
- repeat:
count: !lambda 'return count;'
then:
- logger.log:
format: "Repeat iteration: %d"
args: ['iteration']
- logger.log: "After repeat"
# Test combined continuations (if + while + repeat)
- action: test_combined_continuations
variables:
do_loop: bool
loop_count: int
then:
- if:
condition:
lambda: 'return do_loop;'
then:
- repeat:
count: !lambda 'return loop_count;'
then:
- lambda: 'id(api_continuation_test_counter) = iteration;'
- while:
condition:
lambda: 'return id(api_continuation_test_counter) > 0;'
then:
- logger.log:
format: "Combined: repeat=%d, while=%d"
args: ['iteration', 'id(api_continuation_test_counter)']
- lambda: 'id(api_continuation_test_counter)--;'
else:
- logger.log: "Skipped loops"
- logger.log: "After combined test"
# ==========================================================================
# supports_response: status (auto-detected - api.respond without data)
# Has call_id only - reports success/error without data payload
# ==========================================================================
- action: test_respond_status
then:
- api.respond:
success: true
- logger.log:
format: "Status response sent (call_id=%d)"
args: [call_id]
- action: test_respond_status_error
variables:
error_msg: string
then:
- api.respond:
success: false
error_message: !lambda 'return error_msg;'
# ==========================================================================
# supports_response: optional (auto-detected - api.respond with data)
# Has call_id and return_response - client decides if it wants response
# ==========================================================================
- action: test_respond_optional
variables:
sensor_name: string
value: float
then:
- logger.log:
format: "Optional response (call_id=%d, return_response=%d)"
args: [call_id, return_response]
- api.respond:
data: !lambda |-
root["sensor"] = sensor_name;
root["value"] = value;
root["unit"] = "°C";
- action: test_respond_optional_conditional
variables:
do_succeed: bool
then:
- if:
condition:
lambda: 'return do_succeed;'
then:
- api.respond:
success: true
data: !lambda |-
root["status"] = "ok";
else:
- api.respond:
success: false
error_message: "Operation failed"
# ==========================================================================
# supports_response: only (explicit - always expects data response)
# Has call_id only - response is always expected with data
# ==========================================================================
- action: test_respond_only
supports_response: only
variables:
input: string
then:
- logger.log:
format: "Only response (call_id=%d)"
args: [call_id]
- api.respond:
data: !lambda |-
root["input"] = input;
root["processed"] = true;
- action: test_respond_only_nested
supports_response: only
then:
- api.respond:
data: !lambda |-
root["config"]["wifi"] = "connected";
root["config"]["api"] = true;
root["items"][0] = "item1";
root["items"][1] = "item2";
# ==========================================================================
# supports_response: none (no api.respond action)
# No call_id or return_response - just user variables
# ==========================================================================
- action: test_no_response
variables:
message: string
then:
- logger.log:
format: "No response action: %s"
args: [message.c_str()]
event:
- platform: template
name: Test Event
id: test_event
event_types:
- single_click
- double_click
globals:
- id: api_continuation_test_counter
type: int
restore_value: false
initial_value: '0'
+22
View File
@@ -0,0 +1,22 @@
sensor:
- platform: template
id: pm25_sensor
name: "PM2.5"
lambda: "return 25.0;"
- platform: template
id: pm10_sensor
name: "PM10"
lambda: "return 50.0;"
- platform: aqi
name: "Air Quality Index (AQI)"
pm_2_5: pm25_sensor
pm_10_0: pm10_sensor
calculation_type: AQI
- platform: aqi
name: "Air Quality Index (CAQI)"
pm_2_5: pm25_sensor
pm_10_0: pm10_sensor
calculation_type: CAQI
@@ -1,7 +0,0 @@
substitutions:
irq_pin: GPIO6
packages:
i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml
<<: !include common.yaml
@@ -1,7 +0,0 @@
substitutions:
irq_pin: GPIO9
cs_pin: GPIO8
packages:
spi: !include ../../test_build_components/common/spi/esp32-c3-idf.yaml
<<: !include common.yaml
@@ -1,7 +0,0 @@
substitutions:
dir_pin: GPIO6
packages:
i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml
<<: !include common.yaml
@@ -1,4 +0,0 @@
packages:
i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml
<<: !include common.yaml
@@ -1,4 +0,0 @@
packages:
i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml
<<: !include common.yaml
@@ -1,4 +0,0 @@
packages:
ble: !include ../../test_build_components/common/ble/esp32-c3-idf.yaml
<<: !include common.yaml
@@ -1,6 +0,0 @@
substitutions:
cs_pin: GPIO8
packages:
spi: !include ../../test_build_components/common/spi/esp32-c3-idf.yaml
<<: !include common.yaml
@@ -1,6 +0,0 @@
substitutions:
cs_pin: GPIO8
packages:
spi: !include ../../test_build_components/common/spi/esp32-c3-idf.yaml
<<: !include common.yaml
@@ -1,4 +0,0 @@
packages:
i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml
<<: !include common.yaml
@@ -1,4 +0,0 @@
packages:
ble: !include ../../test_build_components/common/ble/esp32-c3-idf.yaml
<<: !include common.yaml
@@ -1,4 +0,0 @@
packages:
ble: !include ../../test_build_components/common/ble/esp32-c3-idf.yaml
<<: !include common.yaml
@@ -1,4 +0,0 @@
packages:
i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml
<<: !include common.yaml
@@ -1,4 +0,0 @@
packages:
i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml
<<: !include common.yaml
@@ -1,4 +0,0 @@
packages:
uart_19200: !include ../../test_build_components/common/uart_19200/esp32-c3-idf.yaml
<<: !include common.yaml
@@ -1,5 +0,0 @@
substitutions:
packages:
uart: !include ../../test_build_components/common/uart/esp32-c3-idf.yaml
<<: !include common.yaml
+3 -3
View File
@@ -1,11 +1,11 @@
button:
- platform: bl0940
bl0940_id: test_id
bl0940_id: bl0940_test_id
name: Cal Reset
sensor:
- platform: bl0940
id: test_id
id: bl0940_test_id
voltage:
name: BL0940 Voltage
current:
@@ -22,7 +22,7 @@ sensor:
number:
- platform: bl0940
id: bl0940_number_id
bl0940_id: test_id
bl0940_id: bl0940_test_id
current_calibration:
name: Cal Current
min_value: -5
@@ -1,5 +0,0 @@
substitutions:
packages:
uart: !include ../../test_build_components/common/uart/esp32-c3-idf.yaml
<<: !include common.yaml
@@ -1,5 +0,0 @@
substitutions:
packages:
uart: !include ../../test_build_components/common/uart/esp32-c3-idf.yaml
<<: !include common.yaml
+22
View File
@@ -52,3 +52,25 @@ sensor:
name: "BLE Sensor without Lambda"
service_uuid: "abcd1234-abcd-1234-abcd-abcd12345678"
characteristic_uuid: "abcd1237-abcd-1234-abcd-abcd12345678"
number:
- platform: template
name: "Test Number"
id: test_number
optimistic: true
min_value: 0
max_value: 255
step: 1
button:
# Test ble_write with lambda that references a component (function pointer)
- platform: template
name: "BLE Write Lambda Test"
on_press:
- ble_client.ble_write:
id: test_blec
service_uuid: "abcd1234-abcd-1234-abcd-abcd12345678"
characteristic_uuid: "abcd1235-abcd-1234-abcd-abcd12345678"
value: !lambda |-
uint8_t val = (uint8_t)id(test_number).state;
return std::vector<uint8_t>{0xAA, val, 0xBB};
@@ -1,4 +0,0 @@
packages:
ble: !include ../../test_build_components/common/ble/esp32-c3-idf.yaml
<<: !include common.yaml
@@ -1,4 +0,0 @@
packages:
ble: !include ../../test_build_components/common/ble/esp32-c3-idf.yaml
<<: !include common.yaml
@@ -1,4 +0,0 @@
packages:
ble: !include ../../test_build_components/common/ble/esp32-c3-idf.yaml
<<: !include common.yaml
@@ -1,4 +0,0 @@
packages:
ble: !include ../../test_build_components/common/ble/esp32-c3-idf.yaml
<<: !include common.yaml
+10
View File
@@ -0,0 +1,10 @@
esphome:
on_boot:
- bm8563.read_time
- bm8563.write_time
- bm8563.start_timer:
duration: 300s
time:
- platform: bm8563
i2c_id: i2c_bus
@@ -0,0 +1,4 @@
packages:
i2c: !include ../../test_build_components/common/i2c/esp32-ard.yaml
<<: !include common.yaml
@@ -1,4 +0,0 @@
packages:
i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml
<<: !include common.yaml
@@ -1,6 +0,0 @@
substitutions:
cs_pin: GPIO8
packages:
spi: !include ../../test_build_components/common/spi/esp32-c3-idf.yaml
<<: !include common.yaml
@@ -1,4 +0,0 @@
packages:
i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml
<<: !include common.yaml
@@ -9,6 +9,7 @@ bme68x_bsec2_i2c:
sensor:
- platform: bme68x_bsec2
id: bme_sensor
temperature:
name: BME68X Temperature
pressure:
@@ -1,4 +0,0 @@
packages:
i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml
<<: !include common.yaml
@@ -1,4 +0,0 @@
packages:
i2c: !include ../../test_build_components/common/i2c/esp32-s3-idf.yaml
<<: !include common.yaml
@@ -1,4 +0,0 @@
packages:
i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml
<<: !include common.yaml
@@ -1,4 +0,0 @@
packages:
i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml
<<: !include common.yaml
@@ -1,4 +0,0 @@
packages:
i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml
<<: !include common.yaml
@@ -1,6 +0,0 @@
substitutions:
cs_pin: GPIO8
packages:
spi: !include ../../test_build_components/common/spi/esp32-c3-idf.yaml
<<: !include common.yaml
@@ -1,4 +0,0 @@
packages:
i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml
<<: !include common.yaml
@@ -1,6 +0,0 @@
substitutions:
cs_pin: GPIO8
packages:
spi: !include ../../test_build_components/common/spi/esp32-c3-idf.yaml
<<: !include common.yaml
@@ -1,4 +0,0 @@
packages:
i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml
<<: !include common.yaml
@@ -1,5 +1,5 @@
sensor:
- platform: bmp581
- platform: bmp581_i2c
i2c_id: i2c_bus
temperature:
name: BMP581 Temperature
@@ -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,5 +0,0 @@
substitutions:
clock_pin: GPIO5
data_pin: GPIO4
<<: !include common.yaml
@@ -1,5 +0,0 @@
substitutions:
clock_pin: GPIO5
data_pin: GPIO4
<<: !include common.yaml
@@ -0,0 +1,16 @@
esp32_ble_tracker:
sensor:
- platform: bthome_mithermometer
mac_address: A4:C1:38:4E:16:78
bindkey: eef418daf699a0c188f3bfd17e4565d9
temperature:
name: "BTHome Temperature"
humidity:
name: "BTHome Humidity"
battery_level:
name: "BTHome Battery"
battery_voltage:
name: "BTHome Battery Voltage"
signal_strength:
name: "BTHome Signal"
@@ -0,0 +1,4 @@
packages:
ble: !include ../../test_build_components/common/ble/esp32-idf.yaml
<<: !include common.yaml
@@ -1 +0,0 @@
!include common.yaml

Some files were not shown because too many files have changed in this diff Show More