mirror of
https://github.com/esphome/esphome.git
synced 2026-10-06 10:56:43 +00:00
Merge branch 'dev' into enhancement/ethernet-spi-interface
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
)
|
||||
@@ -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 |
@@ -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}"
|
||||
)
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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'
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
Reference in New Issue
Block a user