Merge remote-tracking branch 'upstream/logger-buffered-recursion-guard' into integration

This commit is contained in:
J. Nick Koston
2026-06-18 16:12:45 -05:00
664 changed files with 22117 additions and 5672 deletions
+38
View File
@@ -104,6 +104,44 @@ def set_component_config() -> Callable[[str, Any], None]:
return setter
@pytest.fixture
def choose_variant_with_pins() -> Generator[Callable[[list], None]]:
"""Set the ESP32 variant to the first one on which all the given pins are valid.
For ESP32 only, since the other platforms do not have variants. The core
configuration must already have been set up for an ESP32 target.
Using local imports to avoid importing when ESP32 is not the target.
"""
from esphome import config_validation as cv
from esphome.components.esp32 import KEY_ESP32, KEY_VARIANT, VARIANTS
from esphome.components.esp32.gpio import validate_gpio_pin
from esphome.const import CONF_INPUT, CONF_OUTPUT
from esphome.pins import gpio_pin_schema
def chooser(pins: list) -> None:
for variant in VARIANTS:
try:
CORE.data[KEY_ESP32][KEY_VARIANT] = variant
for pin in pins:
if pin is not None:
pin = gpio_pin_schema(
{
CONF_INPUT: True,
CONF_OUTPUT: True,
},
internal=True,
)(pin)
validate_gpio_pin(pin)
return
except cv.Invalid:
continue
raise cv.Invalid(
f"No compatible variant found for pins: {', '.join(map(str, pins))}"
)
yield chooser
@pytest.fixture
def component_fixture_path(request: pytest.FixtureRequest) -> Callable[[str], Path]:
"""Return a function to get absolute paths relative to the component's fixtures directory."""
@@ -4,77 +4,145 @@ from unittest.mock import patch
import pytest
from esphome.components.const import BYTE_ORDER_BIG, BYTE_ORDER_LITTLE
from esphome.components.display import (
DisplayMetaData,
add_metadata,
get_all_display_metadata,
get_display_metadata,
)
from esphome.cpp_generator import MockObj
from esphome.config import Config
from esphome.core import ID
from esphome.final_validate import full_config
def test_add_metadata_with_string_id():
"""Test adding metadata with a plain string ID."""
def test_add_metadata_basic():
"""Test adding metadata with an ID object."""
with patch("esphome.components.display.CORE.data", {}):
add_metadata("my_display", 320, 240, True)
meta = get_display_metadata("my_display")
add_metadata(ID("my_display"), 320, 240)
meta = get_display_metadata(ID("my_display"))
assert meta == DisplayMetaData(
width=320, height=240, has_writer=True, has_hardware_rotation=False
width=320,
height=240,
has_hardware_rotation=False,
byte_order=BYTE_ORDER_BIG,
)
def test_add_metadata_with_mockobj_id():
"""Test adding metadata with a MockObj ID (converted via str())."""
def test_add_metadata_with_all_fields():
"""Test adding metadata with all fields set."""
with patch("esphome.components.display.CORE.data", {}):
mock_id = MockObj("my_display_obj")
add_metadata(mock_id, 480, 320, False, has_hardware_rotation=True)
meta = get_display_metadata("my_display_obj")
add_metadata(
ID("my_display"),
480,
320,
has_hardware_rotation=True,
byte_order=BYTE_ORDER_LITTLE,
)
meta = get_display_metadata(ID("my_display"))
assert meta == DisplayMetaData(
width=480, height=320, has_writer=False, has_hardware_rotation=True
width=480,
height=320,
has_hardware_rotation=True,
byte_order=BYTE_ORDER_LITTLE,
)
def test_add_metadata_hardware_rotation_default():
"""Test that has_hardware_rotation defaults to False."""
with patch("esphome.components.display.CORE.data", {}):
add_metadata("disp", 128, 64, False)
meta = get_display_metadata("disp")
add_metadata(ID("disp"), 128, 64)
meta = get_display_metadata(ID("disp"))
assert meta.has_hardware_rotation is False
assert meta.byte_order == BYTE_ORDER_BIG
def test_get_display_metadata_missing_returns_none():
"""Test that querying a non-existent ID returns None."""
def test_add_metadata_with_byte_order():
"""Test adding metadata with explicit byte_order."""
with patch("esphome.components.display.CORE.data", {}):
data = get_display_metadata("no_such_display")
assert data.width == 0
assert data.height == 0
assert data.has_writer is False
add_metadata(ID("disp"), 240, 320, byte_order=BYTE_ORDER_LITTLE)
meta = get_display_metadata(ID("disp"))
assert meta.byte_order == BYTE_ORDER_LITTLE
def test_get_display_metadata_missing_reads_raw_config():
"""Querying a non-existent ID falls back to raw config lookup."""
with patch("esphome.components.display.CORE.data", {}):
# Set up a minimal full_config with a display entry so the fallback
# path in get_display_metadata can find the display config.
fc = Config()
fc["display"] = [
{
"id": ID("no_such_display", True),
"auto_clear_enabled": True,
"dimensions": {"width": 320, "height": 240},
"byte_order": BYTE_ORDER_LITTLE,
"rotation": 90,
},
{
"id": ID("other_display", True),
"auto_clear_enabled": "undefined",
"dimensions": (1024, 600),
},
]
fc.declare_ids.append((ID("no_such_display", True), ["display", 0, "id"]))
fc.declare_ids.append((ID("other_display", True), ["display", 1, "id"]))
full_config.set(fc)
data = get_display_metadata(ID("no_such_display"))
assert data.width == 320
assert data.height == 240
assert data.has_hardware_rotation is False
assert data.has_writer is True
assert data.byte_order == BYTE_ORDER_LITTLE
assert data.rotation == 90
data = get_display_metadata(ID("other_display"))
assert data.width == 1024
assert data.height == 600
assert data.has_writer is False
def test_add_multiple_displays():
"""Test adding metadata for multiple displays."""
with patch("esphome.components.display.CORE.data", {}):
add_metadata("disp_a", 320, 240, True)
add_metadata("disp_b", 128, 64, False, has_hardware_rotation=True)
add_metadata(ID("disp_a"), 320, 240)
add_metadata(ID("disp_b"), 128, 64, has_hardware_rotation=True)
all_meta = get_all_display_metadata()
assert len(all_meta) == 2
assert all_meta["disp_a"] == DisplayMetaData(320, 240, True, False)
assert all_meta["disp_b"] == DisplayMetaData(128, 64, False, True)
assert all_meta["disp_a"] == DisplayMetaData(320, 240, False)
assert all_meta["disp_b"] == DisplayMetaData(128, 64, True, BYTE_ORDER_BIG)
def test_add_metadata_overwrites_existing():
"""Test that adding metadata for the same ID overwrites the previous entry."""
def test_add_duplicate_id_asserts():
"""Adding metadata for the same ID object twice should assert."""
with patch("esphome.components.display.CORE.data", {}):
add_metadata("disp", 320, 240, True)
add_metadata("disp", 640, 480, False, has_hardware_rotation=True)
meta = get_display_metadata("disp")
assert meta == DisplayMetaData(640, 480, False, True)
id_obj = ID("disp")
add_metadata(id_obj, 320, 240)
with pytest.raises(AssertionError, match="Duplicate"):
add_metadata(id_obj, 640, 480)
def test_metadata_is_frozen():
"""Test that DisplayMetaData instances are immutable (frozen dataclass)."""
meta = DisplayMetaData(320, 240, True, False)
meta = DisplayMetaData(320, 240, False, BYTE_ORDER_BIG)
with pytest.raises(AttributeError):
meta.width = 640
with pytest.raises(AttributeError):
meta.byte_order = BYTE_ORDER_LITTLE
def test_get_all_metadata_asserts_on_unresolved_id():
"""get_all_display_metadata should assert if any ID has id=None."""
with patch("esphome.components.display.CORE.data", {}):
add_metadata(ID(None), 320, 240)
with pytest.raises(AssertionError, match="resolved"):
get_all_display_metadata()
def test_get_metadata_asserts_on_unresolved_id():
"""get_display_metadata should assert if any ID has id=None."""
with patch("esphome.components.display.CORE.data", {}):
add_metadata(ID(None), 320, 240)
with pytest.raises(AssertionError, match="resolved"):
get_display_metadata(ID("anything"))
@@ -0,0 +1,24 @@
esphome:
name: test
esp32:
board: esp32dev
spi:
clk_pin: GPIO18
mosi_pin: GPIO19
display:
- platform: epaper_spi
id: epaper_display
model: ssd1677
dc_pin: GPIO21
busy_pin: GPIO22
reset_pin: GPIO23
cs_pin: GPIO5
enable_pin:
- GPIO25
- GPIO26
dimensions:
width: 200
height: 200
@@ -0,0 +1,156 @@
"""Tests for display metadata created by the epaper_spi component."""
from collections.abc import Callable
from pathlib import Path
from typing import Any
from esphome import config_validation as cv
from esphome.components.display import get_all_display_metadata, get_display_metadata
from esphome.components.epaper_spi.display import CONFIG_SCHEMA
from esphome.components.esp32 import KEY_BOARD, KEY_VARIANT, VARIANT_ESP32
from esphome.const import PlatformFramework
from esphome.types import ConfigType
from tests.component_tests.types import SetCoreConfigCallable
def _base_config(**overrides: Any) -> ConfigType:
"""Build a minimal valid ssd1677 config, allowing field overrides."""
config: ConfigType = {
"id": "test_display",
"model": "ssd1677",
"dc_pin": 21,
"busy_pin": 22,
"reset_pin": 23,
"cs_pin": 5,
"dimensions": {"width": 200, "height": 300},
}
config.update(overrides)
return config
def test_metadata_dimensions_and_defaults(
set_core_config: SetCoreConfigCallable,
set_component_config: Callable[[str, Any], None],
) -> None:
"""Metadata picks up explicit dimensions and epaper_spi defaults."""
set_core_config(
PlatformFramework.ESP32_IDF,
platform_data={KEY_BOARD: "esp32dev", KEY_VARIANT: VARIANT_ESP32},
)
set_component_config("spi", {"id": "spi_bus", "clk_pin": 18, "mosi_pin": 19})
config = CONFIG_SCHEMA(_base_config())
meta = get_display_metadata(config["id"])
assert meta is not None
assert meta.width == 200
assert meta.height == 300
# epaper_spi always reports full hardware rotation
assert meta.has_hardware_rotation is True
# epaper_spi does not declare a byte order
assert meta.byte_order is cv.UNDEFINED
assert meta.draw_rounding == 0
# no drawing methods configured -> no writer
assert meta.has_writer is False
def test_metadata_default_dimensions_from_model(
set_core_config: SetCoreConfigCallable,
set_component_config: Callable[[str, Any], None],
) -> None:
"""A model with built-in dimensions reports those without explicit dimensions."""
set_core_config(
PlatformFramework.ESP32_IDF,
platform_data={KEY_BOARD: "esp32dev", KEY_VARIANT: VARIANT_ESP32},
)
set_component_config("spi", {"id": "spi_bus", "clk_pin": 18, "mosi_pin": 19})
# waveshare-4.26in is an ssd1677 derivative with default 800x480 dimensions
config = CONFIG_SCHEMA(
{
"id": "wave_display",
"model": "waveshare-4.26in",
"dc_pin": 21,
"busy_pin": 22,
"reset_pin": 23,
"cs_pin": 5,
}
)
meta = get_display_metadata(config["id"])
assert meta is not None
assert meta.width == 800
assert meta.height == 480
def test_metadata_has_writer_with_auto_clear(
set_core_config: SetCoreConfigCallable,
set_component_config: Callable[[str, Any], None],
) -> None:
"""A display with auto_clear_enabled reports has_writer=True."""
set_core_config(
PlatformFramework.ESP32_IDF,
platform_data={KEY_BOARD: "esp32dev", KEY_VARIANT: VARIANT_ESP32},
)
set_component_config("spi", {"id": "spi_bus", "clk_pin": 18, "mosi_pin": 19})
config = CONFIG_SCHEMA(_base_config(auto_clear_enabled=True))
meta = get_display_metadata(config["id"])
assert meta is not None
assert meta.has_writer is True
def test_metadata_rotation_propagated(
set_core_config: SetCoreConfigCallable,
set_component_config: Callable[[str, Any], None],
) -> None:
"""The configured rotation is stored in the metadata."""
set_core_config(
PlatformFramework.ESP32_IDF,
platform_data={KEY_BOARD: "esp32dev", KEY_VARIANT: VARIANT_ESP32},
)
set_component_config("spi", {"id": "spi_bus", "clk_pin": 18, "mosi_pin": 19})
config = CONFIG_SCHEMA(_base_config(rotation=90))
meta = get_display_metadata(config["id"])
assert meta is not None
assert meta.rotation == 90
def test_metadata_multiple_displays_independent(
set_core_config: SetCoreConfigCallable,
set_component_config: Callable[[str, Any], None],
) -> None:
"""Each display gets its own independent metadata entry."""
set_core_config(
PlatformFramework.ESP32_IDF,
platform_data={KEY_BOARD: "esp32dev", KEY_VARIANT: VARIANT_ESP32},
)
set_component_config("spi", {"id": "spi_bus", "clk_pin": 18, "mosi_pin": 19})
CONFIG_SCHEMA(_base_config(id="disp_a", dimensions={"width": 200, "height": 300}))
CONFIG_SCHEMA(_base_config(id="disp_b", dimensions={"width": 400, "height": 480}))
all_meta = get_all_display_metadata()
assert all_meta["disp_a"].width == 200
assert all_meta["disp_a"].height == 300
assert all_meta["disp_b"].width == 400
assert all_meta["disp_b"].height == 480
def test_metadata_via_code_generation(
generate_main: Callable[[str | Path], str],
component_config_path: Callable[[str], Path],
) -> None:
"""Full code generation registers metadata for the configured display."""
generate_main(component_config_path("enable_pin_test.yaml"))
all_meta = get_all_display_metadata()
assert len(all_meta) == 1
meta = next(iter(all_meta.values()))
# enable_pin_test.yaml: ssd1677 at 200x200
assert meta.width == 200
assert meta.height == 200
assert meta.has_hardware_rotation is True
+151 -39
View File
@@ -1,6 +1,8 @@
"""Tests for epaper_spi configuration validation."""
from collections.abc import Callable
from pathlib import Path
import re
from typing import Any
import pytest
@@ -11,17 +13,13 @@ from esphome.components.epaper_spi.display import (
FINAL_VALIDATE_SCHEMA,
MODELS,
)
from esphome.components.esp32 import (
KEY_BOARD,
KEY_VARIANT,
VARIANT_ESP32,
VARIANT_ESP32S3,
)
from esphome.components.esp32 import KEY_BOARD, KEY_VARIANT, VARIANT_ESP32
from esphome.const import (
CONF_BUSY_PIN,
CONF_CS_PIN,
CONF_DC_PIN,
CONF_DIMENSIONS,
CONF_ENABLE_PIN,
CONF_HEIGHT,
CONF_INIT_SEQUENCE,
CONF_RESET_PIN,
@@ -31,6 +29,30 @@ from esphome.const import (
from esphome.types import ConfigType
from tests.component_tests.types import SetCoreConfigCallable
# Pin options whose values must be valid on the chosen ESP32 variant.
_PIN_CONF_KEYS = (
CONF_CS_PIN,
CONF_DC_PIN,
CONF_RESET_PIN,
CONF_BUSY_PIN,
CONF_ENABLE_PIN,
)
def _pins_for(model: Any, config: ConfigType) -> list:
"""Collect every GPIO the config will actually use (model defaults or injected)."""
pins: list = []
for key in _PIN_CONF_KEYS:
# An injected value in the config takes precedence over the model default.
value = config[key] if key in config else model.get_default(key)
if not value: # get_default returns False for pins the model omits
continue
if isinstance(value, list):
pins.extend(value)
else:
pins.append(value)
return pins
def run_schema_validation(
config: ConfigType, with_final_validate: bool = False
@@ -90,29 +112,20 @@ def test_basic_configuration_errors(
def test_all_predefined_models(
set_core_config: SetCoreConfigCallable,
set_component_config: Callable[[str, Any], None],
choose_variant_with_pins: Callable[[list], None],
) -> None:
"""Test all predefined epaper models validate successfully with appropriate defaults."""
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})
# 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
@@ -141,6 +154,10 @@ def test_all_predefined_models(
if not model.get_default(CONF_CS_PIN):
config[CONF_CS_PIN] = 5
# Select an ESP32 variant on which all of this model's pins are valid
# (some models default to high-numbered pins only present on the S3).
choose_variant_with_pins(_pins_for(model, config))
run_schema_validation(config)
@@ -152,27 +169,19 @@ def test_individual_models(
model_name: str,
set_core_config: SetCoreConfigCallable,
set_component_config: Callable[[str, Any], None],
choose_variant_with_pins: Callable[[list], 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},
)
model = MODELS[model_name]
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
@@ -195,6 +204,10 @@ def test_individual_models(
if not model.get_default(CONF_CS_PIN):
config[CONF_CS_PIN] = 5
# Select an ESP32 variant on which all of this model's pins are valid
# (some models default to high-numbered pins only present on the S3).
choose_variant_with_pins(_pins_for(model, config))
# This should not raise any exceptions
run_schema_validation(config)
@@ -342,3 +355,102 @@ def test_busy_pin_input_mode_ssd1677(
reset_pin_config = result[CONF_RESET_PIN]
assert "mode" in reset_pin_config
assert reset_pin_config["mode"]["output"] is True
def test_enable_pin_single(
set_core_config: SetCoreConfigCallable,
set_component_config: Callable[[str, Any], None],
) -> None:
"""Test that a single enable_pin is accepted and normalised to a list of output pins."""
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})
result = run_schema_validation(
{
"id": "test_display",
"model": "ssd1677",
"dc_pin": 21,
"busy_pin": 22,
"reset_pin": 23,
"cs_pin": 5,
"enable_pin": 25,
"dimensions": {
"width": 200,
"height": 200,
},
}
)
# A single pin is normalised to a list by cv.ensure_list
assert CONF_ENABLE_PIN in result
enable_pins = result[CONF_ENABLE_PIN]
assert isinstance(enable_pins, list)
assert len(enable_pins) == 1
# enable pins are configured as outputs
assert enable_pins[0]["mode"]["output"] is True
def test_enable_pin_multiple(
set_core_config: SetCoreConfigCallable,
set_component_config: Callable[[str, Any], None],
) -> None:
"""Test that a list of enable_pins is accepted."""
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})
result = run_schema_validation(
{
"id": "test_display",
"model": "ssd1677",
"dc_pin": 21,
"busy_pin": 22,
"reset_pin": 23,
"cs_pin": 5,
"enable_pin": [25, 26],
"dimensions": {
"width": 200,
"height": 200,
},
}
)
assert CONF_ENABLE_PIN in result
enable_pins = result[CONF_ENABLE_PIN]
assert isinstance(enable_pins, list)
assert len(enable_pins) == 2
assert all(pin["mode"]["output"] is True for pin in enable_pins)
def test_enable_pin_code_generation(
generate_main: Callable[[str | Path], str],
component_config_path: Callable[[str], Path],
) -> None:
"""Test that enable_pins are wired up in the generated C++ code."""
main_cpp = generate_main(component_config_path("enable_pin_test.yaml"))
# Derive the auto-generated pin variable names from the set_pin() lines
# rather than hard-coding them, so the test does not break when unrelated
# codegen details shift the generated IDs.
def pin_var_for(gpio_num: int) -> str:
match = re.search(rf"(\w+)->set_pin\(::GPIO_NUM_{gpio_num}\);", main_cpp)
assert match is not None, (
f"GPIO_NUM_{gpio_num} pin not set up in generated code"
)
return match.group(1)
pin_25 = pin_var_for(25)
pin_26 = pin_var_for(26)
# Both pin objects must be passed to the display via set_enable_pins() as a
# std::vector initializer list, in the configured order.
assert f"set_enable_pins({{{pin_25}, {pin_26}}});" in main_cpp
@@ -0,0 +1,7 @@
esphome:
name: test
esp32:
board: esp32dev
framework:
type: esp-idf
@@ -0,0 +1,10 @@
esphome:
name: test
esp32:
board: esp32dev
flash_mode: qio
flash_frequency: 80MHz
toolchain: platformio
framework:
type: esp-idf
@@ -0,0 +1,17 @@
esphome:
name: test
esp32:
board: esp32dev
framework:
type: esp-idf
ethernet:
type: W5500
clk_pin: 19
mosi_pin: 21
miso_pin: 23
cs_pin: 18
interrupt_pin: 36
reset_pin: 22
clock_speed: 10Mhz
@@ -0,0 +1,14 @@
esphome:
name: test
esp32:
board: esp32dev
framework:
type: esp-idf
wifi:
ssid: "test_ssid"
password: "test_password"
esp32_ble_tracker:
software_coexistence: true
@@ -0,0 +1,11 @@
esphome:
name: test
esp32:
board: esp32dev
framework:
type: esp-idf
wifi:
ssid: "test_ssid"
password: "test_password"
+266 -3
View File
@@ -2,14 +2,25 @@
Test ESP32 configuration
"""
import asyncio
from collections.abc import Callable
from pathlib import Path
from typing import Any
import pytest
from esphome.components.esp32 import VARIANT_ESP32, VARIANTS
from esphome.components.esp32.const import KEY_ESP32, KEY_SDKCONFIG_OPTIONS, KEY_VARIANT
from esphome.components.esp32 import (
VARIANT_ESP32,
VARIANTS,
NetworkSdkconfigData,
_reconcile_network_sdkconfig,
)
from esphome.components.esp32.const import (
KEY_ESP32,
KEY_NETWORK_SDKCONFIG,
KEY_SDKCONFIG_OPTIONS,
KEY_VARIANT,
)
from esphome.components.esp32.gpio import validate_gpio_pin
import esphome.config_validation as cv
from esphome.const import (
@@ -45,16 +56,57 @@ def test_esp32_config(
config = CONFIG_SCHEMA(config)
assert config["variant"] == VARIANT_ESP32
# Check that defining a variant sets the board name correctly
# Check that defining a variant sets the board name correctly.
# Run under the ESP-IDF toolchain so variants without an entry in
# STANDARD_BOARDS (S31, H4, H21) still derive a board name from
# VARIANT_FRIENDLY rather than failing with cv.Invalid. CORE.toolchain
# gets pinned by the first CONFIG_SCHEMA() call above (via
# _resolve_toolchain) and that pinned value wins over the dict's
# CONF_TOOLCHAIN, so clear it between iterations to mirror a fresh
# config run.
for variant in VARIANTS:
CORE.toolchain = None
config = CONFIG_SCHEMA(
{
"variant": variant,
"toolchain": Toolchain.ESP_IDF.value,
}
)
assert VARIANT_FRIENDLY[variant].lower() in config["board"]
@pytest.mark.parametrize(
("config_toolchain", "expected"),
[
# No `toolchain:` set -> the new default for esp32.
(None, Toolchain.ESP_IDF),
# An explicit `toolchain:` still wins over the default.
(Toolchain.PLATFORMIO.value, Toolchain.PLATFORMIO),
(Toolchain.ESP_IDF.value, Toolchain.ESP_IDF),
],
)
def test_esp32_default_toolchain_is_esp_idf(
set_core_config: SetCoreConfigCallable,
config_toolchain: str | None,
expected: Toolchain,
) -> None:
"""With no `toolchain:` set (and nothing pinned via the CLI), esp32 resolves
to the ESP-IDF toolchain; an explicit `toolchain:` still wins."""
set_core_config(PlatformFramework.ESP32_IDF)
from esphome.components.esp32 import CONFIG_SCHEMA
# Fresh run: no --toolchain CLI and no prior config pinned CORE.toolchain.
CORE.toolchain = None
config: dict[str, Any] = {"variant": VARIANT_ESP32}
if config_toolchain is not None:
config["toolchain"] = config_toolchain
CONFIG_SCHEMA(config)
assert CORE.toolchain == expected
@pytest.mark.parametrize(
("config", "error_match"),
[
@@ -73,6 +125,11 @@ def test_esp32_config(
r"Option 'variant' does not match selected board. @ data\['variant'\]",
id="mismatched_board_variant_config",
),
pytest.param(
{"variant": "esp32s31", "toolchain": Toolchain.PLATFORMIO.value},
r"No default board is known for ESP32S31\. Please specify the `board:` option explicitly\. @ data\['variant'\]",
id="variant_without_default_board_requires_explicit_board_under_platformio",
),
pytest.param(
{
"variant": "esp32s2",
@@ -271,3 +328,209 @@ def test_native_idf_enables_reproducible_build(
sdkconfig = CORE.data[KEY_ESP32][KEY_SDKCONFIG_OPTIONS]
assert sdkconfig.get("CONFIG_APP_REPRODUCIBLE_BUILD") is True
def test_flash_mode_sets_sdkconfig_and_pio_option(
generate_main: Callable[[str | Path], str],
component_config_path: Callable[[str], Path],
) -> None:
"""flash_mode/flash_frequency select the esptool flash parameters on both backends."""
generate_main(component_config_path("flash_mode_idf.yaml"))
sdkconfig = CORE.data[KEY_ESP32][KEY_SDKCONFIG_OPTIONS]
assert sdkconfig.get("CONFIG_ESPTOOLPY_FLASHMODE_QIO") is True
assert sdkconfig.get("CONFIG_ESPTOOLPY_FLASHFREQ_80M") is True
assert CORE.platformio_options.get("board_build.flash_mode") == "qio"
assert CORE.platformio_options.get("board_build.f_flash") == "80000000L"
def test_flash_mode_unset_leaves_defaults(
generate_main: Callable[[str | Path], str],
component_config_path: Callable[[str], Path],
) -> None:
"""Without flash_mode the board/sdkconfig defaults stay untouched."""
generate_main(component_config_path("flash_mode_default.yaml"))
sdkconfig = CORE.data[KEY_ESP32][KEY_SDKCONFIG_OPTIONS]
assert not any(key.startswith("CONFIG_ESPTOOLPY_FLASHMODE_") for key in sdkconfig)
assert not any(key.startswith("CONFIG_ESPTOOLPY_FLASHFREQ_") for key in sdkconfig)
assert "board_build.flash_mode" not in CORE.platformio_options
assert "board_build.f_flash" not in CORE.platformio_options
@pytest.mark.parametrize(
("framework", "net", "preset", "expected"),
[
# --- IDF: single-interface cases (must match pre-refactor behavior) ---
pytest.param(
PlatformFramework.ESP32_IDF,
NetworkSdkconfigData(wifi=True),
{},
{
"CONFIG_ESP_WIFI_SOFTAP_SUPPORT": False,
"CONFIG_LWIP_DHCPS": False,
},
id="idf_wifi_no_ap",
),
pytest.param(
PlatformFramework.ESP32_IDF,
NetworkSdkconfigData(wifi=True, wifi_ap=True),
{},
{},
id="idf_wifi_ap_leaves_softap_dhcps",
),
pytest.param(
PlatformFramework.ESP32_IDF,
NetworkSdkconfigData(ethernet=True),
{},
{
"CONFIG_ESP_WIFI_ENABLED": False,
"CONFIG_SW_COEXIST_ENABLE": False,
},
id="idf_ethernet_only",
),
pytest.param(
PlatformFramework.ESP32_IDF,
NetworkSdkconfigData(
wifi=True, bluetooth=True, ble_42=True, software_coexistence=True
),
{},
{
"CONFIG_BT_ENABLED": True,
"CONFIG_BT_BLE_42_FEATURES_SUPPORTED": True,
"CONFIG_SW_COEXIST_ENABLE": True,
"CONFIG_ESP_WIFI_SOFTAP_SUPPORT": False,
"CONFIG_LWIP_DHCPS": False,
},
id="idf_wifi_ble_tracker_coexistence",
),
pytest.param(
PlatformFramework.ESP32_IDF,
NetworkSdkconfigData(bluetooth=True),
{},
{"CONFIG_BT_ENABLED": True},
id="idf_ble_server_only_no_ble42",
),
# --- IDF: user sdkconfig_options always win ---
pytest.param(
PlatformFramework.ESP32_IDF,
NetworkSdkconfigData(wifi=True),
{"CONFIG_ESP_WIFI_SOFTAP_SUPPORT": True},
{
"CONFIG_ESP_WIFI_SOFTAP_SUPPORT": True,
"CONFIG_LWIP_DHCPS": False,
},
id="idf_user_override_wins",
),
# --- IDF: user advanced enable_lwip_dhcp_server: false, even with AP ---
pytest.param(
PlatformFramework.ESP32_IDF,
NetworkSdkconfigData(
wifi=True, wifi_ap=True, enable_lwip_dhcp_server=False
),
{},
{"CONFIG_LWIP_DHCPS": False},
id="idf_user_disables_dhcps_with_ap",
),
# --- IDF: WiFi + Ethernet coexist (the multi-interface unlock) ---
pytest.param(
PlatformFramework.ESP32_IDF,
NetworkSdkconfigData(wifi=True, ethernet=True),
{},
{
"CONFIG_ESP_WIFI_SOFTAP_SUPPORT": False,
"CONFIG_LWIP_DHCPS": False,
},
id="idf_wifi_and_ethernet_keeps_wifi_enabled",
),
# --- Arduino: SoftAP/DHCPS disable is IDF-only ---
pytest.param(
PlatformFramework.ESP32_ARDUINO,
NetworkSdkconfigData(wifi=True),
{},
{},
id="arduino_wifi_no_ap_untouched",
),
pytest.param(
PlatformFramework.ESP32_ARDUINO,
NetworkSdkconfigData(ethernet=True),
{},
{
"CONFIG_ESP_WIFI_ENABLED": False,
"CONFIG_SW_COEXIST_ENABLE": False,
},
id="arduino_ethernet_only_disables_wifi",
),
# --- Arduino + Ethernet: DHCPS stays available even if user disabled it ---
pytest.param(
PlatformFramework.ESP32_ARDUINO,
NetworkSdkconfigData(ethernet=True, enable_lwip_dhcp_server=False),
{},
{
"CONFIG_ESP_WIFI_ENABLED": False,
"CONFIG_SW_COEXIST_ENABLE": False,
},
id="arduino_ethernet_dhcps_exclusion",
),
],
)
def test_reconcile_network_sdkconfig(
set_core_config: SetCoreConfigCallable,
framework: PlatformFramework,
net: NetworkSdkconfigData,
preset: dict[str, Any],
expected: dict[str, Any],
) -> None:
"""The FINAL-priority reconciler resolves WiFi/Ethernet/Bluetooth/coexistence
sdkconfig flags from the requests recorded in NetworkSdkconfigData."""
set_core_config(framework)
CORE.data[KEY_ESP32] = {
KEY_SDKCONFIG_OPTIONS: dict(preset),
KEY_NETWORK_SDKCONFIG: net,
}
asyncio.run(_reconcile_network_sdkconfig())
assert CORE.data[KEY_ESP32][KEY_SDKCONFIG_OPTIONS] == expected
def test_network_wifi_only_reconciles_end_to_end(
generate_main: Callable[[str | Path], str],
component_config_path: Callable[[str], Path],
) -> None:
"""End-to-end: codegen for an ESP-IDF WiFi (no AP) config runs the reconciler
after wifi's request_wifi(), disabling SoftAP support and the DHCP server."""
generate_main(component_config_path("network_wifi_only.yaml"))
sdkconfig = CORE.data[KEY_ESP32][KEY_SDKCONFIG_OPTIONS]
assert sdkconfig.get("CONFIG_ESP_WIFI_SOFTAP_SUPPORT") is False
assert sdkconfig.get("CONFIG_LWIP_DHCPS") is False
# WiFi stack stays enabled (no ethernet) and no Bluetooth requested.
assert "CONFIG_ESP_WIFI_ENABLED" not in sdkconfig
assert "CONFIG_BT_ENABLED" not in sdkconfig
def test_network_ethernet_only_reconciles_end_to_end(
generate_main: Callable[[str | Path], str],
component_config_path: Callable[[str], Path],
) -> None:
"""End-to-end: ethernet's request_ethernet() makes the reconciler disable the
WiFi stack and coexistence when WiFi is absent."""
generate_main(component_config_path("network_ethernet_only.yaml"))
sdkconfig = CORE.data[KEY_ESP32][KEY_SDKCONFIG_OPTIONS]
assert sdkconfig.get("CONFIG_ESP_WIFI_ENABLED") is False
assert sdkconfig.get("CONFIG_SW_COEXIST_ENABLE") is False
def test_network_wifi_ble_coexistence_reconciles_end_to_end(
generate_main: Callable[[str | Path], str],
component_config_path: Callable[[str], Path],
) -> None:
"""End-to-end: WiFi + esp32_ble_tracker software_coexistence resolves to
BT enabled and coexistence on, with SoftAP/DHCP server dropped (no AP)."""
generate_main(component_config_path("network_wifi_ble_coexistence.yaml"))
sdkconfig = CORE.data[KEY_ESP32][KEY_SDKCONFIG_OPTIONS]
assert sdkconfig.get("CONFIG_BT_ENABLED") is True
assert sdkconfig.get("CONFIG_BT_BLE_42_FEATURES_SUPPORTED") is True
assert sdkconfig.get("CONFIG_SW_COEXIST_ENABLE") is True
assert sdkconfig.get("CONFIG_ESP_WIFI_SOFTAP_SUPPORT") is False
assert sdkconfig.get("CONFIG_LWIP_DHCPS") is False
# WiFi present alongside BT -> WiFi stack must stay enabled.
assert "CONFIG_ESP_WIFI_ENABLED" not in sdkconfig
@@ -0,0 +1,47 @@
"""Tests for esp32_ble_server configuration helpers."""
import pytest
from esphome.components.esp32_ble_server import (
CCCD_DESCRIPTOR_UUID,
CUD_DESCRIPTOR_UUID,
DEVICE_INFORMATION_SERVICE_UUID,
uuid_is,
)
@pytest.mark.parametrize(
"uuid",
[
DEVICE_INFORMATION_SERVICE_UUID, # int form (cv.hex_uint32_t)
"180A", # 16 bit short form (bt_uuid)
"180a", # lowercase is normalized by bt_uuid but guard anyway
"0000180A", # 32 bit form
"0000180A-0000-1000-8000-00805F9B34FB", # full 128 bit form
],
)
def test_uuid_is_matches_all_representations(uuid) -> None:
"""All representations of the same 16 bit UUID must compare equal."""
assert uuid_is(uuid, DEVICE_INFORMATION_SERVICE_UUID)
@pytest.mark.parametrize(
"uuid",
[
0x1818, # Cycling Power Service (different int)
"1818", # different 16 bit short form
"0000180B", # adjacent UUID
"0000180A-0000-1000-8000-00805F9B34FC", # wrong base UUID suffix
],
)
def test_uuid_is_rejects_other_uuids(uuid) -> None:
"""A different UUID must not be mistaken for the device information service."""
assert not uuid_is(uuid, DEVICE_INFORMATION_SERVICE_UUID)
@pytest.mark.parametrize("uuid16", [CUD_DESCRIPTOR_UUID, CCCD_DESCRIPTOR_UUID])
def test_uuid_is_matches_descriptor_short_strings(uuid16) -> None:
"""Reserved descriptor UUIDs match whether given as int or short string."""
assert uuid_is(uuid16, uuid16)
assert uuid_is(f"{uuid16:04X}", uuid16)
assert uuid_is(f"{uuid16:08X}", uuid16)
@@ -0,0 +1,177 @@
"""Tests for LVGL final_validation display metadata checks."""
from __future__ import annotations
import pytest
from esphome.components.const import BYTE_ORDER_BIG, BYTE_ORDER_LITTLE, CONF_BYTE_ORDER
from esphome.components.display import add_metadata
from esphome.components.lvgl import final_validation
from esphome.config import Config
from esphome.config_validation import Invalid
from esphome.const import KEY_CORE, KEY_TARGET_FRAMEWORK, KEY_TARGET_PLATFORM
from esphome.core import CORE, ID
from esphome.final_validate import full_config
@pytest.fixture(autouse=True)
def _setup_core():
"""Ensure CORE.data has enough context for final_validation."""
CORE.data[KEY_CORE] = {
KEY_TARGET_PLATFORM: "host",
KEY_TARGET_FRAMEWORK: "",
}
full_config.set(Config())
yield
CORE.reset()
def _register_displays(*display_ids: str) -> None:
"""Register display IDs in full_config so get_path_for_id works."""
fc = full_config.get()
display_list = [{"id": ID(d, True)} for d in display_ids]
fc["display"] = display_list
for i, disp_id in enumerate(display_ids):
fc.declare_ids.append((ID(disp_id, True), ["display", i, "id"]))
def _make_lvgl_config(
display_ids: list[str],
byte_order: str | None = None,
) -> dict:
"""Build a minimal LVGL config dict for final_validation."""
_register_displays(*display_ids)
config = {
"displays": [ID(d, True) for d in display_ids],
"log_level": "WARN",
"color_depth": 16,
"transparency_key": 0x000400,
"draw_rounding": 2,
"buffer_size": 0,
}
if byte_order is not None:
config[CONF_BYTE_ORDER] = byte_order
return config
class TestByteOrderAutoConfig:
"""Test that LVGL auto-configures byte_order from display metadata."""
def test_inherits_big_endian_from_display(self) -> None:
"""LVGL should inherit big_endian from display metadata."""
add_metadata(ID("my_disp"), 320, 240, byte_order=BYTE_ORDER_BIG)
configs = [_make_lvgl_config(["my_disp"])]
final_validation(configs)
assert configs[0][CONF_BYTE_ORDER] == BYTE_ORDER_BIG
def test_inherits_little_endian_from_display(self) -> None:
"""LVGL should inherit little_endian from display metadata."""
add_metadata(ID("my_disp"), 320, 240, byte_order=BYTE_ORDER_LITTLE)
configs = [_make_lvgl_config(["my_disp"])]
final_validation(configs)
assert configs[0][CONF_BYTE_ORDER] == BYTE_ORDER_LITTLE
def test_defaults_to_big_endian_when_no_metadata(self) -> None:
"""LVGL should default to big_endian when display has no metadata."""
configs = [_make_lvgl_config(["my_disp"])]
final_validation(configs)
assert configs[0][CONF_BYTE_ORDER] == BYTE_ORDER_BIG
class TestByteOrderExplicitMismatchError:
"""Test that LVGL rejects explicit byte_order mismatch with display."""
def test_raises_on_mismatch(self) -> None:
"""Explicit LVGL byte_order different from display should raise."""
add_metadata(ID("my_disp"), 320, 240, byte_order=BYTE_ORDER_LITTLE)
configs = [_make_lvgl_config(["my_disp"], byte_order=BYTE_ORDER_BIG)]
with pytest.raises(
Invalid, match="LVGL byte order must match the display byte order"
):
final_validation(configs)
def test_no_error_when_matching(self) -> None:
"""Explicit LVGL byte_order matching display should pass."""
add_metadata(ID("my_disp"), 320, 240, byte_order=BYTE_ORDER_BIG)
configs = [_make_lvgl_config(["my_disp"], byte_order=BYTE_ORDER_BIG)]
final_validation(configs)
class TestByteOrderMultipleDisplays:
"""Test byte_order validation with multiple displays."""
def test_consistent_displays_inherit(self) -> None:
"""All displays with same byte_order should set LVGL byte_order."""
add_metadata(ID("disp_a"), 320, 240, byte_order=BYTE_ORDER_LITTLE)
add_metadata(ID("disp_b"), 128, 64, byte_order=BYTE_ORDER_LITTLE)
configs = [_make_lvgl_config(["disp_a", "disp_b"])]
final_validation(configs)
assert configs[0][CONF_BYTE_ORDER] == BYTE_ORDER_LITTLE
def test_inconsistent_displays_raises(self) -> None:
"""Displays with different byte_order should raise an error."""
add_metadata(ID("disp_a"), 320, 240, byte_order=BYTE_ORDER_BIG)
add_metadata(ID("disp_b"), 128, 64, byte_order=BYTE_ORDER_LITTLE)
configs = [_make_lvgl_config(["disp_a", "disp_b"])]
with pytest.raises(Invalid, match="same byte_order"):
final_validation(configs)
class TestHasWriterCheck:
"""Test that LVGL rejects displays with has_writer set."""
def test_display_with_writer_raises(self) -> None:
"""Display with lambda/pages/auto_clear should be rejected."""
add_metadata(ID("my_disp"), 320, 240, has_writer=True)
configs = [_make_lvgl_config(["my_disp"])]
with pytest.raises(Invalid, match="not compatible with LVGL"):
final_validation(configs)
def test_display_without_writer_passes(self) -> None:
"""Display without writer should pass."""
add_metadata(ID("my_disp"), 320, 240, has_writer=False)
configs = [_make_lvgl_config(["my_disp"])]
final_validation(configs)
class TestRotationCheck:
"""Test that LVGL rejects displays with non-zero rotation."""
def test_display_with_rotation_raises(self) -> None:
"""Display with rotation should be rejected."""
add_metadata(ID("my_disp"), 320, 240, rotation=90)
configs = [_make_lvgl_config(["my_disp"])]
with pytest.raises(Invalid, match="rotation.*not compatible with LVGL"):
final_validation(configs)
def test_display_without_rotation_passes(self) -> None:
"""Display with rotation=0 should pass."""
add_metadata(ID("my_disp"), 320, 240, rotation=0)
configs = [_make_lvgl_config(["my_disp"])]
final_validation(configs)
class TestDrawRoundingMerge:
"""Test that display draw_rounding is merged into LVGL config."""
def test_display_draw_rounding_overrides_lower(self) -> None:
"""Display draw_rounding higher than LVGL default should win."""
add_metadata(ID("my_disp"), 320, 240, draw_rounding=8)
configs = [_make_lvgl_config(["my_disp"])]
final_validation(configs)
assert configs[0]["draw_rounding"] == 8
def test_display_draw_rounding_does_not_lower(self) -> None:
"""Display draw_rounding lower than LVGL config should not reduce it."""
add_metadata(ID("my_disp"), 320, 240, draw_rounding=1)
configs = [_make_lvgl_config(["my_disp"])]
configs[0]["draw_rounding"] = 4
final_validation(configs)
assert configs[0]["draw_rounding"] == 4
def test_zero_draw_rounding_no_change(self) -> None:
"""Display with draw_rounding=0 should not affect LVGL config."""
add_metadata(ID("my_disp"), 320, 240, draw_rounding=0)
configs = [_make_lvgl_config(["my_disp"])]
final_validation(configs)
assert configs[0]["draw_rounding"] == 2
@@ -124,15 +124,15 @@ def test_code_generation(
main_cpp = generate_main(component_fixture_path("mipi_dsi.yaml"))
assert (
"alignas(mipi_dsi::MIPI_DSI) static unsigned char mipi_dsi__p4_nano__pstorage[sizeof(mipi_dsi::MIPI_DSI)];"
"alignas(mipi_dsi::MipiDsi) static unsigned char mipi_dsi__p4_nano__pstorage[sizeof(mipi_dsi::MipiDsi)];"
in main_cpp
)
assert (
"static mipi_dsi::MIPI_DSI *const p4_nano = reinterpret_cast<mipi_dsi::MIPI_DSI *>(mipi_dsi__p4_nano__pstorage);"
"static mipi_dsi::MipiDsi *const p4_nano = reinterpret_cast<mipi_dsi::MipiDsi *>(mipi_dsi__p4_nano__pstorage);"
in main_cpp
)
assert (
"new(p4_nano) mipi_dsi::MIPI_DSI(800, 1280, display::COLOR_BITNESS_565, 16);"
"new(p4_nano) mipi_dsi::MipiDsi(800, 1280, display::COLOR_BITNESS_565, 16);"
in main_cpp
)
assert "set_init_sequence({224, 1, 0, 225, 1, 147, 226, 1," in main_cpp
+1 -38
View File
@@ -1,16 +1,10 @@
"""Tests for mpip_spi configuration validation."""
from collections.abc import Callable, Generator
from unittest import mock
import pytest
from esphome import config_validation as cv
from esphome.components.esp32 import KEY_ESP32, KEY_VARIANT, VARIANTS
from esphome.components.esp32.gpio import validate_gpio_pin
from esphome.const import CONF_INPUT, CONF_OUTPUT
from esphome.core import CORE
from esphome.pins import gpio_pin_schema
# choose_variant_with_pins is provided by the shared parent conftest.
@pytest.fixture(autouse=True)
@@ -21,34 +15,3 @@ def mock_spi_final_validate():
return_value=lambda config: None,
):
yield
@pytest.fixture
def choose_variant_with_pins() -> Generator[Callable[[list], None]]:
"""
Set the ESP32 variant for the given model based on pins. For ESP32 only since the other platforms
do not have variants.
"""
def chooser(pins: list) -> None:
for variant in VARIANTS:
try:
CORE.data[KEY_ESP32][KEY_VARIANT] = variant
for pin in pins:
if pin is not None:
pin = gpio_pin_schema(
{
CONF_INPUT: True,
CONF_OUTPUT: True,
},
internal=True,
)(pin)
validate_gpio_pin(pin)
return
except cv.Invalid:
continue
raise cv.Invalid(
f"No compatible variant found for pins: {', '.join(map(str, pins))}"
)
yield chooser
@@ -3,22 +3,15 @@
from collections.abc import Callable
from pathlib import Path
from esphome.components.display import (
DisplayMetaData,
get_all_display_metadata,
get_display_metadata,
)
from esphome.components.const import BYTE_ORDER_BIG
from esphome.components.display import get_all_display_metadata, get_display_metadata
from esphome.components.esp32 import (
KEY_BOARD,
KEY_VARIANT,
VARIANT_ESP32,
VARIANT_ESP32S3,
)
from esphome.components.mipi_spi.display import (
CONFIG_SCHEMA,
FINAL_VALIDATE_SCHEMA,
get_instance,
)
from esphome.components.mipi_spi.display import CONFIG_SCHEMA, FINAL_VALIDATE_SCHEMA
from esphome.const import PlatformFramework
from tests.component_tests.types import SetCoreConfigCallable
@@ -38,38 +31,32 @@ def test_metadata_native_quad_default_test_card(
PlatformFramework.ESP32_IDF,
platform_data={KEY_BOARD: "esp32-s3-devkitc-1", KEY_VARIANT: VARIANT_ESP32S3},
)
config = validated_config({"model": "JC3636W518"})
get_instance(config)
meta = get_display_metadata(str(config["id"]))
config = CONFIG_SCHEMA({"model": "JC3636W518", "id": "jc3232w518"})
meta = get_display_metadata(config["id"])
assert meta is not None
assert meta.width == 360
assert meta.height == 360
# final validation auto-enables show_test_card when no drawing methods are configured
assert meta.has_writer is True
assert meta.has_hardware_rotation is True
assert meta.byte_order == BYTE_ORDER_BIG
def test_metadata_single_mode_with_dc_pin(
set_core_config: SetCoreConfigCallable,
) -> None:
"""A single-mode display with no explicit drawing gets a test card from final validation."""
"""A single-mode display with no explicit drawing gets metadata from schema validation."""
set_core_config(
PlatformFramework.ESP32_IDF,
platform_data={KEY_BOARD: "esp32dev", KEY_VARIANT: VARIANT_ESP32},
)
config = validated_config(
{
"model": "ST7735",
"dc_pin": 18,
}
config = CONFIG_SCHEMA(
{"model": "ST7735", "dc_pin": 18, "id": "single_mode_with_dc_pin"}
)
get_instance(config)
meta = get_display_metadata(str(config["id"]))
meta = get_display_metadata(config["id"])
assert meta is not None
assert meta.width == 128
assert meta.height == 160
assert meta.has_writer is True
assert meta.has_hardware_rotation is True
assert meta.byte_order == BYTE_ORDER_BIG
def test_metadata_custom_dimensions(
@@ -80,47 +67,22 @@ def test_metadata_custom_dimensions(
PlatformFramework.ESP32_IDF,
platform_data={KEY_BOARD: "esp32dev", KEY_VARIANT: VARIANT_ESP32},
)
config = validated_config(
config = CONFIG_SCHEMA(
{
"model": "custom",
"dc_pin": 18,
"dimensions": {"width": 480, "height": 320},
"init_sequence": [[0xA0, 0x01]],
"id": "custom_dimensions",
}
)
get_instance(config)
meta = get_display_metadata(str(config["id"]))
meta = get_display_metadata(config["id"])
assert meta is not None
assert meta.width == 480
assert meta.height == 320
# final validation auto-enables show_test_card
assert meta.has_writer is True
assert meta.has_hardware_rotation is True
def test_metadata_with_test_card_has_writer(
set_core_config: SetCoreConfigCallable,
) -> None:
"""When show_test_card is enabled, has_writer should be True."""
set_core_config(
PlatformFramework.ESP32_IDF,
platform_data={KEY_BOARD: "esp32dev", KEY_VARIANT: VARIANT_ESP32},
)
config = validated_config(
{
"model": "custom",
"dc_pin": 18,
"dimensions": {"width": 240, "height": 240},
"init_sequence": [[0xA0, 0x01]],
"show_test_card": True,
}
)
get_instance(config)
meta = get_display_metadata(str(config["id"]))
assert meta is not None
assert meta.has_writer is True
def test_metadata_no_swap_xy_not_full_hardware_rotation(
set_core_config: SetCoreConfigCallable,
) -> None:
@@ -130,9 +92,8 @@ def test_metadata_no_swap_xy_not_full_hardware_rotation(
platform_data={KEY_BOARD: "esp32-s3-devkitc-1", KEY_VARIANT: VARIANT_ESP32S3},
)
# JC3248W535 has swap_xy=cv.UNDEFINED -> transforms={mirror_x, mirror_y} only
config = validated_config({"model": "JC3248W535"})
get_instance(config)
meta = get_display_metadata(str(config["id"]))
config = CONFIG_SCHEMA({"model": "JC3248W535", "id": "jc3248w535"})
meta = get_display_metadata(config["id"])
assert meta is not None
assert meta.has_hardware_rotation is False
@@ -145,7 +106,7 @@ def test_metadata_multiple_displays_independent(
PlatformFramework.ESP32_IDF,
platform_data={KEY_BOARD: "esp32dev", KEY_VARIANT: VARIANT_ESP32},
)
config_a = validated_config(
CONFIG_SCHEMA(
{
"id": "disp_a",
"model": "custom",
@@ -154,7 +115,7 @@ def test_metadata_multiple_displays_independent(
"init_sequence": [[0xA0, 0x01]],
}
)
config_b = validated_config(
CONFIG_SCHEMA(
{
"id": "disp_b",
"model": "custom",
@@ -163,13 +124,16 @@ def test_metadata_multiple_displays_independent(
"init_sequence": [[0xA0, 0x01]],
}
)
get_instance(config_a)
get_instance(config_b)
all_meta = get_all_display_metadata()
# final validation auto-enables show_test_card for both
assert all_meta["disp_a"] == DisplayMetaData(320, 240, True, True)
assert all_meta["disp_b"] == DisplayMetaData(128, 64, True, True)
assert all_meta["disp_a"].width == 320
assert all_meta["disp_a"].height == 240
assert all_meta["disp_a"].has_hardware_rotation is True
assert all_meta["disp_a"].byte_order == BYTE_ORDER_BIG
assert all_meta["disp_b"].width == 128
assert all_meta["disp_b"].height == 64
assert all_meta["disp_b"].has_hardware_rotation is True
assert all_meta["disp_b"].byte_order == BYTE_ORDER_BIG
def test_metadata_via_code_generation_native(
@@ -179,12 +143,13 @@ def test_metadata_via_code_generation_native(
"""Full code generation for native.yaml should produce correct metadata."""
generate_main(component_fixture_path("native.yaml"))
all_meta = get_all_display_metadata()
# native.yaml: model JC3636W518 -> 360x360, no writer, full hardware rotation
# native.yaml: model JC3636W518 -> 360x360, full hardware rotation
assert len(all_meta) == 1
meta = next(iter(all_meta.values()))
assert meta == DisplayMetaData(
width=360, height=360, has_writer=True, has_hardware_rotation=True
)
assert meta.width == 360
assert meta.height == 360
assert meta.has_hardware_rotation is True
assert meta.byte_order == BYTE_ORDER_BIG
def test_metadata_via_code_generation_lvgl(
@@ -194,9 +159,10 @@ def test_metadata_via_code_generation_lvgl(
"""Full code generation for lvgl.yaml should produce correct metadata."""
generate_main(component_fixture_path("lvgl.yaml"))
all_meta = get_all_display_metadata()
# lvgl.yaml: model ST7735 -> 128x160, no writer (lvgl draws directly), full hw rotation
# lvgl.yaml: model ST7735 -> 128x160, full hw rotation
assert len(all_meta) == 1
meta = next(iter(all_meta.values()))
assert meta == DisplayMetaData(
width=128, height=160, has_writer=False, has_hardware_rotation=True
)
assert meta.width == 128
assert meta.height == 160
assert meta.has_hardware_rotation is True
assert meta.byte_order == BYTE_ORDER_BIG
+2 -2
View File
@@ -314,7 +314,7 @@ def test_native_generation(
main_cpp = generate_main(component_fixture_path("native.yaml"))
assert (
"mipi_spi::MipiSpiBuffer<uint16_t, mipi_spi::PIXEL_MODE_16, true, mipi_spi::PIXEL_MODE_16, mipi_spi::BUS_TYPE_QUAD, 360, 360, 0, 1, 0, true, 1, 1>()"
"mipi_spi::MipiSpiBuffer<uint16_t, mipi_spi::PIXEL_MODE_16, true, mipi_spi::PIXEL_MODE_16, mipi_spi::BUS_TYPE_QUAD, 360, 360, 0, 1, 0, 0, 0, true, 1, 1>()"
in main_cpp
)
assert "set_init_sequence({240, 1, 8, 242" in main_cpp
@@ -330,7 +330,7 @@ def test_lvgl_generation(
main_cpp = generate_main(component_fixture_path("lvgl.yaml"))
assert (
"mipi_spi::MipiSpi<uint16_t, mipi_spi::PIXEL_MODE_16, true, mipi_spi::PIXEL_MODE_16, mipi_spi::BUS_TYPE_SINGLE, 128, 160, 0, 0, 0, true>();"
"mipi_spi::MipiSpi<uint16_t, mipi_spi::PIXEL_MODE_16, true, mipi_spi::PIXEL_MODE_16, mipi_spi::BUS_TYPE_SINGLE, 128, 160, 0, 0, 0, 0, 0, true>();"
in main_cpp
)
assert "set_init_sequence({1, 0, 10, 255, 177" in main_cpp
@@ -0,0 +1,434 @@
"""Tests for padding, offset calculation, and SPI mode configuration in mipi_spi."""
from __future__ import annotations
from collections.abc import Callable
from pathlib import Path
import pytest
from esphome.components.esp32 import (
KEY_BOARD,
KEY_VARIANT,
VARIANT_ESP32,
VARIANT_ESP32S3,
)
from esphome.components.mipi_spi.display import (
CONFIG_SCHEMA,
FINAL_VALIDATE_SCHEMA,
MODELS,
get_instance,
)
from esphome.components.spi import CONF_SPI_MODE, TYPE_OCTAL, TYPE_QUAD, TYPE_SINGLE
from esphome.const import CONF_CS_PIN, CONF_DC_PIN, PlatformFramework
from esphome.types import ConfigType
from tests.component_tests.types import SetCoreConfigCallable
def validated_config(config: ConfigType) -> ConfigType:
"""Run schema + final validation and return the validated config."""
config = CONFIG_SCHEMA(config)
FINAL_VALIDATE_SCHEMA(config)
return config
class TestSPIModeCalculation:
"""Test default SPI mode calculation logic."""
@pytest.mark.parametrize(
("bus_mode", "cs_pin", "expected_mode"),
[
pytest.param(
TYPE_OCTAL,
None,
"MODE3",
id="octal_bus_no_cs",
),
pytest.param(
TYPE_OCTAL,
14,
"MODE3",
id="octal_bus_with_cs",
),
pytest.param(
TYPE_SINGLE,
None,
"MODE3",
id="single_bus_no_cs",
),
pytest.param(
TYPE_SINGLE,
14,
"MODE0",
id="single_bus_with_cs",
),
pytest.param(
TYPE_QUAD,
None,
"MODE0",
id="quad_bus_no_cs",
),
pytest.param(
TYPE_QUAD,
14,
"MODE0",
id="quad_bus_with_cs",
),
],
)
def test_default_spi_mode_calculation(
self,
bus_mode: str,
cs_pin: int | None,
expected_mode: str,
set_core_config: SetCoreConfigCallable,
) -> None:
"""Test that SPI mode is correctly calculated based on bus mode and CS pin."""
set_core_config(
PlatformFramework.ESP32_IDF,
platform_data={
KEY_BOARD: "esp32-s3-devkitc-1",
KEY_VARIANT: VARIANT_ESP32S3,
},
)
config: ConfigType = {
"model": "custom",
"dimensions": {"width": 320, "height": 240},
"init_sequence": [[0xA0, 0x01]],
"bus_mode": bus_mode,
}
# Add dc_pin for modes that require it (single and octal)
# quad mode does not allow dc_pin
if bus_mode != TYPE_QUAD:
config[CONF_DC_PIN] = 11
# Add CS pin if specified
if cs_pin is not None:
config[CONF_CS_PIN] = cs_pin
validated = validated_config(config)
# The validated config should have the correct SPI mode set by model_schema
assert validated.get(CONF_SPI_MODE) == expected_mode
def test_explicit_spi_mode_overrides_default(
self,
set_core_config: SetCoreConfigCallable,
) -> None:
"""Test that an explicitly configured SPI mode is not overridden."""
set_core_config(
PlatformFramework.ESP32_IDF,
platform_data={
KEY_BOARD: "esp32-s3-devkitc-1",
KEY_VARIANT: VARIANT_ESP32S3,
},
)
# For octal bus, default is MODE3, but we specify MODE0
config = validated_config(
{
"model": "custom",
"dc_pin": 11, # Required for octal mode
"dimensions": {"width": 320, "height": 240},
"init_sequence": [[0xA0, 0x01]],
"bus_mode": TYPE_OCTAL,
"spi_mode": "MODE0", # Explicitly set
}
)
assert config[CONF_SPI_MODE] == "MODE0"
class TestModelWithPaddingDimensions:
"""Test that padding dimensions are correctly returned by models."""
def test_model_get_dimensions_returns_six_values(
self,
set_core_config: SetCoreConfigCallable,
) -> None:
"""Test that get_dimensions() returns 6 values including padding."""
set_core_config(
PlatformFramework.ESP32_IDF,
platform_data={
KEY_BOARD: "esp32-s3-devkitc-1",
KEY_VARIANT: VARIANT_ESP32S3,
},
)
# Test with a real model
model = MODELS["ST7735"]
config = {"model": "ST7735", "dc_pin": 18}
# Call get_dimensions - should return 6 values (width, height, offset_x, offset_y, pad_width, pad_height)
dimensions = model.get_dimensions(config)
assert len(dimensions) == 6
assert all(isinstance(v, int) for v in dimensions)
def test_custom_model_padding_values(
self,
set_core_config: SetCoreConfigCallable,
) -> None:
"""Test padding values for a custom model with explicit offset."""
set_core_config(
PlatformFramework.ESP32_IDF,
platform_data={KEY_BOARD: "esp32dev", KEY_VARIANT: VARIANT_ESP32},
)
config = validated_config(
{
"model": "custom",
"dc_pin": 18,
"dimensions": {
"width": 240,
"height": 320,
"offset_width": 20,
"offset_height": 10,
},
"init_sequence": [[0xA0, 0x01]],
}
)
# For custom models, the model is created dynamically from the config
# We can verify the config has the right dimensions
assert config["dimensions"]["width"] == 240
assert config["dimensions"]["height"] == 320
assert config["dimensions"]["offset_width"] == 20
assert config["dimensions"]["offset_height"] == 10
# Padding is not stored in config for custom models (defaults to 0)
assert config["dimensions"].get("offset_width_pad", 0) == 0
assert config["dimensions"].get("offset_height_pad", 0) == 0
class TestNewModelVariants:
"""Test new model variants added in this change."""
def test_m5core2_with_native_dimensions(
self,
set_core_config: SetCoreConfigCallable,
) -> None:
"""Test M5CORE2 variant with reset native_width and native_height."""
set_core_config(
PlatformFramework.ESP32_IDF,
platform_data={
KEY_BOARD: "esp32-s3-devkitc-1",
KEY_VARIANT: VARIANT_ESP32S3,
},
)
# M5CORE2 should validate successfully
config = validated_config({"model": "M5CORE2"})
assert config is not None
# Verify the model has correct dimensions
model = MODELS["M5CORE2"]
dimensions = model.get_dimensions(config)
width, height, _, _, _, _ = dimensions
assert width == 320
assert height == 240
def test_geekmagic_smalltv_variant(
self,
set_core_config: SetCoreConfigCallable,
) -> None:
"""Test GEEKMAGIC-SMALLTV variant of ST7789V."""
set_core_config(
PlatformFramework.ESP32_IDF,
platform_data={KEY_BOARD: "esp32dev", KEY_VARIANT: VARIANT_ESP32},
)
# GEEKMAGIC-SMALLTV should validate successfully
config = validated_config({"model": "GEEKMAGIC-SMALLTV"})
assert config is not None
# Verify it's a variant of ST7789V with expected dimensions
model = MODELS["GEEKMAGIC-SMALLTV"]
dimensions = model.get_dimensions(config)
width, height, offset_x, offset_y, _, _ = dimensions
assert width == 240
assert height == 240
assert offset_x == 0
assert offset_y == 0
def test_all_predefined_models_with_new_get_dimensions_signature(
self,
set_core_config: SetCoreConfigCallable,
) -> None:
"""Verify all predefined models work with new 6-value get_dimensions()."""
set_core_config(
PlatformFramework.ESP32_IDF,
platform_data={
KEY_BOARD: "esp32-s3-devkitc-1",
KEY_VARIANT: VARIANT_ESP32S3,
},
)
for name, model in MODELS.items():
# Skip custom model
if name == "custom":
continue
config = {"model": name}
# Try to get dimensions - should return 6 values for all models
dimensions = model.get_dimensions(config)
assert len(dimensions) == 6, (
f"Model {name} should return 6 dimensions, got {len(dimensions)}"
)
class TestTemplateParameterPassing:
"""Test that padding parameters are correctly passed to C++ templates."""
def test_instance_creation_with_padding(
self,
generate_main: Callable[[str | Path], str],
component_fixture_path: Callable[[str], Path],
) -> None:
"""Test that get_instance() correctly passes padding parameters to template."""
main_cpp = generate_main(component_fixture_path("native.yaml"))
# native.yaml uses JC3636W518 which should have 8 template parameters for MipiSpiBuffer
# (BUFFERTYPE, BUFFERPIXEL, IS_BIG_ENDIAN, DISPLAYPIXEL, BUS_TYPE,
# WIDTH, HEIGHT, OFFSET_WIDTH, OFFSET_HEIGHT, PAD_WIDTH, PAD_HEIGHT, MADCTL, HAS_HARDWARE_ROTATION,
# FRACTION, ROUNDING)
# The instantiation should include padding values (0, 0 for default)
assert (
"mipi_spi::MipiSpiBuffer<uint16_t, mipi_spi::PIXEL_MODE_16, true, mipi_spi::PIXEL_MODE_16, mipi_spi::BUS_TYPE_QUAD, 360, 360, 0, 1, 0, 0, 0, true, 1, 1>()"
in main_cpp
), (
"Padding parameters (0, 0) should be in the MipiSpiBuffer template instantiation"
)
def test_single_mode_with_offset_padding(
self,
set_core_config: SetCoreConfigCallable,
) -> None:
"""Test that single-mode display with custom offset works with padding."""
set_core_config(
PlatformFramework.ESP32_IDF,
platform_data={KEY_BOARD: "esp32dev", KEY_VARIANT: VARIANT_ESP32},
)
config = validated_config(
{
"model": "custom",
"dc_pin": 18,
"dimensions": {
"width": 240,
"height": 320,
"offset_width": 40,
"offset_height": 20,
},
"init_sequence": [[0xA0, 0x01]],
"buffer_size": 0.25,
}
)
# Should not raise any errors
instance = get_instance(config)
assert instance is not None
class TestUserConfiguredPadding:
"""Test that pad_width and pad_height can be configured in user dimensions."""
def test_explicit_pad_width_and_height_in_dimensions(
self,
set_core_config: SetCoreConfigCallable,
) -> None:
"""Test that pad_width and pad_height can be explicitly set in dimensions."""
set_core_config(
PlatformFramework.ESP32_IDF,
platform_data={KEY_BOARD: "esp32dev", KEY_VARIANT: VARIANT_ESP32},
)
config = validated_config(
{
"model": "custom",
"dc_pin": 18,
"dimensions": {
"width": 240,
"height": 320,
"offset_width": 40,
"offset_height": 20,
"pad_width": 80,
"pad_height": 40,
},
"init_sequence": [[0xA0, 0x01]],
"buffer_size": 0.25,
}
)
# Config should validate successfully with padding dimensions
assert config is not None
assert config["dimensions"]["pad_width"] == 80
assert config["dimensions"]["pad_height"] == 40
def test_padding_for_native_dimension_calculation(
self,
set_core_config: SetCoreConfigCallable,
) -> None:
"""Test that explicit padding allows native dimensions to be calculated."""
set_core_config(
PlatformFramework.ESP32_IDF,
platform_data={KEY_BOARD: "esp32dev", KEY_VARIANT: VARIANT_ESP32},
)
# A controller that has 320x320 total pixels with:
# - 240x320 active display area
# - offset_width=40, offset_height=20
# - pad_width=40 (remaining pixels on right), pad_height=60 (remaining pixels on bottom)
config = validated_config(
{
"model": "custom",
"dc_pin": 18,
"dimensions": {
"width": 240, # Active display width
"height": 320, # Active display height
"offset_width": 40,
"offset_height": 0,
"pad_width": 40, # Pixels after width+offset
"pad_height": 0, # Pixels after height+offset
},
"init_sequence": [[0xA0, 0x01]],
"buffer_size": 0.25,
}
)
# Get instance should work and correctly calculate native dimensions
instance = get_instance(config)
assert instance is not None
def test_padding_without_offset(
self,
set_core_config: SetCoreConfigCallable,
) -> None:
"""Test padding can be used without offset for controllers with top-left-aligned displays."""
set_core_config(
PlatformFramework.ESP32_IDF,
platform_data={KEY_BOARD: "esp32dev", KEY_VARIANT: VARIANT_ESP32},
)
# A display with no offset but padding on right and bottom
config = validated_config(
{
"model": "custom",
"dc_pin": 18,
"dimensions": {
"width": 240,
"height": 240,
"offset_width": 0,
"offset_height": 0,
"pad_width": 0,
"pad_height": 16,
},
"init_sequence": [[0xA0, 0x01]],
"buffer_size": 0.25,
}
)
assert config is not None
assert config["dimensions"]["width"] == 240
assert config["dimensions"]["height"] == 240
assert config["dimensions"]["pad_height"] == 16
+899
View File
@@ -0,0 +1,899 @@
"""Tests for the motion component."""
from __future__ import annotations
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
from voluptuous import Invalid, MultipleInvalid
from esphome.components.motion import (
CALIBRATE_ACTION_SCHEMA,
CLEAR_ACTION_SCHEMA,
CONF_AXIS_MAP,
CONF_SAVE,
CONF_TRANSFORM_MATRIX,
_axis_map,
_axis_map_to_matrix,
_build_calibrate_action,
_transform_matrix,
_validate_matrix_options,
clear_calibration_to_code,
)
from esphome.components.motion.sensor import (
_ACCELERATIONS,
_ANGULAR_RATES,
_GYROSCOPES,
CONF_PITCH,
CONF_ROLL,
CONFIG_SCHEMA,
build_sensor_expr,
)
from esphome.const import CONF_ID, CONF_ON_ERROR, CONF_ON_SUCCESS
from esphome.cpp_generator import MockObj
# --- Axis map validation ---
class TestAxisMapValidation:
"""Tests for the _axis_map validator."""
def test_identity_map(self):
result = _axis_map({"x": "x", "y": "y", "z": "z"})
assert result == {"x": "x", "y": "y", "z": "z"}
def test_axis_swap(self):
result = _axis_map({"x": "y", "y": "z", "z": "x"})
assert result == {"x": "y", "y": "z", "z": "x"}
def test_negation(self):
result = _axis_map({"x": "-y", "y": "z", "z": "x"})
assert result == {"x": "-y", "y": "z", "z": "x"}
def test_plus_prefix(self):
result = _axis_map({"x": "+y", "y": "z", "z": "x"})
assert result == {"x": "+y", "y": "z", "z": "x"}
def test_case_insensitive(self):
result = _axis_map({"x": "X", "y": "Y", "z": "Z"})
assert result == {"x": "X", "y": "Y", "z": "Z"}
def test_invalid_axis_value(self):
with pytest.raises(MultipleInvalid):
_axis_map({"x": "a", "y": "y", "z": "z"})
def test_duplicate_mapping(self):
with pytest.raises(MultipleInvalid):
_axis_map({"x": "x", "y": "x", "z": "z"})
def test_all_same_axis(self):
with pytest.raises(MultipleInvalid):
_axis_map({"x": "x", "y": "x", "z": "x"})
def test_empty_value(self):
with pytest.raises(MultipleInvalid):
_axis_map({"x": "", "y": "y", "z": "z"})
def test_invalid_and_duplicate(self):
"""Both invalid value and duplicate should produce multiple errors."""
with pytest.raises(MultipleInvalid) as exc_info:
_axis_map({"x": "a", "y": "x", "z": "z"})
# Should have at least the invalid regex error and the duplicate error
assert len(exc_info.value.errors) >= 2
# --- Transform matrix validation ---
class TestTransformMatrix:
"""Tests for the _transform_matrix validator."""
def test_flat_identity(self):
result = _transform_matrix([1, 0, 0, 0, 1, 0, 0, 0, 1])
assert result == [1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0]
def test_flat_values_converted_to_float(self):
result = _transform_matrix([1, 2, 3, 4, 5, 6, 7, 8, 9])
assert all(isinstance(v, float) for v in result)
def test_nested_3x3(self):
result = _transform_matrix([[1, 0, 0], [0, 1, 0], [0, 0, 1]])
assert result == [1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0]
def test_nested_3x3_values(self):
result = _transform_matrix(
[[0.5, 0.1, -0.2], [-0.1, 0.9, 0.3], [0.2, -0.3, 0.8]]
)
assert len(result) == 9
assert result[0] == pytest.approx(0.5)
assert result[3] == pytest.approx(-0.1)
assert result[8] == pytest.approx(0.8)
def test_flat_wrong_length_short(self):
with pytest.raises(Invalid, match="exactly 9"):
_transform_matrix([1, 0, 0])
def test_flat_wrong_length_long(self):
with pytest.raises(Invalid, match="exactly 9"):
_transform_matrix([1] * 12)
def test_nested_wrong_row_count(self):
with pytest.raises(Invalid, match="3 rows"):
_transform_matrix([[1, 0, 0], [0, 1, 0]])
def test_nested_wrong_column_count(self):
with pytest.raises(Invalid, match="3 numbers"):
_transform_matrix([[1, 0], [0, 1, 0], [0, 0, 1]])
def test_empty_list(self):
with pytest.raises(Invalid):
_transform_matrix([])
def test_not_a_list(self):
with pytest.raises(Invalid):
_transform_matrix("identity")
class TestValidateMatrixOptions:
"""Tests for mutual exclusivity of axis_map and transform_matrix."""
def test_neither_passes(self):
config = {"some_key": "value"}
assert _validate_matrix_options(config) is config
def test_axis_map_only_passes(self):
config = {CONF_AXIS_MAP: {"x": "x", "y": "y", "z": "z"}}
assert _validate_matrix_options(config) is config
def test_transform_matrix_only_passes(self):
config = {CONF_TRANSFORM_MATRIX: [1, 0, 0, 0, 1, 0, 0, 0, 1]}
assert _validate_matrix_options(config) is config
def test_both_raises(self):
config = {
CONF_AXIS_MAP: {"x": "x", "y": "y", "z": "z"},
CONF_TRANSFORM_MATRIX: [1, 0, 0, 0, 1, 0, 0, 0, 1],
}
with pytest.raises(Invalid, match="mutually exclusive"):
_validate_matrix_options(config)
# --- Axis map to matrix ---
class TestAxisMapToMatrix:
"""Tests for _axis_map_to_matrix conversion."""
def test_identity(self):
assert _axis_map_to_matrix({"x": "x", "y": "y", "z": "z"}) == [
1,
0,
0,
0,
1,
0,
0,
0,
1,
]
def test_swap_xy(self):
# x←y, y←x, z←z
assert _axis_map_to_matrix({"x": "y", "y": "x", "z": "z"}) == [
0,
1,
0,
1,
0,
0,
0,
0,
1,
]
def test_rotate_xyz(self):
# x←y, y←z, z←x
assert _axis_map_to_matrix({"x": "y", "y": "z", "z": "x"}) == [
0,
1,
0,
0,
0,
1,
1,
0,
0,
]
def test_negate_x(self):
assert _axis_map_to_matrix({"x": "-x", "y": "y", "z": "z"}) == [
-1,
0,
0,
0,
1,
0,
0,
0,
1,
]
def test_negate_z(self):
assert _axis_map_to_matrix({"x": "x", "y": "y", "z": "-z"}) == [
1,
0,
0,
0,
1,
0,
0,
0,
-1,
]
def test_swap_and_negate(self):
# x←-y, y←z, z←x
assert _axis_map_to_matrix({"x": "-y", "y": "z", "z": "x"}) == [
0,
-1,
0,
0,
0,
1,
1,
0,
0,
]
def test_plus_prefix_ignored(self):
assert _axis_map_to_matrix({"x": "+y", "y": "z", "z": "x"}) == [
0,
1,
0,
0,
0,
1,
1,
0,
0,
]
# --- Sensor expression generation ---
def _expr_str(sensor_type: str) -> str:
"""Build a sensor expression via the production function and return its string form."""
return str(build_sensor_expr(sensor_type, MockObj("data")))
class TestSensorExpressions:
"""Tests that sensor code generation produces correct C++ expressions."""
@pytest.mark.parametrize(
"sensor_type,expected_index",
[
("acceleration_x", 0),
("acceleration_y", 1),
("acceleration_z", 2),
],
)
def test_acceleration_sensors(self, sensor_type, expected_index):
assert _expr_str(sensor_type) == f"data.acceleration[{expected_index}]"
@pytest.mark.parametrize(
"sensor_type,expected_index",
[
("angular_rate_x", 0),
("angular_rate_y", 1),
("angular_rate_z", 2),
],
)
def test_angular_rate_sensors(self, sensor_type, expected_index):
assert _expr_str(sensor_type) == f"data.angular_rate[{expected_index}]"
@pytest.mark.parametrize(
"sensor_type,expected_index",
[
("gyroscope_x", 0),
("gyroscope_y", 1),
("gyroscope_z", 2),
],
)
def test_gyroscope_maps_to_angular_rate(self, sensor_type, expected_index):
"""Gyroscope sensor types should be remapped to angular_rate in the expression."""
assert _expr_str(sensor_type) == f"data.angular_rate[{expected_index}]"
def test_roll_expression(self):
expr = _expr_str("roll")
assert "std::atan2" in expr
assert "data.acceleration[1]" in expr
assert "data.acceleration[2]" in expr
assert "180.0f" in expr
assert "std::numbers::pi_v<float>" in expr
# Roll should NOT reference acceleration[0]
assert "data.acceleration[0]" not in expr
def test_pitch_expression(self):
expr = _expr_str("pitch")
assert "std::atan2" in expr
assert "std::sqrt" in expr
# All three axes used
assert "data.acceleration[0]" in expr
assert "data.acceleration[1]" in expr
assert "data.acceleration[2]" in expr
assert "180.0f" in expr
assert "std::numbers::pi_v<float>" in expr
# Pitch negates the x component
assert "(-data.acceleration[0])" in expr
# --- Calibration math ---
#
# Pure-Python reimplementation of the C++ calibration algorithms so we can
# verify the mathematical properties without needing to compile C++.
def _mat_vec(m: list[float], v: list[float]) -> list[float]:
"""Multiply a row-major 3x3 matrix by a 3-vector."""
return [
m[0] * v[0] + m[1] * v[1] + m[2] * v[2],
m[3] * v[0] + m[4] * v[1] + m[5] * v[2],
m[6] * v[0] + m[7] * v[1] + m[8] * v[2],
]
def _mat_mul(a: list[float], b: list[float]) -> list[float]:
"""Multiply two row-major 3x3 matrices."""
r = [0.0] * 9
for i in range(3):
for j in range(3):
r[i * 3 + j] = sum(a[i * 3 + k] * b[k * 3 + j] for k in range(3))
return r
def _transpose(m: list[float]) -> list[float]:
"""Transpose a row-major 3x3 matrix."""
return [m[0], m[3], m[6], m[1], m[4], m[7], m[2], m[5], m[8]]
def _det(m: list[float]) -> float:
"""Determinant of a 3x3 matrix."""
return (
m[0] * (m[4] * m[8] - m[5] * m[7])
- m[1] * (m[3] * m[8] - m[5] * m[6])
+ m[2] * (m[3] * m[7] - m[4] * m[6])
)
def _calibrate_level(
raw: list[float], matrix: list[float] | None = None
) -> list[float]:
"""Python port of MotionComponent::calibrate_level.
Composes the correction with *matrix* (defaults to identity).
"""
import math
if matrix is None:
matrix = list(IDENTITY)
# Apply current matrix first
mapped = _mat_vec(matrix, raw)
nx, ny, nz = mapped
mag = math.sqrt(nx * nx + ny * ny + nz * nz)
nx /= mag
ny /= mag
nz /= mag
if nz > 0.9999:
return matrix[:] # already aligned, preserve existing matrix
if nz < -0.9999:
r = [1, 0, 0, 0, -1, 0, 0, 0, -1]
else:
f = 1.0 / (1.0 + nz)
r = [
1.0 - nx * nx * f,
-nx * ny * f,
-nx,
-nx * ny * f,
1.0 - ny * ny * f,
-ny,
nx,
ny,
nz,
]
return _mat_mul(r, matrix)
def _calibrate_heading(matrix: list[float], raw: list[float]) -> list[float]:
"""Python port of MotionComponent::calibrate_heading."""
import math
mapped = _mat_vec(matrix, raw)
mx, my = mapped[0], mapped[1]
h = math.sqrt(mx * mx + my * my)
sign_mx = 1.0 if mx >= 0 else -1.0
cos_phi = sign_mx * mx / h # = |mx| / h
sin_phi = sign_mx * my / h
old = matrix[:]
new = old[:]
new[0] = cos_phi * old[0] + sin_phi * old[3]
new[1] = cos_phi * old[1] + sin_phi * old[4]
new[2] = cos_phi * old[2] + sin_phi * old[5]
new[3] = -sin_phi * old[0] + cos_phi * old[3]
new[4] = -sin_phi * old[1] + cos_phi * old[4]
new[5] = -sin_phi * old[2] + cos_phi * old[5]
return new
IDENTITY = [1, 0, 0, 0, 1, 0, 0, 0, 1]
class TestCalibrateLevel:
"""Verify the Rodrigues-based level calibration matrix."""
def _assert_maps_to_z(self, raw: list[float]) -> list[float]:
"""Assert that the calibration matrix maps raw to [0, 0, 1]."""
import math
m = _calibrate_level(raw)
mag = math.sqrt(sum(v * v for v in raw))
norm = [v / mag for v in raw]
result = _mat_vec(m, norm)
assert result[0] == pytest.approx(0, abs=1e-6)
assert result[1] == pytest.approx(0, abs=1e-6)
assert result[2] == pytest.approx(1, abs=1e-6)
return m
def test_already_flat(self):
m = _calibrate_level([0, 0, 1.0])
assert m == IDENTITY
def test_preserves_existing_matrix_when_flat(self):
"""If already flat after axis mapping, level cal should not change the matrix."""
swap = [0, 1, 0, 1, 0, 0, 0, 0, 1] # swap X↔Y
m = _calibrate_level([0, 0, 1.0], swap)
assert m == swap
def test_composes_with_existing_matrix(self):
"""Level calibration should correct tilt while preserving an existing axis swap."""
import math
swap = [0, 1, 0, 1, 0, 0, 0, 0, 1] # swap X↔Y
# Tilted raw: gravity has X component in raw frame
raw = [0.3, 0.0, 0.954]
m = _calibrate_level(raw, swap)
# After calibration, current raw should map to [0, 0, ~1]
mag = math.sqrt(sum(v * v for v in raw))
norm = [v / mag for v in raw]
result = _mat_vec(m, norm)
assert result[0] == pytest.approx(0, abs=1e-5)
assert result[1] == pytest.approx(0, abs=1e-5)
assert result[2] == pytest.approx(1, abs=1e-5)
# Result should differ from calibrating without the swap
m_no_swap = _calibrate_level(raw)
assert m != m_no_swap
def test_upside_down(self):
m = _calibrate_level([0, 0, -1.0])
# 180° about X
assert m == [1, 0, 0, 0, -1, 0, 0, 0, -1]
result = _mat_vec(m, [0, 0, -1])
assert result[2] == pytest.approx(1, abs=1e-6)
def test_gravity_along_x(self):
self._assert_maps_to_z([1.0, 0, 0])
def test_gravity_along_neg_x(self):
self._assert_maps_to_z([-1.0, 0, 0])
def test_gravity_along_y(self):
self._assert_maps_to_z([0, 1.0, 0])
def test_tilted_45_degrees(self):
import math
self._assert_maps_to_z(
[math.sin(math.radians(45)), 0, math.cos(math.radians(45))]
)
def test_arbitrary_vector(self):
self._assert_maps_to_z([0.3, -0.5, 0.81])
def test_unnormalized_input(self):
"""Input does not need to be unit length."""
self._assert_maps_to_z([0.6, -1.0, 1.62])
@pytest.mark.parametrize(
"raw",
[
[1.0, 0, 0],
[0, 1.0, 0],
[0.3, -0.5, 0.81],
[-0.7, 0.4, 0.59],
],
)
def test_result_is_proper_rotation(self, raw):
"""The resulting matrix should be orthogonal with determinant +1."""
m = _calibrate_level(raw)
# R^T * R ≈ I
product = _mat_mul(_transpose(m), m)
for i in range(9):
expected = 1.0 if i % 4 == 0 else 0.0
assert product[i] == pytest.approx(expected, abs=1e-6)
# det ≈ 1
assert _det(m) == pytest.approx(1.0, abs=1e-6)
class TestCalibrateHeading:
"""Verify the Z-rotation heading correction."""
def test_y_axis_tilt_no_heading_error(self):
"""Device tilted purely around Y — heading should already be correct."""
import math
flat_raw = [0, 0, 1.0]
level_m = _calibrate_level(flat_raw)
# Tilt 30° around Y: gravity = [-sin30, 0, cos30]
tilted_raw = [-math.sin(math.radians(30)), 0, math.cos(math.radians(30))]
heading_m = _calibrate_heading(level_m, tilted_raw)
# Matrix should barely change since there's no Y component
for i in range(9):
assert heading_m[i] == pytest.approx(level_m[i], abs=1e-6)
def test_corrects_heading_rotation(self):
"""After level+heading calibration, mapped Y should be ~0 when tilted."""
import math
# Simulate a sensor whose chip is rotated 30° around Z relative to enclosure
angle = math.radians(30)
# When the enclosure is flat, the raw reading is [0, 0, 1] regardless of Z rotation
level_m = _calibrate_level([0, 0, 1.0])
# When tilted around the enclosure's Y axis, the raw reading in the
# chip frame has both X and Y components due to the Z-rotation offset
tilt = math.radians(20)
# In enclosure frame: [-sin(tilt), 0, cos(tilt)]
# Rotated by Z-angle into chip frame:
ex = -math.sin(tilt) * math.cos(angle)
ey = -math.sin(tilt) * math.sin(angle)
ez = math.cos(tilt)
tilted_raw = [ex, ey, ez]
heading_m = _calibrate_heading(level_m, tilted_raw)
# After correction, mapped Y should be 0
result = _mat_vec(heading_m, tilted_raw)
assert result[1] == pytest.approx(0, abs=1e-6)
# Z should still be correct
assert result[2] == pytest.approx(math.cos(tilt), abs=1e-6)
def test_full_calibration_sequence(self):
"""End-to-end: level then heading produces correct frame alignment."""
import math
# Chip is mounted tilted 15° around Y and 25° around Z
# Build the chip-to-enclosure rotation: Rz(25°) * Ry(15°)
yz = math.radians(25)
yy = math.radians(15)
# Ry(yy)
ry = [
math.cos(yy),
0,
math.sin(yy),
0,
1,
0,
-math.sin(yy),
0,
math.cos(yy),
]
# Rz(yz)
rz = [
math.cos(yz),
-math.sin(yz),
0,
math.sin(yz),
math.cos(yz),
0,
0,
0,
1,
]
chip_rot = _mat_mul(rz, ry) # chip orientation in enclosure frame
# Inverse (transpose) maps enclosure vectors to chip readings
chip_rot_inv = _transpose(chip_rot)
# Step 1: Device flat — gravity in enclosure frame is [0, 0, 1]
flat_raw = _mat_vec(chip_rot_inv, [0, 0, 1])
level_m = _calibrate_level(flat_raw)
# After level calibration, flat reading should map to [0, 0, 1]
check_flat = _mat_vec(level_m, flat_raw)
assert check_flat[0] == pytest.approx(0, abs=1e-5)
assert check_flat[1] == pytest.approx(0, abs=1e-5)
assert check_flat[2] == pytest.approx(1, abs=1e-5)
# Step 2: Tilt enclosure around Y by 20°
tilt = math.radians(20)
tilted_enclosure = [-math.sin(tilt), 0, math.cos(tilt)]
tilted_raw = _mat_vec(chip_rot_inv, tilted_enclosure)
heading_m = _calibrate_heading(level_m, tilted_raw)
# After heading calibration, the mapped reading should be
# [-sin(tilt), 0, cos(tilt)] — all horizontal component in X
result = _mat_vec(heading_m, tilted_raw)
assert result[0] == pytest.approx(-math.sin(tilt), abs=1e-5)
assert result[1] == pytest.approx(0, abs=1e-5)
assert result[2] == pytest.approx(math.cos(tilt), abs=1e-5)
@pytest.mark.parametrize(
"raw",
[
[0.3, -0.5, 0.81],
[-0.7, 0.4, 0.59],
],
)
def test_heading_preserves_orthogonality(self, raw):
"""Heading correction composed with level should remain a proper rotation."""
level_m = _calibrate_level(raw)
# Create a tilted reading for heading calibration
tilt_raw = [v + 0.3 for v in raw] # perturb to get XY component
heading_m = _calibrate_heading(level_m, tilt_raw)
product = _mat_mul(_transpose(heading_m), heading_m)
for i in range(9):
expected = 1.0 if i % 4 == 0 else 0.0
assert product[i] == pytest.approx(expected, abs=1e-5)
assert _det(heading_m) == pytest.approx(1.0, abs=1e-5)
# --- Calibration action schema & codegen ---
class TestCalibrateActionSchema:
"""Tests for the CALIBRATE_ACTION_SCHEMA used by both calibration actions."""
def test_schema_accepts_on_success_key(self):
"""on_success must be a recognised optional key."""
schema_keys = {str(k) for k in CALIBRATE_ACTION_SCHEMA.schema}
assert CONF_ON_SUCCESS in schema_keys
def test_schema_accepts_on_error_key(self):
"""on_error must be a recognised optional key."""
schema_keys = {str(k) for k in CALIBRATE_ACTION_SCHEMA.schema}
assert CONF_ON_ERROR in schema_keys
@pytest.fixture
def mock_codegen():
"""Mock cg and automation functions used by _build_calibrate_action."""
mock_var = MagicMock()
mock_parent = MagicMock()
with (
patch(
"esphome.components.motion.cg.get_variable",
new_callable=AsyncMock,
return_value=mock_parent,
) as mock_get_var,
patch(
"esphome.components.motion.cg.new_Pvariable",
return_value=mock_var,
) as mock_new_pvar,
patch(
"esphome.components.motion.automation.build_automation",
new_callable=AsyncMock,
) as mock_build_auto,
):
yield {
"get_variable": mock_get_var,
"new_Pvariable": mock_new_pvar,
"build_automation": mock_build_auto,
"var": mock_var,
"parent": mock_parent,
}
@pytest.mark.asyncio
async def test_build_calibrate_action_no_triggers(mock_codegen):
"""Without on_success/on_error, build_automation should not be called."""
config = {CONF_ID: MagicMock()}
action_id = MagicMock()
template_arg = MagicMock()
result = await _build_calibrate_action(config, action_id, template_arg, [])
assert result is mock_codegen["var"]
mock_codegen["new_Pvariable"].assert_called_once_with(
action_id, template_arg, mock_codegen["parent"]
)
mock_codegen["build_automation"].assert_not_called()
@pytest.mark.asyncio
async def test_build_calibrate_action_with_on_success(mock_codegen):
"""on_success should wire build_automation to get_success_trigger()."""
on_success_config = MagicMock()
config = {CONF_ID: MagicMock(), CONF_ON_SUCCESS: on_success_config}
await _build_calibrate_action(config, MagicMock(), MagicMock(), [])
mock_codegen["build_automation"].assert_called_once_with(
mock_codegen["var"].get_success_trigger(), [], on_success_config
)
@pytest.mark.asyncio
async def test_build_calibrate_action_with_on_error(mock_codegen):
"""on_error should wire build_automation to get_error_trigger()."""
on_error_config = MagicMock()
config = {CONF_ID: MagicMock(), CONF_ON_ERROR: on_error_config}
await _build_calibrate_action(config, MagicMock(), MagicMock(), [])
mock_codegen["build_automation"].assert_called_once_with(
mock_codegen["var"].get_error_trigger(), [], on_error_config
)
@pytest.mark.asyncio
async def test_build_calibrate_action_with_both_triggers(mock_codegen):
"""Both on_success and on_error should each produce a build_automation call."""
on_success_config = MagicMock()
on_error_config = MagicMock()
config = {
CONF_ID: MagicMock(),
CONF_ON_SUCCESS: on_success_config,
CONF_ON_ERROR: on_error_config,
}
await _build_calibrate_action(config, MagicMock(), MagicMock(), [])
assert mock_codegen["build_automation"].call_count == 2
calls = mock_codegen["build_automation"].call_args_list
# First call: on_success
assert calls[0].args == (
mock_codegen["var"].get_success_trigger(),
[],
on_success_config,
)
# Second call: on_error
assert calls[1].args == (
mock_codegen["var"].get_error_trigger(),
[],
on_error_config,
)
# --- Clear calibration action ---
class TestClearActionSchema:
"""Tests for CLEAR_ACTION_SCHEMA."""
def test_schema_has_save_key(self):
schema_keys = {str(k) for k in CLEAR_ACTION_SCHEMA.schema}
assert CONF_SAVE in schema_keys
def test_save_defaults_to_false(self):
result = CLEAR_ACTION_SCHEMA({CONF_ID: "x"})
assert result[CONF_SAVE] is False
@pytest.fixture
def mock_clear_codegen():
"""Mock cg functions used by clear_calibration_to_code."""
mock_var = MagicMock()
mock_parent = MagicMock()
with (
patch(
"esphome.components.motion.cg.get_variable",
new_callable=AsyncMock,
return_value=mock_parent,
),
patch(
"esphome.components.motion.cg.new_Pvariable",
return_value=mock_var,
) as mock_new_pvar,
patch("esphome.components.motion.cg.add") as mock_add,
):
yield {"new_Pvariable": mock_new_pvar, "add": mock_add, "var": mock_var}
@pytest.mark.asyncio
async def test_clear_action_without_save(mock_clear_codegen):
"""With save=False, set_save should not be emitted."""
config = {CONF_ID: MagicMock(), CONF_SAVE: False}
result = await clear_calibration_to_code(config, MagicMock(), MagicMock(), [])
assert result is mock_clear_codegen["var"]
mock_clear_codegen["add"].assert_not_called()
@pytest.mark.asyncio
async def test_clear_action_with_save(mock_clear_codegen):
"""With save=True, set_save(True) should be emitted exactly once."""
config = {CONF_ID: MagicMock(), CONF_SAVE: True}
await clear_calibration_to_code(config, MagicMock(), MagicMock(), [])
mock_clear_codegen["var"].set_save.assert_called_once_with(True)
mock_clear_codegen["add"].assert_called_once()
# --- Calibration persistence invalidation ---
#
# The C++ side stores a hash of the build-time base matrix alongside the saved
# calibration so a changed axis_map invalidates stale NVS data without orphaning
# storage (the pref key stays ID-stable). These tests pin the design properties
# of that base-matrix fingerprint: deterministic for identical maps, distinct
# for different ones.
def _hash_matrix(matrix: list[float]) -> int:
"""Python port of the C++ hash_matrix() (FNV-1a over the float bytes)."""
import struct
data = struct.pack("<9f", *matrix)
h = 2166136261
for b in data:
h ^= b
h = (h * 16777619) & 0xFFFFFFFF
return h
class TestBaseMatrixHash:
"""Properties of the base-matrix fingerprint used for NVS invalidation."""
def test_identical_axis_maps_hash_equal(self):
a = _axis_map_to_matrix({"x": "x", "y": "y", "z": "z"})
b = _axis_map_to_matrix({"x": "x", "y": "y", "z": "z"})
assert _hash_matrix([float(v) for v in a]) == _hash_matrix(
[float(v) for v in b]
)
def test_different_axis_maps_hash_differ(self):
identity = _axis_map_to_matrix({"x": "x", "y": "y", "z": "z"})
swapped = _axis_map_to_matrix({"x": "y", "y": "x", "z": "z"})
assert _hash_matrix([float(v) for v in identity]) != _hash_matrix(
[float(v) for v in swapped]
)
def test_sign_change_hashes_differ(self):
pos = _axis_map_to_matrix({"x": "x", "y": "y", "z": "z"})
neg = _axis_map_to_matrix({"x": "-x", "y": "y", "z": "z"})
assert _hash_matrix([float(v) for v in pos]) != _hash_matrix(
[float(v) for v in neg]
)
# --- Sensor config schema type validation ---
class TestSensorConfigSchema:
"""Tests for sensor CONFIG_SCHEMA type key validation."""
def test_invalid_type_rejected(self):
with pytest.raises((Invalid, MultipleInvalid), match="Unknown value"):
CONFIG_SCHEMA({"type": "invalid_type"})
def test_missing_type_rejected(self):
with pytest.raises((Invalid, MultipleInvalid)):
CONFIG_SCHEMA({})
@pytest.mark.parametrize(
"sensor_type",
_ACCELERATIONS + _GYROSCOPES + _ANGULAR_RATES + [CONF_PITCH, CONF_ROLL],
)
def test_valid_types_accepted(self, sensor_type):
"""Valid sensor types should pass type validation (errors from missing
required fields like motion_id are expected and acceptable)."""
try:
CONFIG_SCHEMA({"type": sensor_type})
except (Invalid, MultipleInvalid) as e:
# Should NOT be a type validation error
assert "Unknown value" not in str(e), (
f"Type '{sensor_type}' was rejected as unknown"
)
+48
View File
@@ -97,6 +97,54 @@ def test_psram_configuration_valid_supported_variants(
FINAL_VALIDATE_SCHEMA(config)
def test_psram_applies_single_mode_default(
set_core_config: SetCoreConfigCallable,
) -> None:
"""On a single-mode variant the omitted mode/speed fall back to defaults."""
set_core_config(
PlatformFramework.ESP32_IDF,
platform_data={KEY_VARIANT: VARIANT_ESP32},
full_config={CONF_ESPHOME: {}},
)
from esphome.components.psram import CONFIG_SCHEMA
config = CONFIG_SCHEMA({})
assert config["mode"] == "quad"
assert config["speed"] == "40MHZ"
assert config["disabled"] is False
assert config["ignore_not_found"] is True
def test_psram_requires_mode_on_multi_mode_variant(
set_core_config: SetCoreConfigCallable,
) -> None:
"""A variant with multiple modes requires an explicit mode selection."""
set_core_config(
PlatformFramework.ESP32_IDF,
platform_data={KEY_VARIANT: VARIANT_ESP32S3},
full_config={CONF_ESPHOME: {}},
)
from esphome.components.psram import CONFIG_SCHEMA
with pytest.raises(cv.Invalid, match=r"requires PSRAM mode selection"):
CONFIG_SCHEMA({})
def test_psram_rejects_mode_invalid_for_variant(
set_core_config: SetCoreConfigCallable,
) -> None:
"""A mode not supported by the active variant is rejected by the schema."""
set_core_config(
PlatformFramework.ESP32_IDF,
platform_data={KEY_VARIANT: VARIANT_ESP32},
full_config={CONF_ESPHOME: {}},
)
from esphome.components.psram import CONFIG_SCHEMA
with pytest.raises(cv.Invalid, match=r"Unknown value 'octal'"):
CONFIG_SCHEMA({"mode": "octal"})
def _setup_psram_final_validation_test(
esp32_config: dict,
set_core_config: SetCoreConfigCallable,
+1 -1
View File
@@ -1,5 +1,5 @@
sensor:
- id: my_sensor
- id: adc_my_sensor
platform: adc
pin: P23
name: ADC Test sensor
+1 -1
View File
@@ -1,5 +1,5 @@
sensor:
- id: my_sensor
- id: adc_my_sensor
platform: adc
pin: GPIO1
name: ADC Test sensor
+1 -1
View File
@@ -1,5 +1,5 @@
sensor:
- id: my_sensor
- id: adc_my_sensor
platform: adc
pin: GPIO1
name: ADC Test sensor
+1 -1
View File
@@ -1,5 +1,5 @@
sensor:
- id: my_sensor
- id: adc_my_sensor
platform: adc
pin: A0
name: ADC Test sensor
+1 -1
View File
@@ -1,5 +1,5 @@
sensor:
- id: my_sensor
- id: adc_my_sensor
platform: adc
pin: GPIO16
name: ADC Test sensor
+1 -1
View File
@@ -1,5 +1,5 @@
sensor:
- id: my_sensor
- id: adc_my_sensor
platform: adc
pin: GPIO1
name: ADC Test sensor
+1 -1
View File
@@ -1,5 +1,5 @@
sensor:
- id: my_sensor
- id: adc_my_sensor
platform: adc
pin: GPIO1
name: ADC Test sensor
+1 -1
View File
@@ -1,5 +1,5 @@
sensor:
- id: my_sensor
- id: adc_my_sensor
platform: adc
pin: VCC
name: ADC Test sensor
+1 -1
View File
@@ -1,5 +1,5 @@
sensor:
- id: my_sensor
- id: adc_my_sensor
platform: adc
pin: A5
name: ADC Test sensor
+1 -1
View File
@@ -1,5 +1,5 @@
sensor:
- id: my_sensor
- id: adc_my_sensor
platform: adc
pin: VCC
name: ADC Test sensor
@@ -1,5 +1,5 @@
sensor:
- id: my_sensor
- id: adc_my_sensor
platform: adc
pin: VCC
name: ADC Test sensor
@@ -1,6 +1,6 @@
binary_sensor:
- platform: gpio
id: bin1
id: alarm_control_panel_bin1
pin: 1
alarm_control_panel:
@@ -18,7 +18,7 @@ alarm_control_panel:
pending_time: 15s
trigger_time: 30s
binary_sensors:
- input: bin1
- input: alarm_control_panel_bin1
bypass_armed_home: true
bypass_armed_night: true
bypass_auto: true
@@ -39,7 +39,7 @@ alarm_control_panel:
pending_time: 15s
trigger_time: 30s
binary_sensors:
- input: bin1
- input: alarm_control_panel_bin1
bypass_armed_home: true
bypass_armed_night: true
bypass_auto: true
@@ -4,7 +4,7 @@ packages:
display:
- platform: ili9xxx
id: main_lcd
id: animation_main_lcd
spi_id: spi_bus
model: ili9342
cs_pin: 12
@@ -4,7 +4,7 @@ packages:
display:
- platform: ili9xxx
id: main_lcd
id: animation_main_lcd
spi_id: spi_bus
model: ili9342
cs_pin: 5
@@ -4,7 +4,7 @@ packages:
display:
- platform: ili9xxx
id: main_lcd
id: animation_main_lcd
spi_id: spi_bus
model: ili9342
cs_pin: 20
+1 -1
View File
@@ -296,7 +296,7 @@ api:
event:
- platform: template
name: Test Event
id: test_event
id: api_test_event
event_types:
- single_click
- double_click
+1 -1
View File
@@ -1,5 +1,5 @@
audio_file:
- id: test_audio
- id: audio_file_test_audio
file:
type: local
path: $component_dir/test.wav
@@ -1,5 +1,5 @@
audio_file:
- id: test_audio
- id: audio_file_test_audio
file:
type: local
path: $component_dir/test.wav
+2 -2
View File
@@ -1,7 +1,7 @@
display:
- platform: ssd1306_i2c
i2c_id: i2c_bus
id: ssd1306_i2c_display
id: axs15231_ssd1306_i2c_display
model: SSD1306_128X64
reset_pin: 19
pages:
@@ -13,6 +13,6 @@ touchscreen:
- platform: axs15231
i2c_id: i2c_bus
id: axs15231_touchscreen
display: ssd1306_i2c_display
display: axs15231_ssd1306_i2c_display
interrupt_pin: 20
reset_pin: 18
@@ -4,7 +4,7 @@ packages:
display:
- platform: ssd1306_i2c
i2c_id: i2c_bus
id: ssd1306_display
id: axs15231_ssd1306_display
model: SSD1306_128X64
reset_pin: 13
pages:
@@ -15,5 +15,5 @@ display:
touchscreen:
- platform: axs15231
i2c_id: i2c_bus
display: ssd1306_display
display: axs15231_ssd1306_display
interrupt_pin: 12
+6 -6
View File
@@ -1,6 +1,6 @@
switch:
- platform: template
id: template_switch1
id: bang_bang_template_switch1
optimistic: true
- platform: template
id: template_switch2
@@ -8,7 +8,7 @@ switch:
sensor:
- platform: template
id: template_sensor1
id: bang_bang_template_sensor1
lambda: |-
if (millis() > 10000) {
return 42.0;
@@ -20,16 +20,16 @@ sensor:
climate:
- platform: bang_bang
name: Bang Bang Climate
sensor: template_sensor1
humidity_sensor: template_sensor1
sensor: bang_bang_template_sensor1
humidity_sensor: bang_bang_template_sensor1
default_target_temperature_low: 18°C
default_target_temperature_high: 24°C
idle_action:
- switch.turn_on: template_switch1
- switch.turn_on: bang_bang_template_switch1
cool_action:
- switch.turn_on: template_switch2
heat_action:
- switch.turn_on: template_switch1
- switch.turn_on: bang_bang_template_switch1
away_config:
default_target_temperature_low: 16°C
default_target_temperature_high: 20°C
+3 -3
View File
@@ -1,7 +1,7 @@
binary_sensor:
- platform: template
trigger_on_initial_state: true
id: some_binary_sensor
id: binary_sensor_some_binary_sensor
name: "Random binary"
lambda: return (random_uint32() & 1) == 0;
filters:
@@ -21,7 +21,7 @@ binary_sensor:
time_off: 100ms
time_on: 400ms
- lambda: |-
if (id(some_binary_sensor).state) {
if (id(binary_sensor_some_binary_sensor).state) {
return x;
}
return {};
@@ -36,7 +36,7 @@ binary_sensor:
- logger.log:
format: "New state is %s"
args: ['x.has_value() ? ONOFF(x) : "Unknown"']
- binary_sensor.invalidate_state: some_binary_sensor
- binary_sensor.invalidate_state: binary_sensor_some_binary_sensor
# Test autorepeat with default configuration (no timings)
- platform: template
+12 -12
View File
@@ -1,20 +1,20 @@
binary_sensor:
- platform: template
id: bin1
id: binary_sensor_map_bin1
lambda: |-
if (millis() > 10000) {
return true;
}
return false;
- platform: template
id: bin2
id: binary_sensor_map_bin2
lambda: |-
if (millis() > 20000) {
return true;
}
return false;
- platform: template
id: bin3
id: binary_sensor_map_bin3
lambda: |-
if (millis() > 30000) {
return true;
@@ -26,33 +26,33 @@ sensor:
name: Binary Sensor Map Group
type: group
channels:
- binary_sensor: bin1
- binary_sensor: binary_sensor_map_bin1
value: 10.0
- binary_sensor: bin2
- binary_sensor: binary_sensor_map_bin2
value: 15.0
- binary_sensor: bin3
- binary_sensor: binary_sensor_map_bin3
value: 100.0
- platform: binary_sensor_map
name: Binary Sensor Map Sum
type: sum
channels:
- binary_sensor: bin1
- binary_sensor: binary_sensor_map_bin1
value: 10.0
- binary_sensor: bin2
- binary_sensor: binary_sensor_map_bin2
value: 15.0
- binary_sensor: bin3
- binary_sensor: binary_sensor_map_bin3
value: 100.0
- platform: binary_sensor_map
name: Binary Sensor Map Bayesian
type: bayesian
prior: 0.4
observations:
- binary_sensor: bin1
- binary_sensor: binary_sensor_map_bin1
prob_given_true: 0.9
prob_given_false: 0.4
- binary_sensor: bin2
- binary_sensor: binary_sensor_map_bin2
prob_given_true: 0.7
prob_given_false: 0.05
- binary_sensor: bin3
- binary_sensor: binary_sensor_map_bin3
prob_given_true: 0.8
prob_given_false: 0.2
+2 -2
View File
@@ -56,7 +56,7 @@ sensor:
number:
- platform: template
name: "Test Number"
id: test_number
id: ble_client_test_number
optimistic: true
min_value: 0
max_value: 255
@@ -72,5 +72,5 @@ button:
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;
uint8_t val = (uint8_t)id(ble_client_test_number).state;
return std::vector<uint8_t>{0xAA, val, 0xBB};
+68
View File
@@ -0,0 +1,68 @@
sensor:
- platform: bmi270
name: "BMI270 Temperature"
- platform: motion
type: acceleration_x
name: "Accel X"
accuracy_decimals: 4
filters:
- sliding_window_moving_average:
window_size: 4
send_every: 1
- platform: motion
type: acceleration_y
name: "Accel Y"
accuracy_decimals: 4
- platform: motion
type: acceleration_z
name: "Accel Z"
accuracy_decimals: 4
# Gyroscope axes (unit: °/s)
- platform: motion
type: gyroscope_x
name: "Gyro X"
- platform: motion
type: gyroscope_y
name: "Gyro Y"
- platform: motion
type: gyroscope_z
name: "Gyro Z"
- platform: motion
type: angular_rate_x
name: "Angular Rate X"
- platform: motion
type: angular_rate_y
name: "Angular Rate Y"
- platform: motion
type: angular_rate_z
name: "Angular Rate Z"
- platform: motion
type: pitch
name: "Pitch"
- platform: motion
type: roll
name: "Roll"
motion:
- platform: bmi270
# Accelerometer full-scale range: 2G | 4G | 8G | 16G
accelerometer_range: 4G
# Accelerometer output data rate: 12_5HZ | 25HZ | 50HZ | 100HZ |
# 200HZ | 400HZ | 800HZ | 1600HZ
accelerometer_odr: 100HZ
# Gyroscope full-scale range: 125DPS | 250DPS | 500DPS | 1000DPS | 2000DPS
gyroscope_range: 2000DPS
# Gyroscope output data rate: 25HZ | 50HZ | 100HZ | 200HZ |
# 400HZ | 800HZ | 1600HZ | 3200HZ
gyroscope_odr: 200HZ
axis_map:
x: y
y: x
z: -z
@@ -0,0 +1,4 @@
packages:
i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml
<<: !include common.yaml
+3 -3
View File
@@ -1,6 +1,6 @@
canbus:
- platform: esp32_can
id: esp32_internal_can
id: canbus_esp32_internal_can
rx_pin: 4
tx_pin: 5
can_id: 4
@@ -40,7 +40,7 @@ canbus:
number:
- platform: template
name: "Test Number"
id: test_number
id: canbus_test_number
optimistic: true
min_value: 0
max_value: 255
@@ -62,5 +62,5 @@ button:
- canbus.send: !lambda return {0, 1, 2};
# Test canbus.send with lambda that references a component (function pointer)
- canbus.send: !lambda |-
uint8_t val = (uint8_t)id(test_number).state;
uint8_t val = (uint8_t)id(canbus_test_number).state;
return std::vector<uint8_t>{0xAA, val, 0xBB};
+3 -3
View File
@@ -6,13 +6,13 @@ cd74hc4067:
sensor:
- platform: adc
id: esp_adc_sensor
id: cd74hc4067_esp_adc_sensor
pin: ${pin}
- platform: cd74hc4067
id: cd74hc4067_adc_0
number: 0
sensor: esp_adc_sensor
sensor: cd74hc4067_esp_adc_sensor
- platform: cd74hc4067
id: cd74hc4067_adc_1
number: 1
sensor: esp_adc_sensor
sensor: cd74hc4067_esp_adc_sensor
+2 -2
View File
@@ -1,6 +1,6 @@
sensor:
- platform: template
id: temp_sensor
id: climate_ir_lg_temp_sensor
lambda: return 22.0;
update_interval: 60s
- platform: template
@@ -12,5 +12,5 @@ climate:
- platform: climate_ir_lg
name: LG Climate
transmitter_id: xmitr
sensor: temp_sensor
sensor: climate_ir_lg_temp_sensor
humidity_sensor: humidity_sensor
@@ -1,15 +1,15 @@
output:
- platform: ${light_platform}
id: light_output_1
id: color_temperature_light_output_1
pin: ${pin_o1}
- platform: ${light_platform}
id: light_output_2
id: color_temperature_light_output_2
pin: ${pin_o2}
light:
- platform: color_temperature
name: Lights
color_temperature: light_output_1
brightness: light_output_2
color_temperature: color_temperature_light_output_1
brightness: color_temperature_light_output_2
cold_white_color_temperature: 153 mireds
warm_white_color_temperature: 500 mireds
-37
View File
@@ -1,37 +0,0 @@
display:
- platform: qspi_dbi
model: RM690B0
data_rate: 80MHz
spi_mode: mode0
dimensions:
width: 450
height: 600
offset_width: 16
color_order: rgb
invert_colors: false
brightness: 255
cs_pin: 11
reset_pin: 13
enable_pin: 9
- platform: qspi_dbi
model: CUSTOM
id: main_lcd
draw_from_origin: true
dimensions:
height: 240
width: 536
transform:
mirror_x: true
swap_xy: true
color_order: rgb
brightness: 255
cs_pin: 6
reset_pin: 17
enable_pin: 38
init_sequence:
- [0x3A, 0x66]
- [0x11]
- delay 120ms
- [0x29]
- delay 20ms
@@ -1,4 +0,0 @@
packages:
qspi: !include ../../test_build_components/common/qspi/esp32-s3-idf.yaml
<<: !include common.yaml
+4 -4
View File
@@ -1,17 +1,17 @@
output:
- platform: ${pwm_platform}
id: fan_output_1
id: copy_fan_output_1
pin: ${pin}
fan:
- platform: speed
id: fan_speed
output: fan_output_1
id: copy_fan_speed
output: copy_fan_output_1
preset_modes:
- Eco
- Turbo
- platform: copy
source_id: fan_speed
source_id: copy_fan_speed
name: Fan Speed Copy
select:
+2 -2
View File
@@ -1,9 +1,9 @@
sensor:
- platform: adc
id: esp_adc_sensor
id: ct_clamp_esp_adc_sensor
pin: ${pin}
- platform: ct_clamp
sensor: esp_adc_sensor
sensor: ct_clamp_esp_adc_sensor
name: CT Clamp
sample_duration: 500ms
update_interval: 5s
+3 -3
View File
@@ -31,7 +31,7 @@ sensor:
switch:
- platform: template
id: template_switch1
id: current_based_template_switch1
optimistic: true
- platform: template
id: template_switch2
@@ -46,7 +46,7 @@ cover:
open_obstacle_current_threshold: 0.8
open_duration: 12s
open_action:
- switch.turn_on: template_switch1
- switch.turn_on: current_based_template_switch1
close_sensor: ade7953_current_b
close_moving_current_threshold: 0.5
close_obstacle_current_threshold: 0.8
@@ -54,7 +54,7 @@ cover:
close_action:
- switch.turn_on: template_switch2
stop_action:
- switch.turn_off: template_switch1
- switch.turn_off: current_based_template_switch1
- switch.turn_off: template_switch2
obstacle_rollback: 30%
start_sensing_delay: 0.8s
+2 -2
View File
@@ -1,8 +1,8 @@
light:
- platform: cwww
name: CWWW Light
cold_white: light_output_1
warm_white: light_output_2
cold_white: cwww_light_output_1
warm_white: cwww_light_output_2
cold_white_color_temperature: 153 mireds
warm_white_color_temperature: 500 mireds
constant_brightness: true
+2 -2
View File
@@ -5,11 +5,11 @@ substitutions:
output:
- platform: ${light_platform}
id: light_output_1
id: cwww_light_output_1
pin: ${pin_o1}
channel: 0
- platform: ${light_platform}
id: light_output_2
id: cwww_light_output_2
pin: ${pin_o2}
channel: 1
phase_angle: 180°
+2 -2
View File
@@ -5,10 +5,10 @@ substitutions:
output:
- platform: ${light_platform}
id: light_output_1
id: cwww_light_output_1
pin: ${pin_o1}
- platform: ${light_platform}
id: light_output_2
id: cwww_light_output_2
pin: ${pin_o2}
<<: !include common.yaml
+2 -2
View File
@@ -5,10 +5,10 @@ substitutions:
output:
- platform: ${light_platform}
id: light_output_1
id: cwww_light_output_1
pin: ${pin_o1}
- platform: ${light_platform}
id: light_output_2
id: cwww_light_output_2
pin: ${pin_o2}
<<: !include common.yaml
@@ -1,11 +0,0 @@
dlms_meter:
decryption_key: "36C66639E48A8CA4D6BC8B282A793BBB" # change this to your decryption key!
sensor:
- platform: dlms_meter
reactive_energy_plus:
name: "Reactive energy taken from grid"
reactive_energy_minus:
name: "Reactive energy put into grid"
<<: !include common.yaml
@@ -1,17 +0,0 @@
dlms_meter:
decryption_key: "36C66639E48A8CA4D6BC8B282A793BBB" # change this to your decryption key!
provider: netznoe # (optional) key - only set if using evn
sensor:
- platform: dlms_meter
# EVN
power_factor:
name: "Power Factor"
text_sensor:
- platform: dlms_meter
# EVN
meternumber:
name: "meterNumber"
<<: !include common.yaml
+40
View File
@@ -1,4 +1,16 @@
dlms_meter:
id: dlms_meter_hub
receive_timeout: 50ms
decryption_key: "36C66639E48A8CA4D6BC8B282A793BBB"
auth_key: "11223344556677889900AABBCCDDEEFF"
skip_crc: true
provider: "netznoe"
custom_patterns:
- "custom_pattern_1"
- "custom_pattern_2"
sensor:
# Old Schema tests
- platform: dlms_meter
voltage_l1:
name: "Voltage L1"
@@ -20,8 +32,36 @@ sensor:
name: "Active energy taken from grid"
active_energy_minus:
name: "Active energy put into grid"
reactive_energy_plus:
name: "Reactive energy taken from grid"
reactive_energy_minus:
name: "Reactive energy put into grid"
power_factor:
name: "Power factor"
# Dynamic Schema tests
- platform: dlms_meter
dlms_meter_id: dlms_meter_hub
obis_code: "1-0:99.99.9"
name: "Custom Dynamic Sensor"
text_sensor:
# Old Schema tests
- platform: dlms_meter
timestamp:
name: "timestamp"
meternumber:
name: "Meter Number"
# Dynamic Schema tests
- platform: dlms_meter
dlms_meter_id: dlms_meter_hub
obis_code: "0-0:99.99.9"
name: "Custom Dynamic Text Sensor"
binary_sensor:
# Dynamic Schema tests (Binary sensors only use the dynamic schema)
- platform: dlms_meter
dlms_meter_id: dlms_meter_hub
obis_code: "0-1:2.3.4"
name: "Custom Binary Sensor"
@@ -1,4 +1,4 @@
packages:
uart_2400: !include ../../test_build_components/common/uart_2400/esp32-ard.yaml
uart: !include ../../test_build_components/common/uart/esp32-ard.yaml
<<: !include common-generic.yaml
<<: !include common.yaml
@@ -1,4 +1,4 @@
packages:
uart_2400: !include ../../test_build_components/common/uart_2400/esp32-idf.yaml
uart: !include ../../test_build_components/common/uart/esp32-idf.yaml
<<: !include common-netznoe.yaml
<<: !include common.yaml
@@ -1,4 +1,4 @@
packages:
uart_2400: !include ../../test_build_components/common/uart_2400/esp8266-ard.yaml
uart: !include ../../test_build_components/common/uart/esp8266-ard.yaml
<<: !include common-generic.yaml
<<: !include common.yaml
@@ -0,0 +1,4 @@
packages:
uart: !include ../../test_build_components/common/uart/rp2040-ard.yaml
<<: !include common.yaml
+2 -2
View File
@@ -1,6 +1,6 @@
binary_sensor:
- platform: template
id: bin1
id: duty_time_bin1
lambda: |-
if (millis() > 10000) {
return true;
@@ -10,4 +10,4 @@ binary_sensor:
sensor:
- platform: duty_time
name: Duty Time
sensor: bin1
sensor: duty_time_bin1
+1 -1
View File
@@ -2,7 +2,7 @@
light:
- platform: rp2040_pio_led_strip
id: led_strip
id: e131_led_strip
pin: 2
pio: 0
num_leds: 256
+2 -2
View File
@@ -1,7 +1,7 @@
display:
- platform: ssd1306_i2c
i2c_id: i2c_bus
id: ssd1306_i2c_display
id: ektf2232_ssd1306_i2c_display
model: SSD1306_128X64
reset_pin: ${display_reset_pin}
pages:
@@ -15,7 +15,7 @@ touchscreen:
id: ektf2232_touchscreen
interrupt_pin: ${interrupt_pin}
reset_pin: ${touch_reset_pin}
display: ssd1306_i2c_display
display: ektf2232_ssd1306_i2c_display
on_touch:
- logger.log:
format: Touch at (%d, %d)
+6 -6
View File
@@ -1,6 +1,6 @@
binary_sensor:
- platform: template
id: bin1
id: endstop_bin1
lambda: |-
if (millis() > 10000) {
return true;
@@ -9,7 +9,7 @@ binary_sensor:
switch:
- platform: template
id: template_switch1
id: endstop_template_switch1
optimistic: true
- platform: template
id: template_switch2
@@ -20,12 +20,12 @@ cover:
id: endstop_cover
name: Endstop Cover
stop_action:
- switch.turn_on: template_switch1
open_endstop: bin1
- switch.turn_on: endstop_template_switch1
open_endstop: endstop_bin1
open_action:
- switch.turn_on: template_switch1
- switch.turn_on: endstop_template_switch1
open_duration: 5min
close_endstop: bin1
close_endstop: endstop_bin1
close_action:
- switch.turn_on: template_switch2
close_duration: 4.5min
+1 -1
View File
@@ -13,7 +13,7 @@ esphome:
canbus:
- platform: esp32_can
id: esp32_internal_can
id: esp32_can_esp32_internal_can
rx_pin: ${rx_pin}
tx_pin: ${tx_pin}
can_id: 4
@@ -3,20 +3,20 @@ esphome:
then:
- canbus.send:
# Extended ID explicit
canbus_id: esp32_internal_can
canbus_id: esp32_can_esp32_internal_can
use_extended_id: true
can_id: 0x100
data: [0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08]
- canbus.send:
# Standard ID by default
canbus_id: esp32_internal_can
canbus_id: esp32_can_esp32_internal_can
can_id: 0x100
data: [0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08]
# Note: esp32_internal_can_2 uses LISTENONLY mode, so no send actions
canbus:
- platform: esp32_can
id: esp32_internal_can
id: esp32_can_esp32_internal_can
rx_pin: GPIO8
tx_pin: GPIO7
can_id: 4
+3 -3
View File
@@ -62,7 +62,7 @@ packet_transport:
encryption:
key: "0123456789abcdef0123456789abcdef"
sensors:
- temp_sensor
- espnow_temp_sensor
providers:
- name: test-provider
encryption:
@@ -70,9 +70,9 @@ packet_transport:
sensor:
- platform: internal_temperature
id: temp_sensor
id: espnow_temp_sensor
- platform: packet_transport
provider: test-provider
remote_id: temp_sensor
remote_id: espnow_temp_sensor
id: remote_temp
@@ -0,0 +1,39 @@
ethernet:
id: eth
type: W5500
clk_pin: 19
mosi_pin: 21
miso_pin: 23
cs_pin: 18
interrupt_pin: 36
reset_pin: 22
enable_on_boot: false
manual_ip:
static_ip: 192.168.178.56
gateway: 192.168.178.1
subnet: 255.255.255.0
mac_address: "02:AA:BB:CC:DD:01"
interface: spi2
esphome:
on_boot:
priority: 200
then:
- if:
condition:
not:
ethernet.enabled:
then:
- ethernet.enable:
button:
- platform: template
name: "Disable Ethernet"
on_press:
- ethernet.disable:
binary_sensor:
- platform: template
name: "Ethernet Connected"
lambda: |-
return id(eth).is_connected();
@@ -1,6 +1,6 @@
light:
- platform: fastled_clockless
id: addr1
id: fastled_clockless_addr1
chipset: WS2811
pin: 13
num_leds: 100
@@ -59,13 +59,13 @@ light:
name: Custom Effect
sequence:
- light.addressable_set:
id: addr1
id: fastled_clockless_addr1
red: 100%
green: 100%
blue: 0%
- delay: 100ms
- light.addressable_set:
id: addr1
id: fastled_clockless_addr1
red: 0%
green: 100%
blue: 0%
+3 -3
View File
@@ -1,6 +1,6 @@
light:
- platform: fastled_spi
id: addr1
id: fastled_spi_addr1
chipset: WS2801
clock_pin: 22
data_pin: 23
@@ -59,13 +59,13 @@ light:
name: Custom Effect
sequence:
- light.addressable_set:
id: addr1
id: fastled_spi_addr1
red: 100%
green: 100%
blue: 0%
- delay: 100ms
- light.addressable_set:
id: addr1
id: fastled_spi_addr1
red: 0%
green: 100%
blue: 0%
+3 -3
View File
@@ -8,7 +8,7 @@ font:
id: roboto32
- file: "gfonts://Roboto"
id: roboto
id: font_roboto
size: 20
glyphs: "0123456789."
extras:
@@ -50,11 +50,11 @@ font:
display:
- platform: ssd1306_i2c
i2c_id: i2c_bus
id: ssd1306_display
id: font_ssd1306_display
model: SSD1306_128X64
reset_pin: ${display_reset_pin}
lambda: |-
it.print(0, 0, id(roboto), "Hello, World!");
it.print(0, 0, id(font_roboto), "Hello, World!");
it.print(0, 20, id(roboto_web), "Hello, World!");
it.print(0, 40, id(monocraft), "Hello, World!");
it.print(0, 60, id(monocraft2), "Hello, World!");
+3 -3
View File
@@ -8,7 +8,7 @@ font:
id: roboto32
- file: "gfonts://Roboto"
id: roboto
id: font_roboto
size: 20
glyphs: "0123456789."
extras:
@@ -44,12 +44,12 @@ font:
display:
- platform: sdl
id: sdl_display
id: font_sdl_display
dimensions:
width: 800
height: 600
lambda: |-
it.print(0, 0, id(roboto), "Hello, World!");
it.print(0, 0, id(font_roboto), "Hello, World!");
it.print(0, 20, id(roboto_web), "Hello, World!");
it.print(0, 40, id(roboto_greek), "Hello κόσμε!");
it.print(0, 60, id(monocraft), "Hello, World!");
+1 -1
View File
@@ -12,7 +12,7 @@ graph:
display:
- platform: ssd1306_i2c
i2c_id: i2c_bus
id: ssd1306_display
id: graph_ssd1306_display
model: SSD1306_128X64
reset_pin: ${reset_pin}
pages:
@@ -1,6 +1,7 @@
display:
- platform: ssd1306_i2c
id: ssd1306_i2c_display
i2c_id: i2c_bus
id: graphical_display_menu_ssd1306_i2c_display
model: SSD1306_128X64
reset_pin: ${reset_pin}
pages:
@@ -10,12 +11,12 @@ display:
font:
- file: "gfonts://Roboto"
id: roboto
id: graphical_display_menu_roboto
size: 20
number:
- platform: template
id: test_number
id: graphical_display_menu_test_number
min_value: 0
step: 1
max_value: 10
@@ -31,13 +32,13 @@ select:
switch:
- platform: template
id: test_switch
id: graphical_display_menu_test_switch
optimistic: true
graphical_display_menu:
id: test_graphical_display_menu
display: ssd1306_i2c_display
font: roboto
display: graphical_display_menu_ssd1306_i2c_display
font: graphical_display_menu_roboto
active: false
mode: rotary
on_enter:
@@ -80,7 +81,7 @@ graphical_display_menu:
lambda: 'ESP_LOGI("graphical_display_menu", "select value: %s, %s", it->get_text().c_str(), it->get_value_text().c_str());'
- type: number
text: "Number"
number: test_number
number: graphical_display_menu_test_number
on_enter:
then:
lambda: 'ESP_LOGI("graphical_display_menu", "number enter: %s, %s", it->get_text().c_str(), it->get_value_text().c_str());'
@@ -97,7 +98,7 @@ graphical_display_menu:
- display_menu.hide: test_graphical_display_menu
- type: switch
text: "Switch"
switch: test_switch
switch: graphical_display_menu_test_switch
on_text: "Bright"
off_text: "Dark"
immediate_edit: false
+2 -2
View File
@@ -1,7 +1,7 @@
display:
- platform: ssd1306_i2c
i2c_id: i2c_bus
id: ssd1306_i2c_display
id: gt911_ssd1306_i2c_display
model: SSD1306_128X64
reset_pin: ${display_reset_pin}
pages:
@@ -13,7 +13,7 @@ touchscreen:
- platform: gt911
i2c_id: i2c_bus
id: gt911_touchscreen
display: ssd1306_i2c_display
display: gt911_ssd1306_i2c_display
interrupt_pin: ${interrupt_pin}
reset_pin: ${reset_pin}
+2 -2
View File
@@ -93,12 +93,12 @@ text_sensor:
event:
- platform: template
name: Test Event
id: test_event
id: homeassistant_test_event
event_types:
- test_event_type
on_event:
- homeassistant.event:
event: esphome.test_event
event: esphome.homeassistant_test_event
data:
event_name: !lambda |-
return event_type;
+4
View File
@@ -0,0 +1,4 @@
packages:
i2c: !include ../../test_build_components/common/i2c/host.yaml
<<: !include common.yaml
@@ -3,7 +3,7 @@ i2s_audio:
speaker:
- platform: i2s_audio
id: speaker_id
id: spdif_speaker_id
dac_type: external
i2s_dout_pin: ${spdif_data_pin}
spdif_mode: true
+1 -1
View File
@@ -3,7 +3,7 @@ packages:
display:
- platform: ili9xxx
id: main_lcd
id: image_main_lcd
spi_id: spi_bus
model: ili9342
cs_pin: 15
+1 -1
View File
@@ -3,7 +3,7 @@ packages:
display:
- platform: ili9xxx
id: main_lcd
id: image_main_lcd
spi_id: spi_bus
model: ili9342
cs_pin: 5
+1 -1
View File
@@ -3,7 +3,7 @@ packages:
display:
- platform: ili9xxx
id: main_lcd
id: image_main_lcd
spi_id: spi_bus
model: ili9342
cs_pin: 20
+1 -1
View File
@@ -23,7 +23,7 @@ infrared:
# Infrared receiver
- platform: ir_rf_proxy
id: ir_rx
id: infrared_ir_rx
name: "IR Receiver"
remote_receiver_id: ir_receiver
@@ -9,11 +9,11 @@ esphome:
sensor:
- platform: adc
id: my_sensor
id: integration_my_sensor
pin: ${pin}
attenuation: 12db
- platform: integration
id: integration_sensor
sensor: my_sensor
sensor: integration_my_sensor
name: Integration Sensor
time_unit: s
@@ -1,8 +1,8 @@
sensor:
- platform: adc
id: my_sensor
id: integration_my_sensor
pin: VCC
- platform: integration
sensor: my_sensor
sensor: integration_my_sensor
name: Integration Sensor
time_unit: s
@@ -1,8 +1,8 @@
sensor:
- platform: adc
id: my_sensor
id: integration_my_sensor
pin: VCC
- platform: integration
sensor: my_sensor
sensor: integration_my_sensor
name: Integration Sensor
time_unit: s
+1 -1
View File
@@ -6,7 +6,7 @@ remote_receiver:
infrared:
# Infrared receiver
- platform: ir_rf_proxy
id: ir_rx
id: ir_rf_proxy_ir_rx
name: "IR Receiver"
receiver_frequency: 38kHz
remote_receiver_id: ir_receiver
+1 -1
View File
@@ -1,6 +1,6 @@
display:
- platform: lcd_gpio
id: my_lcd_gpio
id: lcd_gpio_my_lcd_gpio
dimensions: 18x4
data_pins:
- number: ${d0_pin}
+4 -4
View File
@@ -1,6 +1,6 @@
number:
- platform: template
id: test_number
id: lcd_menu_test_number
min_value: 0
step: 1
max_value: 10
@@ -22,7 +22,7 @@ switch:
display:
- platform: lcd_gpio
id: my_lcd_gpio
id: lcd_menu_my_lcd_gpio
dimensions: 18x4
data_pins:
- number: ${d0_pin}
@@ -36,7 +36,7 @@ display:
lcd_menu:
id: test_lcd_menu
display_id: my_lcd_gpio
display_id: lcd_menu_my_lcd_gpio
mark_back: 0x5e
mark_selected: 0x3e
mark_editing: 0x2a
@@ -83,7 +83,7 @@ lcd_menu:
lambda: 'ESP_LOGI("lcd_menu", "select value: %s, %s", it->get_text().c_str(), it->get_value_text().c_str());'
- type: number
text: Number
number: test_number
number: lcd_menu_test_number
on_enter:
then:
lambda: 'ESP_LOGI("lcd_menu", "number enter: %s, %s", it->get_text().c_str(), it->get_value_text().c_str());'
+4 -1
View File
@@ -156,7 +156,7 @@ light:
- platform: binary
id: test_binary_light
name: Binary Light
output: test_binary
output: light_test_binary
effects:
- strobe:
on_state:
@@ -182,6 +182,9 @@ light:
state += 1;
if (state == 4)
state = 0;
if (initial_run) {
ESP_LOGD("custom_effect", "Effect %s started", it.get_name().c_str());
}
- pulse:
transition_length: 10s
update_interval: 20s
+1 -1
View File
@@ -1,6 +1,6 @@
output:
- platform: gpio
id: test_binary
id: light_test_binary
pin: 12
- platform: ledc
id: test_ledc_1
+1 -1
View File
@@ -1,6 +1,6 @@
output:
- platform: gpio
id: test_binary
id: light_test_binary
pin: 4
- platform: esp8266_pwm
id: test_ledc_1
@@ -5,14 +5,14 @@ esphome:
output:
- platform: gpio
id: test_binary
id: light_test_binary
pin: 0
light:
- platform: binary
id: test_binary_light
name: Binary Light
output: test_binary
output: light_test_binary
effects:
- strobe:
on_state:

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