mirror of
https://github.com/esphome/esphome.git
synced 2026-10-07 03:16:37 +00:00
Merge remote-tracking branch 'upstream/dev' into neutral-ble-client
This commit is contained in:
@@ -39,12 +39,13 @@ def test_trigger_codegen(
|
||||
"set_manufacturer_uuid128((uint8_t*)(const uint8_t[16]){0xCD,0xAB,0xCD,0xAB,"
|
||||
"0xCD,0xAB,0xCD,0xAB,0xCD,0xAB,0xCD,0xAB,0xCD,0xAB,0xCD,0xAB})" in main_cpp
|
||||
)
|
||||
# scan-control actions: templatable continuous lambda + parented actions.
|
||||
# scan-control actions: templatable continuous lambda + stop_scan forwarded
|
||||
# straight to the tracker (register_apply_action, no action class).
|
||||
# Exactly one set_continuous: the bare start_scan emits none, pinning the
|
||||
# restore-configured-mode divergence from esp32 against a future default=.
|
||||
assert main_cpp.count("->set_continuous(") == 1
|
||||
assert "startscanaction_id->set_continuous(" in main_cpp
|
||||
assert "stopscanaction_id->set_parent(" in main_cpp
|
||||
assert main_cpp.count("->stop_scan();") == 1
|
||||
# scan_parameters continuous: false reaches the YAML-mode setter, not the
|
||||
# runtime override.
|
||||
assert "->set_configured_continuous(false)" in main_cpp
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
esphome:
|
||||
name: climateir-heatcool
|
||||
|
||||
esp8266:
|
||||
board: esp01_1m
|
||||
|
||||
remote_transmitter:
|
||||
pin: GPIO5
|
||||
carrier_duty_percent: 50%
|
||||
|
||||
climate:
|
||||
- platform: coolix
|
||||
id: test_coolix
|
||||
name: Coolix
|
||||
supports_heat: false
|
||||
@@ -0,0 +1,16 @@
|
||||
esphome:
|
||||
name: climateir-heatcool
|
||||
|
||||
esp8266:
|
||||
board: esp01_1m
|
||||
|
||||
remote_transmitter:
|
||||
pin: GPIO5
|
||||
carrier_duty_percent: 50%
|
||||
|
||||
climate:
|
||||
- platform: coolix
|
||||
id: test_coolix
|
||||
name: Coolix
|
||||
supports_heat: false
|
||||
supports_heat_cool: true
|
||||
@@ -0,0 +1,14 @@
|
||||
esphome:
|
||||
name: climateir-heatcool
|
||||
|
||||
esp8266:
|
||||
board: esp01_1m
|
||||
|
||||
remote_transmitter:
|
||||
pin: GPIO5
|
||||
carrier_duty_percent: 50%
|
||||
|
||||
climate:
|
||||
- platform: coolix
|
||||
id: test_coolix
|
||||
name: Coolix
|
||||
@@ -0,0 +1,15 @@
|
||||
esphome:
|
||||
name: climateir-heatcool
|
||||
|
||||
esp8266:
|
||||
board: esp01_1m
|
||||
|
||||
remote_transmitter:
|
||||
pin: GPIO5
|
||||
carrier_duty_percent: 50%
|
||||
|
||||
climate:
|
||||
- platform: coolix
|
||||
id: test_coolix
|
||||
name: Coolix
|
||||
supports_heat_cool: false
|
||||
@@ -0,0 +1,15 @@
|
||||
esphome:
|
||||
name: climateir-heatcool
|
||||
|
||||
esp8266:
|
||||
board: esp01_1m
|
||||
|
||||
remote_transmitter:
|
||||
pin: GPIO5
|
||||
carrier_duty_percent: 50%
|
||||
|
||||
climate:
|
||||
- platform: coolix
|
||||
id: test_coolix
|
||||
name: Coolix
|
||||
supports_cool: false
|
||||
@@ -0,0 +1,16 @@
|
||||
esphome:
|
||||
name: climateir-heatcool
|
||||
|
||||
esp8266:
|
||||
board: esp01_1m
|
||||
|
||||
remote_transmitter:
|
||||
pin: GPIO5
|
||||
carrier_duty_percent: 50%
|
||||
|
||||
climate:
|
||||
- platform: coolix
|
||||
id: test_coolix
|
||||
name: Coolix
|
||||
supports_heat: false
|
||||
supports_cool: false
|
||||
@@ -0,0 +1,53 @@
|
||||
"""Tests for the supports_heat_cool default resolved in climate_ir code generation."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from pathlib import Path
|
||||
import re
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
def _emitted_value(main_cpp: str) -> str | None:
|
||||
"""Return the argument of the generated set_supports_heat_cool() call, or None if absent."""
|
||||
match = re.search(r"set_supports_heat_cool\((true|false)\)", main_cpp)
|
||||
return match.group(1) if match else None
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("config", "expected"),
|
||||
[
|
||||
("heat_and_cool.yaml", None),
|
||||
("cool_only.yaml", "false"),
|
||||
("heat_only.yaml", "false"),
|
||||
("neither.yaml", "false"),
|
||||
],
|
||||
)
|
||||
def test_default_requires_heat_and_cool(
|
||||
config: str,
|
||||
expected: str | None,
|
||||
generate_main: Callable[[str | Path], str],
|
||||
component_config_path: Callable[[str], Path],
|
||||
) -> None:
|
||||
"""Without the key, HEAT_COOL follows supports_heat and supports_cool."""
|
||||
main_cpp = generate_main(component_config_path(config))
|
||||
assert _emitted_value(main_cpp) == expected
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("config", "expected"),
|
||||
[
|
||||
("cool_only_override_on.yaml", None),
|
||||
("heat_and_cool_override_off.yaml", "false"),
|
||||
],
|
||||
)
|
||||
def test_explicit_key_overrides_default(
|
||||
config: str,
|
||||
expected: str | None,
|
||||
generate_main: Callable[[str | Path], str],
|
||||
component_config_path: Callable[[str], Path],
|
||||
) -> None:
|
||||
"""A cool-only unit can still offer HEAT_COOL, and a heat+cool unit can drop it."""
|
||||
main_cpp = generate_main(component_config_path(config))
|
||||
assert _emitted_value(main_cpp) == expected
|
||||
@@ -0,0 +1,17 @@
|
||||
esphome:
|
||||
name: test
|
||||
on_boot:
|
||||
then:
|
||||
- counter.set_value: 5
|
||||
- counter.set_value:
|
||||
value: 7
|
||||
- counter.increment:
|
||||
value: -3
|
||||
|
||||
esp32:
|
||||
board: esp32dev
|
||||
|
||||
sensor:
|
||||
- platform: counter
|
||||
id: only_counter
|
||||
name: Only Counter
|
||||
@@ -0,0 +1,42 @@
|
||||
esphome:
|
||||
name: test
|
||||
on_boot:
|
||||
then:
|
||||
- counter.set_value:
|
||||
id: counter_a
|
||||
value: 100
|
||||
- counter.set_value:
|
||||
id: counter_b
|
||||
value: -5000000000
|
||||
- counter.increment: counter_a
|
||||
- counter.increment:
|
||||
id: counter_a
|
||||
value: -7
|
||||
- counter.increment:
|
||||
id: counter_b
|
||||
value: !lambda "return 3;"
|
||||
|
||||
esp32:
|
||||
board: esp32dev
|
||||
|
||||
sensor:
|
||||
- platform: template
|
||||
id: source_sensor
|
||||
lambda: return 1.0;
|
||||
- platform: counter
|
||||
id: counter_a
|
||||
name: Counter A
|
||||
sensor: source_sensor
|
||||
- platform: counter
|
||||
id: counter_b
|
||||
name: Counter B
|
||||
restore: false
|
||||
initial_value: -5000000000
|
||||
- platform: counter
|
||||
id: counter_c
|
||||
name: Counter C
|
||||
binary_sensor: source_binary_sensor
|
||||
|
||||
binary_sensor:
|
||||
- platform: template
|
||||
id: source_binary_sensor
|
||||
@@ -0,0 +1,99 @@
|
||||
"""Tests for the counter sensor."""
|
||||
|
||||
from collections.abc import Callable
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from esphome import config_validation as cv
|
||||
from esphome.components.counter.sensor import CONFIG_SCHEMA, COUNTER_VALUE
|
||||
|
||||
INT64_MAX = 2**63 - 1
|
||||
|
||||
|
||||
def test_counter_constructor_arguments(
|
||||
generate_main: Callable[[str | Path], str],
|
||||
component_config_path: Callable[[str], Path],
|
||||
) -> None:
|
||||
"""Restore (on by default) and initial value (zero by default) are constructor arguments."""
|
||||
main_cpp = generate_main(component_config_path("counter_test.yaml"))
|
||||
|
||||
assert "new(counter_a) counter::CounterSensor(true, 0);" in main_cpp
|
||||
assert "new(counter_b) counter::CounterSensor(false, -5000000000LL);" in main_cpp
|
||||
|
||||
|
||||
def test_counter_sensor_option_registers_source(
|
||||
generate_main: Callable[[str | Path], str],
|
||||
component_config_path: Callable[[str], Path],
|
||||
) -> None:
|
||||
"""Only the counter with a sensor option counts that sensor's updates."""
|
||||
main_cpp = generate_main(component_config_path("counter_test.yaml"))
|
||||
|
||||
assert "counter_a->count_updates_from(source_sensor);" in main_cpp
|
||||
assert main_cpp.count("count_updates_from") == 1
|
||||
|
||||
|
||||
def test_counter_binary_sensor_option_registers_source(
|
||||
generate_main: Callable[[str | Path], str],
|
||||
component_config_path: Callable[[str], Path],
|
||||
) -> None:
|
||||
"""Only the counter with a binary_sensor option counts that sensor's changes to true."""
|
||||
main_cpp = generate_main(component_config_path("counter_test.yaml"))
|
||||
|
||||
assert "counter_c->count_true_from(source_binary_sensor);" in main_cpp
|
||||
assert main_cpp.count("count_true_from") == 1
|
||||
|
||||
|
||||
def test_counter_actions(
|
||||
generate_main: Callable[[str | Path], str],
|
||||
component_config_path: Callable[[str], Path],
|
||||
) -> None:
|
||||
"""Values reach the counter as 64-bit integers; increment defaults to one."""
|
||||
main_cpp = generate_main(component_config_path("counter_test.yaml"))
|
||||
|
||||
assert "::counter_a->set_value(100);" in main_cpp
|
||||
assert "::counter_b->set_value(-5000000000LL);" in main_cpp
|
||||
assert "::counter_a->increment(1);" in main_cpp
|
||||
assert "::counter_a->increment(-7);" in main_cpp
|
||||
assert "::counter_b->increment(static_cast<int64_t>(3));" in main_cpp
|
||||
|
||||
|
||||
def test_counter_actions_without_id_use_only_counter(
|
||||
generate_main: Callable[[str | Path], str],
|
||||
component_config_path: Callable[[str], Path],
|
||||
) -> None:
|
||||
"""With one counter configured, the id may be left out of an action."""
|
||||
main_cpp = generate_main(component_config_path("counter_single.yaml"))
|
||||
|
||||
assert "::only_counter->set_value(5);" in main_cpp
|
||||
assert "::only_counter->set_value(7);" in main_cpp
|
||||
assert "::only_counter->increment(-3);" in main_cpp
|
||||
|
||||
|
||||
@pytest.mark.parametrize("value", [INT64_MAX + 1, -INT64_MAX - 1, 1.5])
|
||||
def test_counter_initial_value_must_be_int64(value: float) -> None:
|
||||
with pytest.raises(cv.Invalid):
|
||||
CONFIG_SCHEMA({"id": "c1", "name": "C1", "initial_value": value})
|
||||
|
||||
|
||||
def test_counter_cannot_count_itself() -> None:
|
||||
"""A counter watching its own updates would recurse forever."""
|
||||
with pytest.raises(cv.Invalid, match="cannot count its own updates"):
|
||||
CONFIG_SCHEMA({"id": "c1", "name": "C1", "sensor": "c1"})
|
||||
|
||||
|
||||
def test_counter_accepts_other_source() -> None:
|
||||
config = CONFIG_SCHEMA({"id": "c1", "name": "C1", "sensor": "other"})
|
||||
|
||||
assert config["sensor"].id == "other"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("value", [0, 1, -1, INT64_MAX, -INT64_MAX])
|
||||
def test_counter_value_accepts_int64_range(value: int) -> None:
|
||||
assert COUNTER_VALUE(value) == value
|
||||
|
||||
|
||||
@pytest.mark.parametrize("value", [INT64_MAX + 1, -INT64_MAX - 1, 1.5])
|
||||
def test_counter_value_rejects_out_of_range(value: float) -> None:
|
||||
with pytest.raises(cv.Invalid):
|
||||
COUNTER_VALUE(value)
|
||||
@@ -0,0 +1,22 @@
|
||||
esphome:
|
||||
name: test
|
||||
on_boot:
|
||||
then:
|
||||
- dfrobot_sen0395.settings:
|
||||
id: mmwave
|
||||
output_latency:
|
||||
delay_after_detect: 0.1s
|
||||
delay_after_disappear: 5.0s
|
||||
|
||||
esp32:
|
||||
board: esp32dev
|
||||
framework:
|
||||
type: esp-idf
|
||||
|
||||
uart:
|
||||
tx_pin: GPIO17
|
||||
rx_pin: GPIO16
|
||||
baud_rate: 115200
|
||||
|
||||
dfrobot_sen0395:
|
||||
id: mmwave
|
||||
@@ -0,0 +1,25 @@
|
||||
"""Codegen tests for dfrobot_sen0395 output_latency units."""
|
||||
|
||||
from collections.abc import Callable
|
||||
from pathlib import Path
|
||||
import re
|
||||
|
||||
|
||||
def _latency_return(main_cpp: str, setter: str) -> str:
|
||||
match = re.search(
|
||||
rf"{re.escape(setter)}\(\[\]\(\) -> float \{{\s*return ([^;]+);",
|
||||
main_cpp,
|
||||
)
|
||||
assert match is not None, f"{setter} not found in generated main"
|
||||
return match.group(1)
|
||||
|
||||
|
||||
def test_output_latency_emits_seconds_not_milliseconds(
|
||||
generate_main: Callable[[str | Path], str],
|
||||
component_config_path: Callable[[str], Path],
|
||||
) -> None:
|
||||
"""YAML 0.1s / 5.0s must reach SetLatencyCommand as seconds, not milliseconds."""
|
||||
main_cpp = generate_main(component_config_path("output_latency.yaml"))
|
||||
|
||||
assert _latency_return(main_cpp, "set_delay_after_detect") == "0.1f"
|
||||
assert _latency_return(main_cpp, "set_delay_after_disappear") == "5.0f"
|
||||
@@ -0,0 +1,29 @@
|
||||
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
|
||||
full_update_every: 5
|
||||
dimensions:
|
||||
width: 200
|
||||
height: 200
|
||||
|
||||
binary_sensor:
|
||||
- platform: gpio
|
||||
pin: GPIO27
|
||||
name: Trigger
|
||||
on_press:
|
||||
- epaper_spi.full_update_next: epaper_display
|
||||
@@ -0,0 +1,22 @@
|
||||
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
|
||||
dimensions:
|
||||
width: 200
|
||||
height: 200
|
||||
border_waveform: 0x1A
|
||||
@@ -0,0 +1,19 @@
|
||||
esphome:
|
||||
name: test
|
||||
|
||||
esp32:
|
||||
board: esp32-s3-devkitc-1
|
||||
variant: esp32s3
|
||||
|
||||
psram:
|
||||
mode: octal
|
||||
speed: 80MHz
|
||||
|
||||
spi:
|
||||
clk_pin: GPIO7
|
||||
mosi_pin: GPIO9
|
||||
|
||||
display:
|
||||
- platform: epaper_spi
|
||||
id: epaper_display
|
||||
model: seeed-reterminal-sticky-gray4
|
||||
@@ -312,6 +312,66 @@ def test_model_with_full_update_every(
|
||||
)
|
||||
|
||||
|
||||
def test_update_interval_below_model_minimum_rejected(
|
||||
set_core_config: SetCoreConfigCallable,
|
||||
set_component_config: Callable[[str, Any], None],
|
||||
) -> None:
|
||||
"""update_interval faster than the model's minimum_update_interval is rejected."""
|
||||
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})
|
||||
|
||||
with pytest.raises(cv.Invalid, match="at least"):
|
||||
run_schema_validation(
|
||||
{
|
||||
"id": "test_display",
|
||||
"model": "ssd1677",
|
||||
"dc_pin": 21,
|
||||
"busy_pin": 22,
|
||||
"reset_pin": 23,
|
||||
"cs_pin": 5,
|
||||
"dimensions": {
|
||||
"width": 200,
|
||||
"height": 200,
|
||||
},
|
||||
"update_interval": "500ms",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def test_reset_duration_over_max_rejected(
|
||||
set_core_config: SetCoreConfigCallable,
|
||||
set_component_config: Callable[[str, Any], None],
|
||||
) -> None:
|
||||
"""reset_duration over the 500ms cap is rejected."""
|
||||
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})
|
||||
|
||||
with pytest.raises(cv.Invalid, match="at most"):
|
||||
run_schema_validation(
|
||||
{
|
||||
"id": "test_display",
|
||||
"model": "ssd1677",
|
||||
"dc_pin": 21,
|
||||
"busy_pin": 22,
|
||||
"reset_pin": 23,
|
||||
"cs_pin": 5,
|
||||
"dimensions": {
|
||||
"width": 200,
|
||||
"height": 200,
|
||||
},
|
||||
"reset_duration": "600ms",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def test_busy_pin_input_mode_ssd1677(
|
||||
set_core_config: SetCoreConfigCallable,
|
||||
set_component_config: Callable[[str, Any], None],
|
||||
@@ -481,6 +541,16 @@ def test_enable_pin_code_generation(
|
||||
assert f"set_enable_pins({{{pin_25}, {pin_26}}});" in main_cpp
|
||||
|
||||
|
||||
def test_full_update_next_action_code_generation(
|
||||
generate_main: Callable[[str | Path], str],
|
||||
component_config_path: Callable[[str], Path],
|
||||
) -> None:
|
||||
"""The epaper_spi.full_update_next action targets the configured display."""
|
||||
main_cpp = generate_main(component_config_path("full_update_next_test.yaml"))
|
||||
|
||||
assert "epaper_display->request_full_update();" in main_cpp
|
||||
|
||||
|
||||
def test_model_with_no_default_init_sequence_generates(
|
||||
generate_main: Callable[[str | Path], str],
|
||||
component_config_path: Callable[[str], Path],
|
||||
|
||||
@@ -0,0 +1,376 @@
|
||||
"""Tests for the SSD1677 border_waveform option and EpaperModel.check_requirements()."""
|
||||
|
||||
from collections.abc import Callable, Generator
|
||||
from pathlib import Path
|
||||
import re
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
|
||||
from esphome import config_validation as cv
|
||||
from esphome.components.epaper_spi.display import CONFIG_SCHEMA, MODELS
|
||||
from esphome.components.epaper_spi.models import EpaperModel
|
||||
from esphome.components.epaper_spi.models.ssd1677 import CONF_BORDER_WAVEFORM
|
||||
from esphome.components.esp32 import (
|
||||
KEY_BOARD,
|
||||
KEY_VARIANT,
|
||||
VARIANT_ESP32,
|
||||
VARIANT_ESP32S3,
|
||||
)
|
||||
from esphome.const import PlatformFramework
|
||||
from esphome.core import CORE
|
||||
from esphome.types import ConfigType
|
||||
from tests.component_tests.types import SetCoreConfigCallable
|
||||
|
||||
|
||||
def _ssd1677_config(**overrides: Any) -> ConfigType:
|
||||
config: ConfigType = {
|
||||
"id": "test_display",
|
||||
"model": "ssd1677",
|
||||
"dc_pin": 21,
|
||||
"busy_pin": 22,
|
||||
"reset_pin": 23,
|
||||
"cs_pin": 5,
|
||||
"dimensions": {"width": 200, "height": 200},
|
||||
}
|
||||
config.update(overrides)
|
||||
return config
|
||||
|
||||
|
||||
def _setup_esp32(
|
||||
set_core_config: SetCoreConfigCallable,
|
||||
set_component_config: Callable[[str, Any], None],
|
||||
variant: str = VARIANT_ESP32,
|
||||
board: str = "esp32dev",
|
||||
) -> None:
|
||||
set_core_config(
|
||||
PlatformFramework.ESP32_IDF,
|
||||
platform_data={KEY_BOARD: board, KEY_VARIANT: variant},
|
||||
)
|
||||
set_component_config("spi", {"id": "spi_bus", "clk_pin": 18, "mosi_pin": 19})
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def temp_model() -> Generator[Callable[..., EpaperModel]]:
|
||||
"""Register a throwaway EpaperModel for a test and remove it from the shared registry after."""
|
||||
created: list[EpaperModel] = []
|
||||
|
||||
def _make(name: str, **defaults: Any) -> EpaperModel:
|
||||
model = EpaperModel(name, class_name="EPaperMono", **defaults)
|
||||
created.append(model)
|
||||
return model
|
||||
|
||||
yield _make
|
||||
for model in created:
|
||||
MODELS.pop(model.name, None)
|
||||
|
||||
|
||||
# --- border_waveform ---------------------------------------------------------
|
||||
|
||||
|
||||
def test_border_waveform_default_mono(
|
||||
set_core_config: SetCoreConfigCallable,
|
||||
set_component_config: Callable[[str, Any], None],
|
||||
) -> None:
|
||||
"""ssd1677 defaults border_waveform to 0x01."""
|
||||
_setup_esp32(set_core_config, set_component_config)
|
||||
|
||||
result = CONFIG_SCHEMA(_ssd1677_config())
|
||||
|
||||
assert result[CONF_BORDER_WAVEFORM] == 0x01
|
||||
|
||||
|
||||
def test_border_waveform_default_gray4(
|
||||
set_core_config: SetCoreConfigCallable,
|
||||
set_component_config: Callable[[str, Any], None],
|
||||
) -> None:
|
||||
"""The 4-level grayscale variant defaults border_waveform to 0x00, independently of mono."""
|
||||
_setup_esp32(
|
||||
set_core_config, set_component_config, VARIANT_ESP32S3, "esp32-s3-devkitc-1"
|
||||
)
|
||||
CORE.raw_config = {"psram": {}}
|
||||
|
||||
result = CONFIG_SCHEMA(
|
||||
{"id": "test_display", "model": "seeed-reterminal-sticky-gray4"}
|
||||
)
|
||||
|
||||
assert result[CONF_BORDER_WAVEFORM] == 0x00
|
||||
|
||||
|
||||
def test_border_waveform_override(
|
||||
set_core_config: SetCoreConfigCallable,
|
||||
set_component_config: Callable[[str, Any], None],
|
||||
) -> None:
|
||||
"""An explicit border_waveform overrides the model default."""
|
||||
_setup_esp32(set_core_config, set_component_config)
|
||||
|
||||
result = CONFIG_SCHEMA(_ssd1677_config(border_waveform=0x1A))
|
||||
|
||||
assert result[CONF_BORDER_WAVEFORM] == 0x1A
|
||||
|
||||
|
||||
def test_border_waveform_accepts_hex_string(
|
||||
set_core_config: SetCoreConfigCallable,
|
||||
set_component_config: Callable[[str, Any], None],
|
||||
) -> None:
|
||||
"""border_waveform accepts a hex string like the YAML author would write."""
|
||||
_setup_esp32(set_core_config, set_component_config)
|
||||
|
||||
result = CONFIG_SCHEMA(_ssd1677_config(border_waveform="0x1A"))
|
||||
|
||||
assert result[CONF_BORDER_WAVEFORM] == 0x1A
|
||||
|
||||
|
||||
def test_border_waveform_out_of_range(
|
||||
set_core_config: SetCoreConfigCallable,
|
||||
set_component_config: Callable[[str, Any], None],
|
||||
) -> None:
|
||||
"""border_waveform rejects values that don't fit in a byte."""
|
||||
_setup_esp32(set_core_config, set_component_config)
|
||||
|
||||
with pytest.raises(cv.Invalid):
|
||||
CONFIG_SCHEMA(_ssd1677_config(border_waveform=0x100))
|
||||
|
||||
|
||||
def test_border_waveform_in_generated_init_sequence(
|
||||
generate_main: Callable[[str | Path], str],
|
||||
component_config_path: Callable[[str], Path],
|
||||
) -> None:
|
||||
"""The configured border_waveform byte reaches the generated init sequence.
|
||||
|
||||
Command 0x3C (60) is followed by a length of 1 and the waveform byte.
|
||||
"""
|
||||
main_cpp = generate_main(component_config_path("ssd1677_border_waveform_test.yaml"))
|
||||
|
||||
assert re.search(r"60,\s*1,\s*0x1A,", main_cpp)
|
||||
|
||||
|
||||
# --- full_update_every / supports_partial_update ------------------------------
|
||||
|
||||
|
||||
def test_full_update_every_rejected_for_gray4(
|
||||
set_core_config: SetCoreConfigCallable,
|
||||
set_component_config: Callable[[str, Any], None],
|
||||
) -> None:
|
||||
"""The gray4 driver's partial updates are black and white and flatten the whole
|
||||
panel, so full_update_every > 1 is refused unless that is explicitly accepted."""
|
||||
_setup_esp32(
|
||||
set_core_config, set_component_config, VARIANT_ESP32S3, "esp32-s3-devkitc-1"
|
||||
)
|
||||
CORE.raw_config = {"psram": {}}
|
||||
|
||||
with pytest.raises(cv.Invalid, match="monochrome_partial_updates: true"):
|
||||
CONFIG_SCHEMA(
|
||||
{
|
||||
"id": "test_display",
|
||||
"model": "seeed-reterminal-sticky-gray4",
|
||||
"full_update_every": 5,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def test_full_update_every_accepted_for_gray4_with_monochrome_partials(
|
||||
set_core_config: SetCoreConfigCallable,
|
||||
set_component_config: Callable[[str, Any], None],
|
||||
) -> None:
|
||||
"""With the trade-off accepted, the gray4 driver takes full_update_every > 1."""
|
||||
_setup_esp32(
|
||||
set_core_config, set_component_config, VARIANT_ESP32S3, "esp32-s3-devkitc-1"
|
||||
)
|
||||
CORE.raw_config = {"psram": {}}
|
||||
|
||||
result = CONFIG_SCHEMA(
|
||||
{
|
||||
"id": "test_display",
|
||||
"model": "seeed-reterminal-sticky-gray4",
|
||||
"full_update_every": 5,
|
||||
"monochrome_partial_updates": True,
|
||||
}
|
||||
)
|
||||
|
||||
assert result["full_update_every"] == 5
|
||||
|
||||
|
||||
def test_monochrome_partial_updates_not_offered_for_mono_sticky(
|
||||
set_core_config: SetCoreConfigCallable,
|
||||
set_component_config: Callable[[str, Any], None],
|
||||
) -> None:
|
||||
"""The option only exists where partial updates lose something."""
|
||||
_setup_esp32(
|
||||
set_core_config, set_component_config, VARIANT_ESP32S3, "esp32-s3-devkitc-1"
|
||||
)
|
||||
CORE.raw_config = {"psram": {}}
|
||||
|
||||
with pytest.raises(cv.Invalid, match="monochrome_partial_updates"):
|
||||
CONFIG_SCHEMA(
|
||||
{
|
||||
"id": "test_display",
|
||||
"model": "seeed-reterminal-sticky",
|
||||
"monochrome_partial_updates": True,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def test_full_update_every_default_accepted_for_gray4(
|
||||
set_core_config: SetCoreConfigCallable,
|
||||
set_component_config: Callable[[str, Any], None],
|
||||
) -> None:
|
||||
"""Leaving full_update_every at its default of 1 is fine for the gray4 driver."""
|
||||
_setup_esp32(
|
||||
set_core_config, set_component_config, VARIANT_ESP32S3, "esp32-s3-devkitc-1"
|
||||
)
|
||||
CORE.raw_config = {"psram": {}}
|
||||
|
||||
result = CONFIG_SCHEMA(
|
||||
{"id": "test_display", "model": "seeed-reterminal-sticky-gray4"}
|
||||
)
|
||||
|
||||
assert result["full_update_every"] == 1
|
||||
|
||||
|
||||
def test_full_update_every_accepted_for_mono_sticky(
|
||||
set_core_config: SetCoreConfigCallable,
|
||||
set_component_config: Callable[[str, Any], None],
|
||||
) -> None:
|
||||
"""The mono sibling model supports partial update, unaffected by the gray4 restriction."""
|
||||
_setup_esp32(
|
||||
set_core_config, set_component_config, VARIANT_ESP32S3, "esp32-s3-devkitc-1"
|
||||
)
|
||||
CORE.raw_config = {"psram": {}}
|
||||
|
||||
result = CONFIG_SCHEMA(
|
||||
{
|
||||
"id": "test_display",
|
||||
"model": "seeed-reterminal-sticky",
|
||||
"full_update_every": 5,
|
||||
}
|
||||
)
|
||||
|
||||
assert result["full_update_every"] == 5
|
||||
|
||||
|
||||
# --- check_requirements -------------------------------------------------------
|
||||
|
||||
|
||||
def test_requirement_missing_raises(
|
||||
set_core_config: SetCoreConfigCallable,
|
||||
set_component_config: Callable[[str, Any], None],
|
||||
) -> None:
|
||||
"""seeed-reterminal-sticky requires psram; without it, config validation fails."""
|
||||
_setup_esp32(
|
||||
set_core_config, set_component_config, VARIANT_ESP32S3, "esp32-s3-devkitc-1"
|
||||
)
|
||||
CORE.raw_config = {}
|
||||
|
||||
with pytest.raises(cv.Invalid, match="requires component 'psram'"):
|
||||
CONFIG_SCHEMA({"id": "test_display", "model": "seeed-reterminal-sticky"})
|
||||
|
||||
|
||||
def test_requirement_satisfied_does_not_raise(
|
||||
set_core_config: SetCoreConfigCallable,
|
||||
set_component_config: Callable[[str, Any], None],
|
||||
) -> None:
|
||||
"""With psram present at the top level, seeed-reterminal-sticky validates."""
|
||||
_setup_esp32(
|
||||
set_core_config, set_component_config, VARIANT_ESP32S3, "esp32-s3-devkitc-1"
|
||||
)
|
||||
CORE.raw_config = {"psram": {}}
|
||||
|
||||
result = CONFIG_SCHEMA({"id": "test_display", "model": "seeed-reterminal-sticky"})
|
||||
|
||||
assert result["model"] == "SEEED-RETERMINAL-STICKY"
|
||||
|
||||
|
||||
def test_requirement_check_skipped_without_raw_config(
|
||||
set_core_config: SetCoreConfigCallable,
|
||||
set_component_config: Callable[[str, Any], None],
|
||||
) -> None:
|
||||
"""With no raw_config (e.g. a schema invoked directly, as in these tests), the
|
||||
requirement check is a no-op rather than a false failure."""
|
||||
_setup_esp32(
|
||||
set_core_config, set_component_config, VARIANT_ESP32S3, "esp32-s3-devkitc-1"
|
||||
)
|
||||
assert CORE.raw_config is None
|
||||
|
||||
# Should not raise even though "psram" is required and nothing was configured.
|
||||
CONFIG_SCHEMA({"id": "test_display", "model": "seeed-reterminal-sticky"})
|
||||
|
||||
|
||||
def test_requirement_missing_multiple_pluralised(
|
||||
temp_model: Callable[..., EpaperModel],
|
||||
) -> None:
|
||||
"""The error message pluralises "component(s)" and lists every missing one."""
|
||||
model = temp_model("test-multi-requirement", requires={"aaa", "bbb"})
|
||||
CORE.raw_config = {}
|
||||
|
||||
with pytest.raises(cv.Invalid, match="requires components 'aaa', 'bbb'"):
|
||||
model.check_requirements()
|
||||
|
||||
|
||||
# --- width_multiple -----------------------------------------------------------
|
||||
|
||||
|
||||
@pytest.mark.parametrize("width", [804, 801])
|
||||
def test_gray4_width_not_multiple_of_8_rejected(
|
||||
set_core_config: SetCoreConfigCallable,
|
||||
set_component_config: Callable[[str, Any], None],
|
||||
width: int,
|
||||
) -> None:
|
||||
"""The gray4 plane split reads two whole buffer bytes per plane byte, so width must be a multiple of 8."""
|
||||
_setup_esp32(
|
||||
set_core_config, set_component_config, VARIANT_ESP32S3, "esp32-s3-devkitc-1"
|
||||
)
|
||||
|
||||
with pytest.raises(cv.Invalid, match="multiple of 8"):
|
||||
CONFIG_SCHEMA(
|
||||
{
|
||||
"id": "test_display",
|
||||
"model": "seeed-reterminal-sticky-gray4",
|
||||
"dimensions": {"width": width, "height": 480},
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def test_gray4_width_multiple_of_8_accepted(
|
||||
set_core_config: SetCoreConfigCallable,
|
||||
set_component_config: Callable[[str, Any], None],
|
||||
) -> None:
|
||||
_setup_esp32(
|
||||
set_core_config, set_component_config, VARIANT_ESP32S3, "esp32-s3-devkitc-1"
|
||||
)
|
||||
|
||||
result = CONFIG_SCHEMA(
|
||||
{
|
||||
"id": "test_display",
|
||||
"model": "seeed-reterminal-sticky-gray4",
|
||||
"dimensions": {"width": 808, "height": 480},
|
||||
}
|
||||
)
|
||||
|
||||
assert result["dimensions"]["width"] == 808
|
||||
|
||||
|
||||
def test_mono_ssd1677_accepts_any_width(
|
||||
set_core_config: SetCoreConfigCallable,
|
||||
set_component_config: Callable[[str, Any], None],
|
||||
) -> None:
|
||||
"""The width restriction applies only to the gray4 model."""
|
||||
_setup_esp32(set_core_config, set_component_config)
|
||||
|
||||
result = CONFIG_SCHEMA(_ssd1677_config(dimensions={"width": 204, "height": 200}))
|
||||
|
||||
assert result["dimensions"]["width"] == 204
|
||||
|
||||
|
||||
# --- extend(class_name=...) --------------------------------------------------
|
||||
|
||||
|
||||
def test_gray4_code_generation(
|
||||
generate_main: Callable[[str | Path], str],
|
||||
component_config_path: Callable[[str], Path],
|
||||
) -> None:
|
||||
"""seeed-reterminal-sticky-gray4 generates the EPaperSSD1677Gray4 driver, not EPaperMono."""
|
||||
main_cpp = generate_main(component_config_path("ssd1677_gray4_test.yaml"))
|
||||
|
||||
assert "epaper_spi::EPaperSSD1677Gray4" in main_cpp
|
||||
assert "epaper_spi::EPaperMono" not in main_cpp
|
||||
@@ -0,0 +1,13 @@
|
||||
esphome:
|
||||
name: test
|
||||
|
||||
esp32:
|
||||
variant: esp32s3
|
||||
framework:
|
||||
type: esp-idf
|
||||
advanced:
|
||||
nvs_cache_in_psram: false
|
||||
|
||||
psram:
|
||||
mode: octal
|
||||
ignore_not_found: false
|
||||
@@ -0,0 +1,14 @@
|
||||
esphome:
|
||||
name: test
|
||||
|
||||
esp32:
|
||||
variant: esp32s3
|
||||
framework:
|
||||
type: esp-idf
|
||||
advanced:
|
||||
nvs_encryption:
|
||||
key_id: 0
|
||||
|
||||
psram:
|
||||
mode: octal
|
||||
ignore_not_found: false
|
||||
@@ -0,0 +1,13 @@
|
||||
esphome:
|
||||
name: test
|
||||
|
||||
esp32:
|
||||
variant: esp32s3
|
||||
framework:
|
||||
type: esp-idf
|
||||
sdkconfig_options:
|
||||
CONFIG_NVS_ENCRYPTION: "y"
|
||||
|
||||
psram:
|
||||
mode: octal
|
||||
ignore_not_found: false
|
||||
@@ -0,0 +1,13 @@
|
||||
esphome:
|
||||
name: test
|
||||
|
||||
esp32:
|
||||
variant: esp32s3
|
||||
framework:
|
||||
type: esp-idf
|
||||
advanced:
|
||||
nvs_cache_in_psram: true
|
||||
|
||||
psram:
|
||||
mode: octal
|
||||
ignore_not_found: false
|
||||
@@ -0,0 +1,11 @@
|
||||
esphome:
|
||||
name: test
|
||||
|
||||
esp32:
|
||||
variant: esp32s3
|
||||
framework:
|
||||
type: esp-idf
|
||||
|
||||
psram:
|
||||
mode: octal
|
||||
ignore_not_found: false
|
||||
@@ -0,0 +1,10 @@
|
||||
esphome:
|
||||
name: test
|
||||
|
||||
esp32:
|
||||
variant: esp32s3
|
||||
framework:
|
||||
type: esp-idf
|
||||
|
||||
psram:
|
||||
mode: octal
|
||||
@@ -0,0 +1,13 @@
|
||||
esphome:
|
||||
name: test
|
||||
|
||||
esp32:
|
||||
variant: esp32s3
|
||||
framework:
|
||||
type: esp-idf
|
||||
sdkconfig_options:
|
||||
CONFIG_NVS_ALLOCATE_CACHE_IN_SPIRAM: "n"
|
||||
|
||||
psram:
|
||||
mode: octal
|
||||
ignore_not_found: false
|
||||
@@ -0,0 +1,11 @@
|
||||
esphome:
|
||||
name: test
|
||||
|
||||
esp32:
|
||||
board: esp32-c6-devkitc-1
|
||||
framework:
|
||||
type: esp-idf
|
||||
|
||||
zigbee:
|
||||
model: zigbee_test
|
||||
power_source: MAINS_SINGLE_PHASE
|
||||
@@ -4,6 +4,7 @@ Test ESP32 configuration
|
||||
|
||||
import asyncio
|
||||
from collections.abc import Callable
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
@@ -1457,7 +1458,7 @@ def test_esp32_s31_gpio_validation(
|
||||
pin = {CONF_NUMBER: 36, CONF_MODE: input_mode}
|
||||
with caplog.at_level("WARNING"):
|
||||
validate_supports(pin)
|
||||
assert "GPIO36 is a strapping PIN" in caplog.text
|
||||
assert "GPIO36 is a strapping pin" in caplog.text
|
||||
|
||||
|
||||
_TLS_SERVER_OPTIONS = (
|
||||
@@ -1488,7 +1489,7 @@ def test_mbedtls_tls_trim_sdkconfig(
|
||||
assert {sdkconfig.get(name) for name in MBEDTLS_TLS_EXTRA_OPTIONS} == {extras}
|
||||
|
||||
|
||||
_OPENTHREAD_EXTRAS = {"CONFIG_MBEDTLS_CCM_C", "CONFIG_MBEDTLS_ECDSA_DETERMINISTIC"}
|
||||
_CCM_ECDSA_EXTRAS = {"CONFIG_MBEDTLS_CCM_C", "CONFIG_MBEDTLS_ECDSA_DETERMINISTIC"}
|
||||
|
||||
|
||||
def test_mbedtls_tls_openthread_keeps_only_what_it_uses(
|
||||
@@ -1500,7 +1501,19 @@ def test_mbedtls_tls_openthread_keeps_only_what_it_uses(
|
||||
sdkconfig = CORE.data[KEY_ESP32][KEY_SDKCONFIG_OPTIONS]
|
||||
assert tuple(sdkconfig.get(name) for name in _TLS_SERVER_OPTIONS) == (None, None)
|
||||
for name in MBEDTLS_TLS_EXTRA_OPTIONS:
|
||||
assert sdkconfig.get(name) is (None if name in _OPENTHREAD_EXTRAS else False)
|
||||
assert sdkconfig.get(name) is (None if name in _CCM_ECDSA_EXTRAS else False)
|
||||
|
||||
|
||||
def test_mbedtls_tls_zigbee_keeps_only_what_it_uses(
|
||||
generate_main: Callable[[str | Path], str],
|
||||
component_config_path: Callable[[str], Path],
|
||||
) -> None:
|
||||
"""The Zigbee config keeps CCM and deterministic ECDSA; the rest is trimmed."""
|
||||
generate_main(component_config_path("tls_zigbee_c6.yaml"))
|
||||
sdkconfig = CORE.data[KEY_ESP32][KEY_SDKCONFIG_OPTIONS]
|
||||
assert tuple(sdkconfig.get(name) for name in _TLS_SERVER_OPTIONS) == (True, False)
|
||||
for name in MBEDTLS_TLS_EXTRA_OPTIONS:
|
||||
assert sdkconfig.get(name) is (None if name in _CCM_ECDSA_EXTRAS else False)
|
||||
|
||||
|
||||
def test_mbedtls_tls_user_sdkconfig_wins(
|
||||
@@ -1529,7 +1542,16 @@ def test_mbedtls_tls_openthread_requires_server_and_extras(
|
||||
"""The OpenThread hooks mark the DTLS server and CCM/deterministic ECDSA as required."""
|
||||
generate_main(component_config_path("mbedtls_tls_openthread.yaml"))
|
||||
assert CORE.data[KEY_ESP32][KEY_MBEDTLS_TLS_SERVER_REQUIRED] is True
|
||||
assert CORE.data[KEY_ESP32][KEY_MBEDTLS_TLS_EXTRAS_REQUIRED] == _OPENTHREAD_EXTRAS
|
||||
assert CORE.data[KEY_ESP32][KEY_MBEDTLS_TLS_EXTRAS_REQUIRED] == _CCM_ECDSA_EXTRAS
|
||||
|
||||
|
||||
def test_mbedtls_tls_zigbee_requires_extras(
|
||||
generate_main: Callable[[str | Path], str],
|
||||
component_config_path: Callable[[str], Path],
|
||||
) -> None:
|
||||
"""The Zigbee hooks mark the CCM/deterministic ECDSA as required."""
|
||||
generate_main(component_config_path("tls_zigbee_c6.yaml"))
|
||||
assert CORE.data[KEY_ESP32][KEY_MBEDTLS_TLS_EXTRAS_REQUIRED] == _CCM_ECDSA_EXTRAS
|
||||
|
||||
|
||||
_VASPRINTF_STUB_FLAGS = {"-Wl,--wrap=vasprintf", "-Wl,--undefined=__wrap_vasprintf"}
|
||||
@@ -1554,3 +1576,137 @@ def test_vasprintf_stub_only_on_rom_vsnprintf_variants(
|
||||
assert (CORE.build_flags >= _VASPRINTF_STUB_FLAGS) is expected
|
||||
defines = {define.name for define in CORE.defines}
|
||||
assert ("USE_ESP32_VASPRINTF_STUB" in defines) is expected
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("fixture", "expected"),
|
||||
[
|
||||
("nvs_cache_psram_guaranteed.yaml", True),
|
||||
("nvs_cache_psram_explicit.yaml", True),
|
||||
("nvs_cache_psram_not_guaranteed.yaml", None),
|
||||
("nvs_cache_psram_disabled.yaml", None),
|
||||
# the encryption keys must stay in internal RAM, whichever way encryption is enabled
|
||||
("nvs_cache_psram_encrypted.yaml", None),
|
||||
("nvs_cache_psram_encrypted_sdkconfig.yaml", None),
|
||||
],
|
||||
)
|
||||
def test_nvs_cache_in_psram_sdkconfig(
|
||||
generate_main: Callable[[str | Path], str],
|
||||
component_config_path: Callable[[str], Path],
|
||||
fixture: str,
|
||||
expected: bool | None,
|
||||
) -> None:
|
||||
"""The NVS cache moves to PSRAM only with guaranteed PSRAM, the option not off and no NVS encryption."""
|
||||
generate_main(component_config_path(fixture))
|
||||
sdkconfig = CORE.data[KEY_ESP32][KEY_SDKCONFIG_OPTIONS]
|
||||
assert sdkconfig.get("CONFIG_NVS_ALLOCATE_CACHE_IN_SPIRAM") is expected
|
||||
|
||||
|
||||
def test_nvs_cache_in_psram_user_sdkconfig_wins(
|
||||
generate_main: Callable[[str | Path], str],
|
||||
component_config_path: Callable[[str], Path],
|
||||
) -> None:
|
||||
"""A raw sdkconfig_options value for the NVS cache option is left alone."""
|
||||
generate_main(component_config_path("nvs_cache_psram_user_off.yaml"))
|
||||
sdkconfig = CORE.data[KEY_ESP32][KEY_SDKCONFIG_OPTIONS]
|
||||
assert sdkconfig["CONFIG_NVS_ALLOCATE_CACHE_IN_SPIRAM"] == RawSdkconfigValue("n")
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("full_config", "error_match"),
|
||||
[
|
||||
pytest.param(
|
||||
{CONF_ESPHOME: {}, "psram": {"disabled": False, "ignore_not_found": True}},
|
||||
r"'nvs_cache_in_psram' requires PSRAM with 'ignore_not_found: false'",
|
||||
id="nvs_cache_in_psram_needs_guaranteed_psram",
|
||||
),
|
||||
pytest.param(
|
||||
{CONF_ESPHOME: {}},
|
||||
r"'nvs_cache_in_psram' requires PSRAM with 'ignore_not_found: false'",
|
||||
id="nvs_cache_in_psram_needs_psram",
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_nvs_cache_in_psram_explicit_true_errors(
|
||||
full_config: dict, error_match: str, set_core_config: SetCoreConfigCallable
|
||||
) -> None:
|
||||
"""An explicit nvs_cache_in_psram: true that cannot apply is a config error, not a silent no-op."""
|
||||
set_core_config(PlatformFramework.ESP32_IDF, full_config=full_config)
|
||||
from esphome.components.esp32 import CONFIG_SCHEMA, FINAL_VALIDATE_SCHEMA
|
||||
|
||||
config = {
|
||||
"variant": "esp32s3",
|
||||
"framework": {"type": "esp-idf", "advanced": {"nvs_cache_in_psram": True}},
|
||||
}
|
||||
with pytest.raises(cv.Invalid, match=error_match):
|
||||
FINAL_VALIDATE_SCHEMA(CONFIG_SCHEMA(config))
|
||||
|
||||
|
||||
def test_nvs_cache_in_psram_explicit_true_rejects_encryption(
|
||||
set_core_config: SetCoreConfigCallable,
|
||||
) -> None:
|
||||
set_core_config(
|
||||
PlatformFramework.ESP32_IDF,
|
||||
full_config={
|
||||
CONF_ESPHOME: {},
|
||||
"psram": {"disabled": False, "ignore_not_found": False},
|
||||
},
|
||||
)
|
||||
from esphome.components.esp32 import CONFIG_SCHEMA, FINAL_VALIDATE_SCHEMA
|
||||
|
||||
config = {
|
||||
"variant": "esp32s3",
|
||||
"framework": {
|
||||
"type": "esp-idf",
|
||||
"advanced": {"nvs_cache_in_psram": True, "nvs_encryption": {"key_id": 0}},
|
||||
},
|
||||
}
|
||||
with pytest.raises(cv.Invalid, match="cannot be used with NVS encryption"):
|
||||
FINAL_VALIDATE_SCHEMA(CONFIG_SCHEMA(config))
|
||||
|
||||
|
||||
def test_nvs_cache_in_psram_default_with_encryption_is_quiet(
|
||||
generate_main: Callable[[str | Path], str],
|
||||
component_config_path: Callable[[str], Path],
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
"""Encryption on a board that never mentioned the option must not warn about it."""
|
||||
with caplog.at_level(logging.WARNING):
|
||||
generate_main(component_config_path("nvs_cache_psram_encrypted.yaml"))
|
||||
assert "nvs_cache_in_psram" not in caplog.text
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_nvs_cache_in_psram_explicit_request_warns_when_encrypted(
|
||||
set_core_config: SetCoreConfigCallable, caplog: pytest.LogCaptureFixture
|
||||
) -> None:
|
||||
"""An explicit request dropped for NVS encryption enabled elsewhere logs a warning."""
|
||||
set_core_config(
|
||||
PlatformFramework.ESP32_IDF, platform_data={KEY_SDKCONFIG_OPTIONS: {}}
|
||||
)
|
||||
from esphome.components.esp32 import (
|
||||
_apply_nvs_cache_in_psram,
|
||||
add_idf_sdkconfig_option,
|
||||
)
|
||||
|
||||
add_idf_sdkconfig_option("CONFIG_NVS_ENCRYPTION", True)
|
||||
with caplog.at_level(logging.WARNING):
|
||||
await _apply_nvs_cache_in_psram(True)
|
||||
assert "nvs_cache_in_psram ignored" in caplog.text
|
||||
assert (
|
||||
"CONFIG_NVS_ALLOCATE_CACHE_IN_SPIRAM"
|
||||
not in CORE.data[KEY_ESP32][KEY_SDKCONFIG_OPTIONS]
|
||||
)
|
||||
|
||||
|
||||
def test_nvs_cache_in_psram_explicit_true_on_valid_board_is_quiet(
|
||||
generate_main: Callable[[str | Path], str],
|
||||
component_config_path: Callable[[str], Path],
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
"""An explicit true that applies sets the option and warns about nothing."""
|
||||
with caplog.at_level(logging.WARNING):
|
||||
generate_main(component_config_path("nvs_cache_psram_explicit.yaml"))
|
||||
sdkconfig = CORE.data[KEY_ESP32][KEY_SDKCONFIG_OPTIONS]
|
||||
assert sdkconfig.get("CONFIG_NVS_ALLOCATE_CACHE_IN_SPIRAM") is True
|
||||
assert "nvs_cache_in_psram" not in caplog.text
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
"""The Bluedroid queued connection guard is emitted only for client builds on unfixed ESP-IDF."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from esphome import config_validation as cv
|
||||
from esphome.components import esp32_ble_tracker
|
||||
from esphome.core import CORE
|
||||
|
||||
# Spelled out so a typo in the component's flags fails here
|
||||
_GUARD_FLAGS = {
|
||||
"-Wl,--wrap=l2cble_init_direct_conn",
|
||||
"-Wl,--undefined=__wrap_l2cble_init_direct_conn",
|
||||
}
|
||||
|
||||
|
||||
def _pin_idf(monkeypatch: pytest.MonkeyPatch, idf: str) -> None:
|
||||
monkeypatch.setattr(esp32_ble_tracker, "idf_version", lambda: cv.Version.parse(idf))
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("config_file", "idf", "expected"),
|
||||
[
|
||||
pytest.param("scan_window_raised.yaml", "5.5.5", True, id="client"),
|
||||
pytest.param("scan_window_raised.yaml", "6.0.3", False, id="client_fixed_idf"),
|
||||
pytest.param("scan_window_scan_only.yaml", "5.5.5", False, id="scan_only"),
|
||||
],
|
||||
)
|
||||
def test_guard_only_in_client_builds_on_unfixed_idf(
|
||||
generate_main: Callable[[str | Path], str],
|
||||
component_config_path: Callable[[str], Path],
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
config_file: str,
|
||||
idf: str,
|
||||
expected: bool,
|
||||
) -> None:
|
||||
_pin_idf(monkeypatch, idf)
|
||||
generate_main(component_config_path(config_file))
|
||||
assert (CORE.build_flags >= _GUARD_FLAGS) is expected
|
||||
assert CORE.build_flags.isdisjoint(_GUARD_FLAGS) is not expected
|
||||
defines = {define.name for define in CORE.defines}
|
||||
assert ("USE_ESP32_BLE_TRACKER_DIRECT_CONN_GUARD" in defines) is expected
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("idf", "expected"),
|
||||
[
|
||||
("5.1.6", True), # series that never got the fix
|
||||
("5.2.7", True),
|
||||
("5.2.8", False),
|
||||
("5.3.5", True),
|
||||
("5.3.6", False),
|
||||
("5.4.4", True),
|
||||
("5.4.5", True), # no fixed 5.4, 5.5 or 6.1 tag yet
|
||||
("5.5.5", True),
|
||||
("5.5.6", True),
|
||||
("6.0.2", True),
|
||||
("6.0.3", False),
|
||||
("6.1.0", True),
|
||||
("6.1.1", True),
|
||||
("6.2.0", False),
|
||||
("7.0.0", False),
|
||||
],
|
||||
)
|
||||
def test_needs_direct_conn_guard(
|
||||
monkeypatch: pytest.MonkeyPatch, idf: str, expected: bool
|
||||
) -> None:
|
||||
_pin_idf(monkeypatch, idf)
|
||||
assert esp32_ble_tracker._needs_direct_conn_guard() is expected
|
||||
@@ -0,0 +1,16 @@
|
||||
esphome:
|
||||
name: test
|
||||
|
||||
esp32:
|
||||
board: esp32dev
|
||||
|
||||
ethernet:
|
||||
id: eth_component
|
||||
type: KSZ8851SNL
|
||||
clk_pin: GPIO18
|
||||
mosi_pin: GPIO23
|
||||
miso_pin: GPIO19
|
||||
cs_pin: GPIO5
|
||||
interrupt_pin: GPIO36
|
||||
reset_pin: GPIO22
|
||||
clock_speed: 20MHz
|
||||
@@ -0,0 +1,18 @@
|
||||
esphome:
|
||||
name: test
|
||||
|
||||
esp32:
|
||||
board: esp32dev
|
||||
framework:
|
||||
version: 6.0.0
|
||||
|
||||
ethernet:
|
||||
id: eth_component
|
||||
type: KSZ8851SNL
|
||||
clk_pin: GPIO18
|
||||
mosi_pin: GPIO23
|
||||
miso_pin: GPIO19
|
||||
cs_pin: GPIO5
|
||||
interrupt_pin: GPIO36
|
||||
reset_pin: GPIO22
|
||||
clock_speed: 20MHz
|
||||
@@ -0,0 +1,105 @@
|
||||
"""Tests for the KSZ8851SNL SPI ethernet type."""
|
||||
|
||||
from collections.abc import Callable
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
from voluptuous import Invalid
|
||||
|
||||
from esphome import config_validation as cv
|
||||
from esphome.components.esp32 import (
|
||||
KEY_BOARD,
|
||||
KEY_IDF_VERSION,
|
||||
KEY_VARIANT,
|
||||
VARIANT_ESP32S3,
|
||||
)
|
||||
from esphome.components.esp32.const import (
|
||||
KEY_COMPONENTS,
|
||||
KEY_ESP32,
|
||||
KEY_REF,
|
||||
KEY_SDKCONFIG_OPTIONS,
|
||||
)
|
||||
from esphome.components.ethernet import CONF_CLOCK_SPEED, CONFIG_SCHEMA
|
||||
from esphome.const import PlatformFramework
|
||||
from esphome.core import CORE
|
||||
|
||||
from ..types import SetCoreConfigCallable
|
||||
|
||||
_BASE_CONFIG = {
|
||||
"type": "KSZ8851SNL",
|
||||
"clk_pin": 47,
|
||||
"mosi_pin": 48,
|
||||
"miso_pin": 14,
|
||||
"cs_pin": 21,
|
||||
}
|
||||
|
||||
_SDKCONFIG_OPTION = "CONFIG_ETH_SPI_ETHERNET_KSZ8851SNL"
|
||||
_IDF_COMPONENT = "espressif/ksz8851snl"
|
||||
|
||||
|
||||
def _sdkconfig() -> dict[str, object]:
|
||||
return CORE.data[KEY_ESP32][KEY_SDKCONFIG_OPTIONS]
|
||||
|
||||
|
||||
def _idf_components() -> dict[str, dict[str, str | None]]:
|
||||
return CORE.data[KEY_ESP32][KEY_COMPONENTS]
|
||||
|
||||
|
||||
def test_codegen_on_idf5_wires_the_spi_driver(
|
||||
generate_main: Callable[[str | Path], str],
|
||||
component_config_path: Callable[[str], Path],
|
||||
) -> None:
|
||||
"""On IDF 5.x the driver is built into esp_eth and enabled by its Kconfig option."""
|
||||
main_cpp = generate_main(component_config_path("ksz8851snl_idf5.yaml"))
|
||||
|
||||
assert "eth_component->set_type(ethernet::ETHERNET_TYPE_KSZ8851SNL);" in main_cpp
|
||||
assert "eth_component->set_cs_pin(5);" in main_cpp
|
||||
assert "eth_component->set_interrupt_pin(36);" in main_cpp
|
||||
assert "USE_ETHERNET_KSZ8851SNL" in {d.name for d in CORE.defines}
|
||||
assert _sdkconfig()[_SDKCONFIG_OPTION] is True
|
||||
# Built into IDF 5.x, so the registry component must not be pulled in.
|
||||
assert _IDF_COMPONENT not in _idf_components()
|
||||
|
||||
|
||||
def test_codegen_on_idf6_pulls_the_registry_component(
|
||||
generate_main: Callable[[str | Path], str],
|
||||
component_config_path: Callable[[str], Path],
|
||||
) -> None:
|
||||
"""IDF 6.0 dropped the built-in driver, so the managed component is required."""
|
||||
generate_main(component_config_path("ksz8851snl_idf6.yaml"))
|
||||
|
||||
assert _idf_components()[_IDF_COMPONENT][KEY_REF] == "1.2.0"
|
||||
# The Kconfig option no longer exists on IDF 6.0.
|
||||
assert _SDKCONFIG_OPTION not in _sdkconfig()
|
||||
|
||||
|
||||
def _set_esp32_s3(set_core_config: SetCoreConfigCallable) -> None:
|
||||
set_core_config(
|
||||
PlatformFramework.ESP32_IDF,
|
||||
platform_data={
|
||||
KEY_BOARD: "esp32-s3-devkitc-1",
|
||||
KEY_VARIANT: VARIANT_ESP32S3,
|
||||
KEY_IDF_VERSION: cv.Version(5, 3, 2),
|
||||
},
|
||||
)
|
||||
# _validate derives use_address from the node name, which has no default here.
|
||||
CORE.name = "ksz8851snl-test"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("clock_speed", ["26.67MHz", "40MHz"])
|
||||
def test_accepts_clock_speed_up_to_the_datasheet_maximum(
|
||||
set_core_config: SetCoreConfigCallable, clock_speed: str
|
||||
) -> None:
|
||||
"""The datasheet rates fSCLK to 40MHz, so the whole range must be accepted."""
|
||||
_set_esp32_s3(set_core_config)
|
||||
config = CONFIG_SCHEMA({**_BASE_CONFIG, CONF_CLOCK_SPEED: clock_speed})
|
||||
assert config[CONF_CLOCK_SPEED] == cv.frequency(clock_speed)
|
||||
|
||||
|
||||
def test_rejects_clock_speed_above_the_datasheet_maximum(
|
||||
set_core_config: SetCoreConfigCallable,
|
||||
) -> None:
|
||||
"""The shared 80MHz ceiling is out of spec for this part."""
|
||||
_set_esp32_s3(set_core_config)
|
||||
with pytest.raises(Invalid, match="value must be at most 40000000"):
|
||||
CONFIG_SCHEMA({**_BASE_CONFIG, CONF_CLOCK_SPEED: "80MHz"})
|
||||
@@ -0,0 +1,22 @@
|
||||
esphome:
|
||||
name: test-color-wipe-color
|
||||
|
||||
esp32:
|
||||
board: esp32dev
|
||||
framework:
|
||||
type: arduino
|
||||
|
||||
light:
|
||||
- platform: fastled_clockless
|
||||
id: wipe_light
|
||||
chipset: WS2811
|
||||
pin: GPIO13
|
||||
num_leds: 10
|
||||
effects:
|
||||
- addressable_color_wipe:
|
||||
name: Wipe
|
||||
colors:
|
||||
- color: darkred
|
||||
num_leds: 1
|
||||
- red: 50%
|
||||
num_leds: 1
|
||||
@@ -0,0 +1,40 @@
|
||||
"""Regression test: addressable_color_wipe's `color:` must scale by color_brightness.
|
||||
|
||||
The per-item schema tests in test_effect_color.py only check the validated config
|
||||
dict; they would still pass if addressable_color_wipe_effect_to_code dropped
|
||||
color_brightness on the floor (as it originally did), since a dark color's red/green/
|
||||
blue are already peak-normalized to 1.0 by the time codegen sees them. Generate the
|
||||
actual C++ and check the AddressableColorWipeEffectColor byte values instead, so a
|
||||
regression in the scaling multiplication itself is caught.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def main_cpp(
|
||||
generate_main: Callable[[str | Path], str],
|
||||
component_config_path: Callable[[str], Path],
|
||||
) -> str:
|
||||
return generate_main(
|
||||
component_config_path("addressable_color_wipe_color_test.yaml")
|
||||
)
|
||||
|
||||
|
||||
def test_dark_color_name_is_scaled_by_color_brightness(main_cpp: str) -> None:
|
||||
# color: darkred (0x8B0000) normalizes to r=1.0 but color_brightness=0x8B/0xFF,
|
||||
# so the byte value must come back down to 0x8B (139), not 255.
|
||||
assert ".r = 139," in main_cpp
|
||||
assert ".g = 0," in main_cpp
|
||||
assert ".b = 0," in main_cpp
|
||||
|
||||
|
||||
def test_explicit_channel_is_unaffected(main_cpp: str) -> None:
|
||||
# red: 50% with no color: or color_brightness: set must still scale by the
|
||||
# default color_brightness of 1.0, i.e. come out as plain 50% of 255.
|
||||
assert ".r = 128," in main_cpp
|
||||
@@ -0,0 +1,119 @@
|
||||
"""Tests for the light `color` option."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
|
||||
import pytest
|
||||
|
||||
from esphome import config_validation as cv
|
||||
from esphome.components.light.automation import (
|
||||
LIGHT_CONTROL_ACTION_SCHEMA,
|
||||
LIGHT_STATE_SCHEMA,
|
||||
)
|
||||
from esphome.const import (
|
||||
CONF_BLUE,
|
||||
CONF_COLOR_BRIGHTNESS,
|
||||
CONF_GREEN,
|
||||
CONF_ID,
|
||||
CONF_RED,
|
||||
)
|
||||
|
||||
LOGGER_NAME: str = "esphome.components.light.automation"
|
||||
|
||||
|
||||
def test_color_name_sets_rgb() -> None:
|
||||
result = LIGHT_STATE_SCHEMA({"color": "Tomato"})
|
||||
assert "color" not in result
|
||||
assert result[CONF_RED] == 1.0
|
||||
assert result[CONF_GREEN] == pytest.approx(0x63 / 0xFF)
|
||||
assert result[CONF_BLUE] == pytest.approx(0x47 / 0xFF)
|
||||
assert result[CONF_COLOR_BRIGHTNESS] == 1.0
|
||||
|
||||
|
||||
def test_color_name_in_control_action() -> None:
|
||||
result = LIGHT_CONTROL_ACTION_SCHEMA({CONF_ID: "test_light", "color": "blue"})
|
||||
assert (result[CONF_RED], result[CONF_GREEN], result[CONF_BLUE]) == (0, 0, 1.0)
|
||||
|
||||
|
||||
def test_unknown_color_name() -> None:
|
||||
with pytest.raises(cv.Invalid, match="notacolor"):
|
||||
LIGHT_STATE_SCHEMA({"color": "notacolor"})
|
||||
|
||||
|
||||
def test_color_name_conflicts_with_rgb() -> None:
|
||||
with pytest.raises(cv.Invalid, match="cannot be used with"):
|
||||
LIGHT_STATE_SCHEMA({"color": "red", CONF_GREEN: 0.5})
|
||||
|
||||
|
||||
def test_dark_color_sets_color_brightness() -> None:
|
||||
result = LIGHT_STATE_SCHEMA({"color": "darkred"})
|
||||
assert result[CONF_RED] == 1.0
|
||||
assert result[CONF_GREEN] == 0.0
|
||||
assert result[CONF_BLUE] == 0.0
|
||||
assert result[CONF_COLOR_BRIGHTNESS] == pytest.approx(0x8B / 0xFF)
|
||||
|
||||
|
||||
def test_black_sets_zero_color_brightness() -> None:
|
||||
result = LIGHT_STATE_SCHEMA({"color": "black"})
|
||||
assert result[CONF_COLOR_BRIGHTNESS] == 0.0
|
||||
assert (result[CONF_RED], result[CONF_GREEN], result[CONF_BLUE]) == (0, 0, 0)
|
||||
|
||||
|
||||
def test_explicit_color_brightness_wins(caplog: pytest.LogCaptureFixture) -> None:
|
||||
with caplog.at_level(logging.WARNING, logger=LOGGER_NAME):
|
||||
result = LIGHT_STATE_SCHEMA({"color": "darkred", CONF_COLOR_BRIGHTNESS: 0.25})
|
||||
assert result[CONF_COLOR_BRIGHTNESS] == 0.25
|
||||
assert "overrides the brightness of color 'darkred'" in caplog.text
|
||||
|
||||
|
||||
def test_explicit_color_brightness_full_color(
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
with caplog.at_level(logging.WARNING, logger=LOGGER_NAME):
|
||||
result = LIGHT_STATE_SCHEMA({"color": "red", CONF_COLOR_BRIGHTNESS: 0.25})
|
||||
assert result[CONF_COLOR_BRIGHTNESS] == 0.25
|
||||
assert not [r for r in caplog.records if r.name == LOGGER_NAME]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("value", [0x223344, "0x223344", "0X223344"])
|
||||
def test_hex_color(value: int | str) -> None:
|
||||
result = LIGHT_STATE_SCHEMA({"color": value})
|
||||
assert "color" not in result
|
||||
assert result[CONF_COLOR_BRIGHTNESS] == pytest.approx(0x44 / 0xFF)
|
||||
assert result[CONF_BLUE] == 1.0
|
||||
assert result[CONF_RED] == pytest.approx(0x22 / 0x44)
|
||||
assert result[CONF_GREEN] == pytest.approx(0x33 / 0x44)
|
||||
|
||||
|
||||
def test_hex_color_full_level() -> None:
|
||||
result = LIGHT_STATE_SCHEMA({"color": 0xFF8000})
|
||||
assert result[CONF_COLOR_BRIGHTNESS] == 1.0
|
||||
assert result[CONF_RED] == 1.0
|
||||
assert result[CONF_GREEN] == pytest.approx(0x80 / 0xFF)
|
||||
assert result[CONF_BLUE] == 0.0
|
||||
|
||||
|
||||
def test_hex_color_black() -> None:
|
||||
result = LIGHT_STATE_SCHEMA({"color": 0})
|
||||
assert result[CONF_COLOR_BRIGHTNESS] == 0.0
|
||||
assert (result[CONF_RED], result[CONF_GREEN], result[CONF_BLUE]) == (0, 0, 0)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("value", [0x1000000, -1, "0xGG", "0x1000000"])
|
||||
def test_hex_color_invalid(value: int | str) -> None:
|
||||
with pytest.raises(cv.Invalid):
|
||||
LIGHT_STATE_SCHEMA({"color": value})
|
||||
|
||||
|
||||
def test_hex_color_conflicts_with_rgb() -> None:
|
||||
with pytest.raises(cv.Invalid, match="cannot be used with"):
|
||||
LIGHT_STATE_SCHEMA({"color": 0x223344, CONF_RED: 0.5})
|
||||
|
||||
|
||||
def test_explicit_color_brightness_warning_names_hex_color(
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
with caplog.at_level(logging.WARNING, logger=LOGGER_NAME):
|
||||
LIGHT_STATE_SCHEMA({"color": 0x223344, CONF_COLOR_BRIGHTNESS: 0.25})
|
||||
assert "overrides the brightness of color '0x223344'" in caplog.text
|
||||
@@ -0,0 +1,69 @@
|
||||
"""Tests for the `color` option in per-item effect color lists."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
|
||||
from esphome import config_validation as cv
|
||||
from esphome.components.light.effects import (
|
||||
ADDRESSABLE_COLOR_WIPE_COLOR_SCHEMA,
|
||||
STROBE_COLOR_SCHEMA,
|
||||
)
|
||||
from esphome.const import CONF_BLUE, CONF_COLOR_BRIGHTNESS, CONF_GREEN, CONF_RED
|
||||
|
||||
|
||||
def test_strobe_plain_defaults_full_level() -> None:
|
||||
result = STROBE_COLOR_SCHEMA({"duration": "1s"})
|
||||
assert (result[CONF_RED], result[CONF_GREEN], result[CONF_BLUE]) == (1.0, 1.0, 1.0)
|
||||
assert result[CONF_COLOR_BRIGHTNESS] == 1.0
|
||||
|
||||
|
||||
def test_strobe_color_name() -> None:
|
||||
result = STROBE_COLOR_SCHEMA({"duration": "1s", "color": "darkred"})
|
||||
assert "color" not in result
|
||||
assert (result[CONF_RED], result[CONF_GREEN], result[CONF_BLUE]) == (1.0, 0.0, 0.0)
|
||||
assert result[CONF_COLOR_BRIGHTNESS] == pytest.approx(0x8B / 0xFF)
|
||||
|
||||
|
||||
def test_strobe_hex_color() -> None:
|
||||
result = STROBE_COLOR_SCHEMA({"duration": "1s", "color": "0x223344"})
|
||||
assert result[CONF_BLUE] == 1.0
|
||||
assert result[CONF_COLOR_BRIGHTNESS] == pytest.approx(0x44 / 0xFF)
|
||||
|
||||
|
||||
def test_strobe_explicit_channels_unaffected() -> None:
|
||||
result = STROBE_COLOR_SCHEMA({"duration": "1s", "red": "50%"})
|
||||
assert result[CONF_RED] == 0.5
|
||||
assert (result[CONF_GREEN], result[CONF_BLUE]) == (1.0, 1.0)
|
||||
assert result[CONF_COLOR_BRIGHTNESS] == 1.0
|
||||
|
||||
|
||||
def test_strobe_color_conflicts_with_rgb() -> None:
|
||||
with pytest.raises(cv.Invalid, match="cannot be used with"):
|
||||
STROBE_COLOR_SCHEMA({"duration": "1s", "color": "red", "red": "10%"})
|
||||
|
||||
|
||||
def test_addressable_color_wipe_plain_defaults_full_level() -> None:
|
||||
result = ADDRESSABLE_COLOR_WIPE_COLOR_SCHEMA({"num_leds": 1})
|
||||
assert (result[CONF_RED], result[CONF_GREEN], result[CONF_BLUE]) == (1.0, 1.0, 1.0)
|
||||
assert result[CONF_COLOR_BRIGHTNESS] == 1.0
|
||||
|
||||
|
||||
def test_addressable_color_wipe_color_name() -> None:
|
||||
result = ADDRESSABLE_COLOR_WIPE_COLOR_SCHEMA({"num_leds": 1, "color": "darkred"})
|
||||
assert "color" not in result
|
||||
assert (result[CONF_RED], result[CONF_GREEN], result[CONF_BLUE]) == (1.0, 0.0, 0.0)
|
||||
assert result[CONF_COLOR_BRIGHTNESS] == pytest.approx(0x8B / 0xFF)
|
||||
|
||||
|
||||
def test_addressable_color_wipe_explicit_channels_unaffected() -> None:
|
||||
result = ADDRESSABLE_COLOR_WIPE_COLOR_SCHEMA({"num_leds": 1, "red": "50%"})
|
||||
assert result[CONF_RED] == 0.5
|
||||
assert result[CONF_COLOR_BRIGHTNESS] == 1.0
|
||||
|
||||
|
||||
def test_addressable_color_wipe_color_conflicts_with_rgb() -> None:
|
||||
with pytest.raises(cv.Invalid, match="cannot be used with"):
|
||||
ADDRESSABLE_COLOR_WIPE_COLOR_SCHEMA(
|
||||
{"num_leds": 1, "color": "blue", "blue": "10%"}
|
||||
)
|
||||
@@ -0,0 +1,37 @@
|
||||
"""Limits and flash layout that let LightState stay small."""
|
||||
|
||||
import pytest
|
||||
|
||||
from esphome import config_validation as cv
|
||||
from esphome.components.light.effects import (
|
||||
MAX_EFFECTS,
|
||||
MONOCHROMATIC_EFFECTS,
|
||||
validate_effects,
|
||||
)
|
||||
|
||||
|
||||
def _effects(count: int) -> list[dict[str, dict[str, str]]]:
|
||||
return [{"pulse": {"name": f"Pulse {i}"}} for i in range(count)]
|
||||
|
||||
|
||||
def test_rejects_more_effects_than_the_index_holds() -> None:
|
||||
with pytest.raises(cv.Invalid, match=f"at most {MAX_EFFECTS} effects"):
|
||||
validate_effects(MONOCHROMATIC_EFFECTS)(_effects(MAX_EFFECTS + 1))
|
||||
|
||||
|
||||
def test_accepts_a_normal_effect_list() -> None:
|
||||
assert len(validate_effects(MONOCHROMATIC_EFFECTS)(_effects(3))) == 3
|
||||
|
||||
|
||||
def test_gamma_table_initializer_holds_the_lut_then_gamma_times_100() -> None:
|
||||
from esphome.components.light import gamma_table_initializer, generate_gamma_table
|
||||
|
||||
init = gamma_table_initializer(2.8)
|
||||
lut = ", ".join(f"0x{int(v):04X}" for v in generate_gamma_table(2.8))
|
||||
assert init == f"{{{{{lut}}}, 280}}"
|
||||
|
||||
|
||||
def test_gamma_table_initializer_saturates_gamma_that_overflows_uint16() -> None:
|
||||
from esphome.components.light import gamma_table_initializer
|
||||
|
||||
assert gamma_table_initializer(1000.0).endswith(", 65535}")
|
||||
@@ -34,12 +34,13 @@ def test_trigger_codegen(
|
||||
"set_manufacturer_uuid128((uint8_t*)(const uint8_t[16]){0xCD,0xAB,0xCD,0xAB,"
|
||||
"0xCD,0xAB,0xCD,0xAB,0xCD,0xAB,0xCD,0xAB,0xCD,0xAB,0xCD,0xAB})" in main_cpp
|
||||
)
|
||||
# scan-control actions: templatable continuous lambda + parented actions.
|
||||
# scan-control actions: templatable continuous lambda + stop_scan forwarded
|
||||
# straight to the tracker (register_apply_action, no action class).
|
||||
# Exactly one set_continuous: the bare start_scan emits none, pinning the
|
||||
# restore-configured-mode divergence from esp32 against a future default=.
|
||||
assert main_cpp.count("->set_continuous(") == 1
|
||||
assert "startscanaction_id->set_continuous(" in main_cpp
|
||||
assert "stopscanaction_id->set_parent(" in main_cpp
|
||||
assert main_cpp.count("->stop_scan();") == 1
|
||||
# scan_parameters continuous: false reaches the YAML-mode setter, not the
|
||||
# runtime override.
|
||||
assert "->set_configured_continuous(false)" in main_cpp
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
"""Tests for the logger component."""
|
||||
|
||||
from collections.abc import Callable
|
||||
from pathlib import Path
|
||||
import re
|
||||
|
||||
import pytest
|
||||
|
||||
from esphome.core import CORE
|
||||
|
||||
|
||||
def test_logger_pre_setup_before_other_components(generate_main):
|
||||
"""Logger::pre_setup() must be called before any other component is created.
|
||||
@@ -84,3 +90,24 @@ def test_libretiny_uart0_is_emitted(generate_main):
|
||||
)
|
||||
|
||||
assert "set_uart_selection(logger::UART_SELECTION_UART0);" in main_cpp
|
||||
|
||||
|
||||
def test_ram_log_strings_is_deprecated(
|
||||
generate_main: Callable[[str | Path], str],
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
"""`false` is accepted but ignored: no RAM build flag, and a warning with the removal version."""
|
||||
generate_main("tests/component_tests/logger/test_logger_ram_log_strings.yaml")
|
||||
|
||||
assert not any("STORE_LOG_STR" in flag for flag in CORE.build_flags)
|
||||
assert "esp8266_store_log_strings_in_flash: false' is ignored" in caplog.text
|
||||
assert "2027.4.0" in caplog.text
|
||||
|
||||
|
||||
def test_flash_log_strings_default_does_not_warn(
|
||||
generate_main: Callable[[str | Path], str],
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
generate_main("tests/component_tests/logger/test_logger.yaml")
|
||||
|
||||
assert "esp8266_store_log_strings_in_flash" not in caplog.text
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
---
|
||||
esphome:
|
||||
name: test
|
||||
|
||||
esp8266:
|
||||
board: d1_mini_lite
|
||||
|
||||
logger:
|
||||
esp8266_store_log_strings_in_flash: false
|
||||
@@ -0,0 +1,36 @@
|
||||
esphome:
|
||||
name: test-debug-outline
|
||||
|
||||
esp32:
|
||||
board: lolin_c3_mini
|
||||
|
||||
spi:
|
||||
mosi_pin:
|
||||
number: GPIO2
|
||||
ignore_strapping_warning: true
|
||||
clk_pin: GPIO1
|
||||
|
||||
display:
|
||||
- platform: mipi_spi
|
||||
data_rate: 20MHz
|
||||
model: st7735
|
||||
cs_pin:
|
||||
number: GPIO8
|
||||
ignore_strapping_warning: true
|
||||
dc_pin:
|
||||
number: GPIO3
|
||||
|
||||
lvgl:
|
||||
debug_outline: true
|
||||
widgets:
|
||||
- obj:
|
||||
id: root_obj
|
||||
widgets:
|
||||
- label:
|
||||
id: title_label
|
||||
text: Title
|
||||
- button:
|
||||
id: ok_button
|
||||
widgets:
|
||||
- label:
|
||||
text: OK
|
||||
@@ -0,0 +1,28 @@
|
||||
esphome:
|
||||
name: test-keyboard-before-textarea
|
||||
|
||||
esp32:
|
||||
board: lolin_c3_mini
|
||||
|
||||
spi:
|
||||
mosi_pin:
|
||||
number: GPIO2
|
||||
ignore_strapping_warning: true
|
||||
clk_pin: GPIO1
|
||||
|
||||
display:
|
||||
- platform: mipi_spi
|
||||
data_rate: 20MHz
|
||||
model: st7735
|
||||
cs_pin:
|
||||
number: GPIO8
|
||||
ignore_strapping_warning: true
|
||||
dc_pin: GPIO3
|
||||
|
||||
lvgl:
|
||||
widgets:
|
||||
- keyboard:
|
||||
id: kb
|
||||
textarea: ta
|
||||
- textarea:
|
||||
id: ta
|
||||
@@ -0,0 +1,34 @@
|
||||
esphome:
|
||||
name: test-keyboard-update-textarea
|
||||
on_boot:
|
||||
- lvgl.keyboard.update:
|
||||
id: kb
|
||||
textarea: ta2
|
||||
|
||||
esp32:
|
||||
board: lolin_c3_mini
|
||||
|
||||
spi:
|
||||
mosi_pin:
|
||||
number: GPIO2
|
||||
ignore_strapping_warning: true
|
||||
clk_pin: GPIO1
|
||||
|
||||
display:
|
||||
- platform: mipi_spi
|
||||
data_rate: 20MHz
|
||||
model: st7735
|
||||
cs_pin:
|
||||
number: GPIO8
|
||||
ignore_strapping_warning: true
|
||||
dc_pin: GPIO3
|
||||
|
||||
lvgl:
|
||||
widgets:
|
||||
- keyboard:
|
||||
id: kb
|
||||
textarea: ta1
|
||||
- textarea:
|
||||
id: ta1
|
||||
- textarea:
|
||||
id: ta2
|
||||
@@ -0,0 +1,35 @@
|
||||
esphome:
|
||||
name: test-multi-instance-theme
|
||||
|
||||
host:
|
||||
mac_address: "62:23:45:AF:B3:DD"
|
||||
|
||||
display:
|
||||
- platform: snapshot
|
||||
id: sdl0
|
||||
dimensions:
|
||||
width: 100
|
||||
height: 100
|
||||
- platform: snapshot
|
||||
id: sdl1
|
||||
dimensions:
|
||||
width: 100
|
||||
height: 100
|
||||
|
||||
lvgl:
|
||||
- id: lvgl_0
|
||||
displays: sdl0
|
||||
widgets:
|
||||
- label:
|
||||
id: label_0
|
||||
text: "First"
|
||||
|
||||
- id: lvgl_1
|
||||
displays: sdl1
|
||||
theme:
|
||||
label:
|
||||
bg_color: 0xFF0000
|
||||
widgets:
|
||||
- label:
|
||||
id: label_1
|
||||
text: "Second"
|
||||
@@ -0,0 +1,35 @@
|
||||
esphome:
|
||||
name: test-debug-outline
|
||||
|
||||
esp32:
|
||||
board: lolin_c3_mini
|
||||
|
||||
spi:
|
||||
mosi_pin:
|
||||
number: GPIO2
|
||||
ignore_strapping_warning: true
|
||||
clk_pin: GPIO1
|
||||
|
||||
display:
|
||||
- platform: mipi_spi
|
||||
data_rate: 20MHz
|
||||
model: st7735
|
||||
cs_pin:
|
||||
number: GPIO8
|
||||
ignore_strapping_warning: true
|
||||
dc_pin:
|
||||
number: GPIO3
|
||||
|
||||
lvgl:
|
||||
widgets:
|
||||
- obj:
|
||||
id: root_obj
|
||||
widgets:
|
||||
- label:
|
||||
id: title_label
|
||||
text: Title
|
||||
- button:
|
||||
id: ok_button
|
||||
widgets:
|
||||
- label:
|
||||
text: OK
|
||||
@@ -191,6 +191,24 @@ class TestTimingSchema:
|
||||
result = TIMING_SCHEMA({"type": "ease_in_out", "weight": 0.5})
|
||||
assert result["weight"] == pytest.approx(0.5)
|
||||
|
||||
def test_ease_in_default_weight(self) -> None:
|
||||
result = TIMING_SCHEMA("ease_in")
|
||||
assert result["type"] == "ease_in"
|
||||
assert result["weight"] == pytest.approx(1.0)
|
||||
|
||||
def test_ease_in_custom_weight(self) -> None:
|
||||
result = TIMING_SCHEMA({"type": "ease_in", "weight": 0.5})
|
||||
assert result["weight"] == pytest.approx(0.5)
|
||||
|
||||
def test_ease_out_default_weight(self) -> None:
|
||||
result = TIMING_SCHEMA("ease_out")
|
||||
assert result["type"] == "ease_out"
|
||||
assert result["weight"] == pytest.approx(1.0)
|
||||
|
||||
def test_ease_out_custom_weight(self) -> None:
|
||||
result = TIMING_SCHEMA({"type": "ease_out", "weight": 0.5})
|
||||
assert result["weight"] == pytest.approx(0.5)
|
||||
|
||||
def test_gravity_defaults(self) -> None:
|
||||
result = TIMING_SCHEMA("gravity")
|
||||
assert result["type"] == "gravity"
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
"""Tests for the LVGL ``debug_outline`` option code generation."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
|
||||
_OUTLINE_COLOR_RE = re.compile(
|
||||
r"lv_obj_set_style_outline_color\(.+?, (lv_color_make\(.+?\)),"
|
||||
)
|
||||
_OUTLINE_WIDTH_RE = re.compile(r"lv_obj_set_style_outline_width\(")
|
||||
_OUTLINE_PAD_RE = re.compile(r"lv_obj_set_style_outline_pad\(.+?, 0,")
|
||||
|
||||
|
||||
class TestDebugOutlineCodeGeneration:
|
||||
"""Verify that ``debug_outline`` outlines every widget in a distinct colour."""
|
||||
|
||||
def test_debug_outline_styles_every_widget(
|
||||
self, generate_main, component_config_path
|
||||
):
|
||||
"""Four widgets are declared, so four outlines with different colours appear."""
|
||||
main_cpp = generate_main(component_config_path("debug_outline.yaml"))
|
||||
assert len(_OUTLINE_WIDTH_RE.findall(main_cpp)) == 4
|
||||
assert len(_OUTLINE_PAD_RE.findall(main_cpp)) == 4
|
||||
colors = _OUTLINE_COLOR_RE.findall(main_cpp)
|
||||
assert len(colors) == 4
|
||||
assert len(set(colors)) == 4
|
||||
|
||||
def test_debug_outline_default_emits_nothing(
|
||||
self, generate_main, component_config_path
|
||||
):
|
||||
"""Without ``debug_outline`` no outline styles are generated."""
|
||||
main_cpp = generate_main(component_config_path("no_debug_outline.yaml"))
|
||||
assert "lv_obj_set_style_outline_" not in main_cpp
|
||||
@@ -0,0 +1,47 @@
|
||||
"""Regression test: a keyboard: declared before its textarea: sibling must
|
||||
still get attached to it, and only after both widgets exist.
|
||||
|
||||
attach_textareas() runs as a deferred pass, after every widget (across every
|
||||
LVGL instance) is created, so it must emit the attach call after the
|
||||
keyboard's own creation statement, not inline during widget creation.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from esphome.__main__ import generate_cpp_contents
|
||||
from esphome.config import read_config
|
||||
from esphome.core import CORE
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def main_cpp(request: pytest.FixtureRequest) -> str:
|
||||
config_path = (
|
||||
Path(request.fspath).parent / "config" / "keyboard_before_textarea_test.yaml"
|
||||
)
|
||||
original_path = CORE.config_path
|
||||
try:
|
||||
CORE.config_path = config_path
|
||||
CORE.config = read_config({})
|
||||
generate_cpp_contents(CORE.config)
|
||||
return CORE.cpp_main_section
|
||||
finally:
|
||||
CORE.config_path = original_path
|
||||
CORE.reset()
|
||||
|
||||
|
||||
def test_keyboard_and_textarea_are_created(main_cpp: str) -> None:
|
||||
assert "lv_keyboard_create(" in main_cpp
|
||||
assert "lv_textarea_create(" in main_cpp
|
||||
|
||||
|
||||
def test_attach_call_runs_after_keyboard_and_textarea_are_created(
|
||||
main_cpp: str,
|
||||
) -> None:
|
||||
attach_index = main_cpp.find("lv_keyboard_set_textarea(kb->obj, ta);")
|
||||
assert attach_index != -1, "keyboard was never attached to its textarea"
|
||||
assert attach_index > main_cpp.find("lv_keyboard_create(")
|
||||
assert attach_index > main_cpp.find("lv_textarea_create(")
|
||||
@@ -0,0 +1,33 @@
|
||||
"""Regression test: lvgl.keyboard.update must be able to change which
|
||||
textarea a keyboard is attached to.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from esphome.__main__ import generate_cpp_contents
|
||||
from esphome.config import read_config
|
||||
from esphome.core import CORE
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def main_cpp(request: pytest.FixtureRequest) -> str:
|
||||
config_path = (
|
||||
Path(request.fspath).parent / "config" / "keyboard_update_textarea_test.yaml"
|
||||
)
|
||||
original_path = CORE.config_path
|
||||
try:
|
||||
CORE.config_path = config_path
|
||||
CORE.config = read_config({})
|
||||
generate_cpp_contents(CORE.config)
|
||||
return CORE.cpp_main_section
|
||||
finally:
|
||||
CORE.config_path = original_path
|
||||
CORE.reset()
|
||||
|
||||
|
||||
def test_keyboard_update_changes_textarea(main_cpp: str) -> None:
|
||||
assert "lv_keyboard_set_textarea(kb->obj, ta2);" in main_cpp
|
||||
@@ -286,7 +286,7 @@ def test_add_text_with_index_moves_before_firing_on_add(main_cpp: str) -> None:
|
||||
|
||||
|
||||
def test_add_button_with_checkable_flag(main_cpp: str) -> None:
|
||||
assert "lv_obj_t *dyn_button_VAR_ = lv_btn_create(test_list);" in main_cpp
|
||||
assert "lv_obj_t *dyn_button_VAR_ = lv_button_create(test_list);" in main_cpp
|
||||
assert (
|
||||
"lv_obj_add_flag(dyn_button_VAR_, (lv_obj_flag_t)(LV_OBJ_FLAG_CHECKABLE));"
|
||||
in main_cpp
|
||||
@@ -310,7 +310,7 @@ def test_add_nested_hierarchy_with_compound_child(main_cpp: str) -> None:
|
||||
assert (
|
||||
"lv_obj_t *dyn_label_1_VAR_ = lv_label_create(dyn_obj_VAR_);\n"
|
||||
" lv_obj_add_style(dyn_label_1_VAR_, _lv_theme_style_label_main_default, "
|
||||
"(lv_state_t)(LV_PART_MAIN));\n"
|
||||
"LV_PART_MAIN);\n"
|
||||
' lv_label_set_text(dyn_label_1_VAR_, "Nested");'
|
||||
) in main_cpp
|
||||
|
||||
@@ -321,7 +321,7 @@ def test_add_applies_theme_styles_to_dynamic_widget(main_cpp: str) -> None:
|
||||
"""
|
||||
assert (
|
||||
"lv_obj_add_style(dyn_label_1_VAR_, _lv_theme_style_label_main_default, "
|
||||
"(lv_state_t)(LV_PART_MAIN));"
|
||||
"LV_PART_MAIN);"
|
||||
) in main_cpp
|
||||
assert "LvDropdownType *dyn_dropdown_1_VAR_ = new LvDropdownType();" in main_cpp
|
||||
assert "lv_dropdown_create(dyn_obj_VAR_)" in main_cpp
|
||||
@@ -343,7 +343,7 @@ def test_add_nested_same_type_child_does_not_shadow_parent(main_cpp: str) -> Non
|
||||
assert (
|
||||
"lv_obj_t *dyn_label_2_VAR_ = lv_label_create(dyn_obj_1_VAR_);\n"
|
||||
" lv_obj_add_style(dyn_label_2_VAR_, _lv_theme_style_label_main_default, "
|
||||
"(lv_state_t)(LV_PART_MAIN));\n"
|
||||
"LV_PART_MAIN);\n"
|
||||
' lv_label_set_text(dyn_label_2_VAR_, "Grandchild");'
|
||||
) in main_cpp
|
||||
|
||||
|
||||
@@ -1,16 +1,4 @@
|
||||
"""Regression test: on_add:/on_remove: containing an lvgl action must not deadlock.
|
||||
|
||||
ListType.to_code() used to build the on_add/on_remove automations directly, during
|
||||
widget creation. Every lvgl action's to_code awaits wait_for_widgets(), which only
|
||||
resolves once *all* widgets - including the list itself - have finished being
|
||||
created. Building an automation containing an lvgl action from inside that same
|
||||
widget-creation walk therefore could never complete: codegen deadlocked with
|
||||
"Circular dependency detected!". Fixed by deferring the actual build_automation()
|
||||
call to finish_list_triggers(), run after set_widgets_completed(True) - and,
|
||||
critically, before generate_triggers(), which is what processes other widgets'
|
||||
on_click etc. automations that might reference this list (e.g. via lvgl.list.add),
|
||||
and which therefore need the list's own on_add/on_remove triggers to already exist.
|
||||
"""
|
||||
"""Regression test: on_add:/on_remove: containing an lvgl action must not deadlock."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
"""``generate_lv_conf_h()`` writes ``#define LV_... 0`` for every LVGL option
|
||||
the configuration does not use, so ``lv_conf.h`` must leave alone the options
|
||||
the user defines in build flags. Otherwise the header, which is included after
|
||||
the compiler ``-D`` flags, turns the option off again.
|
||||
|
||||
Build flags can come from ``esphome: build_flags:`` or from the deprecated
|
||||
``esphome: platformio_options: build_flags:``; both must be honoured.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
|
||||
import pytest
|
||||
|
||||
from esphome.components.lvgl import defines as df, generate_lv_conf_h
|
||||
from esphome.const import CONF_BUILD_FLAGS, CONF_ESPHOME, CONF_PLATFORMIO_OPTIONS
|
||||
from esphome.core import CORE
|
||||
|
||||
|
||||
def _set_esphome_config(
|
||||
build_flags: list[str] | None = None,
|
||||
pio_build_flags: list[str] | str | None = None,
|
||||
) -> None:
|
||||
pio_options = {} if pio_build_flags is None else {"build_flags": pio_build_flags}
|
||||
CORE.config = {
|
||||
CONF_ESPHOME: {
|
||||
CONF_PLATFORMIO_OPTIONS: pio_options,
|
||||
CONF_BUILD_FLAGS: build_flags or [],
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def test_unused_define_is_disabled_without_build_flag() -> None:
|
||||
_set_esphome_config()
|
||||
assert "#define LV_USE_OBSERVER 0" in generate_lv_conf_h().splitlines()
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"flags",
|
||||
[
|
||||
{"build_flags": ["-DLV_USE_OBSERVER=1"]},
|
||||
{"build_flags": ["-D LV_USE_OBSERVER"]},
|
||||
{"pio_build_flags": ["-DLV_USE_OBSERVER=1"]},
|
||||
{"pio_build_flags": "-DLV_USE_OBSERVER=1"},
|
||||
],
|
||||
ids=["esphome", "esphome-spaced", "platformio_options", "platformio_options-str"],
|
||||
)
|
||||
def test_build_flag_define_is_not_disabled(flags: dict) -> None:
|
||||
_set_esphome_config(**flags)
|
||||
assert "#define LV_USE_OBSERVER 0" not in generate_lv_conf_h().splitlines()
|
||||
|
||||
|
||||
def test_esphome_build_flag_clash_warns(caplog: pytest.LogCaptureFixture) -> None:
|
||||
_set_esphome_config(build_flags=["-DLV_USE_ARC=1"])
|
||||
df.add_define("LV_USE_ARC")
|
||||
with caplog.at_level(logging.WARNING):
|
||||
generate_lv_conf_h()
|
||||
assert "LV_USE_ARC" in caplog.text
|
||||
@@ -9,13 +9,16 @@ from esphome.components.lvgl.schemas import theme_schema
|
||||
from esphome.config_validation import Invalid
|
||||
|
||||
|
||||
def _config(displays: list[str], theme: dict | None = None) -> dict:
|
||||
def _config(
|
||||
displays: list[str], theme: dict | None = None, debug_outline: bool = False
|
||||
) -> dict:
|
||||
config = {
|
||||
df.CONF_DISPLAYS: displays,
|
||||
"log_level": "WARN",
|
||||
"color_depth": 16,
|
||||
"byte_order": "big_endian",
|
||||
df.CONF_TRANSPARENCY_KEY: 0x000400,
|
||||
df.CONF_DEBUG_OUTLINE: debug_outline,
|
||||
}
|
||||
if theme is not None:
|
||||
config[df.CONF_THEME] = theme
|
||||
@@ -53,3 +56,20 @@ class TestThemeOnMultipleInstances:
|
||||
def test_passes_when_no_instance_has_theme(self) -> None:
|
||||
configs = [_config(["disp_a"]), _config(["disp_b"])]
|
||||
multi_conf_validate(configs)
|
||||
|
||||
|
||||
class TestDebugOutlineOnMultipleInstances:
|
||||
def test_raises_when_instances_differ(self) -> None:
|
||||
configs = [
|
||||
_config(["disp_a"], debug_outline=True),
|
||||
_config(["disp_b"], debug_outline=False),
|
||||
]
|
||||
with pytest.raises(Invalid, match="'debug_outline' must be the same"):
|
||||
multi_conf_validate(configs)
|
||||
|
||||
def test_passes_when_instances_match(self) -> None:
|
||||
configs = [
|
||||
_config(["disp_a"], debug_outline=True),
|
||||
_config(["disp_b"], debug_outline=True),
|
||||
]
|
||||
multi_conf_validate(configs)
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
"""Regression test: a widget's theme style must be attached regardless of
|
||||
which LVGL instance declares theme: and which instance's widgets are built
|
||||
first.
|
||||
|
||||
theme_to_code() runs once per LVGL instance, interleaved with that instance's
|
||||
own add_widgets(). A widget built by an earlier instance, before any instance
|
||||
has declared theming for its type, must still pick up a later instance's
|
||||
theme: declaration for that type - including on the later instance's own
|
||||
widgets, which get_widget_theme_styles()'s memoisation can poison too.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from esphome.__main__ import generate_cpp_contents
|
||||
from esphome.config import read_config
|
||||
from esphome.core import CORE
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def main_cpp(request: pytest.FixtureRequest) -> str:
|
||||
config_path = (
|
||||
Path(request.fspath).parent / "config" / "multi_instance_theme_test.yaml"
|
||||
)
|
||||
original_path = CORE.config_path
|
||||
try:
|
||||
CORE.config_path = config_path
|
||||
CORE.config = read_config({})
|
||||
generate_cpp_contents(CORE.config)
|
||||
return CORE.cpp_main_section
|
||||
finally:
|
||||
CORE.config_path = original_path
|
||||
CORE.reset()
|
||||
|
||||
|
||||
def test_earlier_instance_widget_is_themed(main_cpp: str) -> None:
|
||||
assert "lv_obj_add_style(label_0, _lv_theme_style_label_main_default," in main_cpp
|
||||
|
||||
|
||||
def test_later_instance_widget_is_themed(main_cpp: str) -> None:
|
||||
assert "lv_obj_add_style(label_1, _lv_theme_style_label_main_default," in main_cpp
|
||||
@@ -1,5 +1,7 @@
|
||||
"""Tests for mipi_rgb configuration validation."""
|
||||
|
||||
from collections.abc import Generator
|
||||
|
||||
import pytest
|
||||
|
||||
from esphome import config_validation as cv
|
||||
@@ -17,6 +19,7 @@ from esphome.components.esp32 import (
|
||||
VARIANT_ESP32S3,
|
||||
VARIANT_ESP32S31,
|
||||
)
|
||||
from esphome.components.mipi import DriverChip
|
||||
import esphome.components.pca9554 # noqa: F401
|
||||
import esphome.components.xl9535 # noqa: F401
|
||||
from esphome.const import (
|
||||
@@ -44,6 +47,19 @@ DATA_PINS = {
|
||||
}
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _remove_test_models() -> Generator[None]:
|
||||
"""Unregister chips created by a test.
|
||||
|
||||
display.py modules drain DriverChip.models when first imported, so a
|
||||
leftover TEST-* chip could become a selectable model there.
|
||||
"""
|
||||
existing = set(DriverChip.models)
|
||||
yield
|
||||
for name in set(DriverChip.models) - existing:
|
||||
del DriverChip.models[name]
|
||||
|
||||
|
||||
def _set_s3(set_core_config: SetCoreConfigCallable) -> None:
|
||||
set_core_config(
|
||||
PlatformFramework.ESP32_IDF,
|
||||
@@ -176,6 +192,41 @@ def test_configuration_succeeds_on_supported_variants(
|
||||
CONFIG_SCHEMA(config)
|
||||
|
||||
|
||||
def test_st7701s_default_reset_delay() -> None:
|
||||
"""ST7701S instances default to a 50ms reset delay.
|
||||
|
||||
The datasheet's stated 5ms is too short in practice; ST7701S overrides the
|
||||
DriverChip default of 10ms with its own default of 50ms.
|
||||
"""
|
||||
from esphome.components.mipi_rgb.models.st7701s import st7701s
|
||||
|
||||
assert st7701s.get_default("reset_delay") == 50
|
||||
|
||||
|
||||
def test_st7701s_reset_delay_can_be_overridden() -> None:
|
||||
"""An explicit reset_delay overrides the ST7701S default of 50ms."""
|
||||
from esphome.components.mipi_rgb.models.st7701s import ST7701S
|
||||
|
||||
chip = ST7701S("TEST-ST7701S-RESET-DELAY", width=480, height=480, reset_delay=99)
|
||||
|
||||
assert chip.get_default("reset_delay") == 99
|
||||
|
||||
|
||||
def test_st7701s_extend_inherits_reset_delay_default() -> None:
|
||||
"""extend() carries the 50ms default forward to derived board models.
|
||||
|
||||
Every shipped ST7701S variant is built via ``st7701s.extend(...)`` rather
|
||||
than direct construction, so the override in ``ST7701S.__init__`` must
|
||||
survive that path (see DriverChip.extend, which re-passes the copied
|
||||
defaults as kwargs to the constructor).
|
||||
"""
|
||||
from esphome.components.mipi_rgb.models.st7701s import st7701s
|
||||
|
||||
extended = st7701s.extend("TEST-ST7701S-EXTEND", width=480, height=480)
|
||||
|
||||
assert extended.get_default("reset_delay") == 50
|
||||
|
||||
|
||||
def test_only_on_variant_rejects_unsupported_variant(
|
||||
set_core_config: SetCoreConfigCallable,
|
||||
) -> None:
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
"""End-to-end tests for the mipi_rgb SPI reset sequence.
|
||||
|
||||
These exercise the actual codegen path (mipi_rgb/display.py's
|
||||
``model.get_sequence(config, add_reset=True)`` call) rather than calling
|
||||
DriverChip.get_sequence directly, so a regression that drops add_reset or
|
||||
reintroduces a hardcoded SWRESET into a model's initsequence would be caught
|
||||
here.
|
||||
"""
|
||||
|
||||
from collections.abc import Callable
|
||||
from pathlib import Path
|
||||
|
||||
# A model with no reset_pin default: SWRESET ({1, 0}) is prepended ahead of the
|
||||
# inherited ST7701S reset_delay ({50, 255}).
|
||||
_NO_RESET_PIN_YAML = """
|
||||
esphome:
|
||||
name: mipi-rgb-reset-test
|
||||
esp32:
|
||||
board: esp32-s3-devkitc-1
|
||||
framework:
|
||||
type: esp-idf
|
||||
psram:
|
||||
mode: octal
|
||||
spi:
|
||||
id: spi_bus
|
||||
clk_pin: 10
|
||||
mosi_pin: 11
|
||||
display:
|
||||
- platform: mipi_rgb
|
||||
id: no_reset_display
|
||||
spi_id: spi_bus
|
||||
model: MAKERFABS-4
|
||||
"""
|
||||
|
||||
# A model with a reset_pin default: no SWRESET, just the settling delay.
|
||||
_RESET_PIN_YAML = """
|
||||
esphome:
|
||||
name: mipi-rgb-reset-test
|
||||
esp32:
|
||||
board: esp32-s3-devkitc-1
|
||||
framework:
|
||||
type: esp-idf
|
||||
psram:
|
||||
mode: octal
|
||||
spi:
|
||||
id: spi_bus
|
||||
clk_pin: 6
|
||||
mosi_pin: 7
|
||||
display:
|
||||
- platform: mipi_rgb
|
||||
id: has_reset_display
|
||||
spi_id: spi_bus
|
||||
model: WAVESHARE-3.16-320X820
|
||||
"""
|
||||
|
||||
|
||||
def test_swreset_and_reset_delay_without_reset_pin(
|
||||
generate_main: Callable[[str | Path], str],
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
"""A model with no reset_pin gets SWRESET plus the ST7701S 50ms delay."""
|
||||
yaml_file = tmp_path / "no_reset.yaml"
|
||||
yaml_file.write_text(_NO_RESET_PIN_YAML)
|
||||
|
||||
main_cpp = generate_main(yaml_file)
|
||||
|
||||
assert "no_reset_display->set_init_sequence({1, 0, 50, 255," in main_cpp
|
||||
|
||||
|
||||
def test_reset_delay_only_with_reset_pin(
|
||||
generate_main: Callable[[str | Path], str],
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
"""A model with a reset_pin default skips SWRESET but keeps the settling delay."""
|
||||
yaml_file = tmp_path / "has_reset.yaml"
|
||||
yaml_file.write_text(_RESET_PIN_YAML)
|
||||
|
||||
main_cpp = generate_main(yaml_file)
|
||||
|
||||
assert "has_reset_display->set_init_sequence({50, 255," in main_cpp
|
||||
assert "has_reset_display->set_init_sequence({1, 0," not in main_cpp
|
||||
@@ -0,0 +1,64 @@
|
||||
"""Tests for DriverChip.get_sequence's reset-delay handling."""
|
||||
|
||||
from collections.abc import Generator
|
||||
|
||||
import pytest
|
||||
|
||||
from esphome.components.mipi import CONF_INVERT_COLORS, CONF_PIXEL_MODE, DriverChip
|
||||
|
||||
# A minimal config with no reset pin: enough for get_sequence(add_madctl=False) to run
|
||||
# without needing a full display configuration.
|
||||
_BASE_CONFIG = {CONF_PIXEL_MODE: "16bit", CONF_INVERT_COLORS: False}
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _remove_test_models() -> Generator[None]:
|
||||
"""Unregister chips created by a test."""
|
||||
existing = set(DriverChip.models)
|
||||
yield
|
||||
for name in set(DriverChip.models) - existing:
|
||||
del DriverChip.models[name]
|
||||
|
||||
|
||||
def test_get_sequence_defaults_to_10ms_reset_delay() -> None:
|
||||
"""A model with no reset_delay default falls back to a 10ms settling delay."""
|
||||
chip = DriverChip("TEST-GET-SEQUENCE-DEFAULT")
|
||||
|
||||
sequence = chip.get_sequence(_BASE_CONFIG, add_madctl=False, add_reset=True)
|
||||
|
||||
# SWRESET ({1, 0}) is prepended (no reset pin configured), followed by the
|
||||
# 10ms settling delay, flattened to {10, 255}.
|
||||
assert sequence[:4] == (1, 0, 10, 255)
|
||||
|
||||
|
||||
def test_get_sequence_uses_model_reset_delay_default() -> None:
|
||||
"""A model's own reset_delay default overrides the base 10ms default."""
|
||||
chip = DriverChip("TEST-GET-SEQUENCE-CUSTOM-DELAY", reset_delay=99)
|
||||
|
||||
sequence = chip.get_sequence(_BASE_CONFIG, add_madctl=False, add_reset=True)
|
||||
|
||||
assert sequence[:4] == (1, 0, 99, 255)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("reset_delay", [0, 256])
|
||||
def test_get_sequence_rejects_out_of_range_reset_delay(reset_delay: int) -> None:
|
||||
"""reset_delay outside 1-255ms is rejected.
|
||||
|
||||
This matches the 1-255ms range map_sequence() already allows for a
|
||||
"delay N" entry in a custom init sequence.
|
||||
"""
|
||||
chip = DriverChip("TEST-GET-SEQUENCE-BAD-DELAY", reset_delay=reset_delay)
|
||||
|
||||
with pytest.raises(ValueError, match="reset_delay must be between 1 and 255ms"):
|
||||
chip.get_sequence(_BASE_CONFIG, add_madctl=False, add_reset=True)
|
||||
|
||||
|
||||
def test_get_sequence_skips_reset_delay_validation_without_add_reset() -> None:
|
||||
"""An out-of-range reset_delay is only checked when add_reset is requested.
|
||||
|
||||
mipi_dsi calls get_sequence with add_reset=False and never uses
|
||||
reset_delay, so an invalid default there should not raise.
|
||||
"""
|
||||
chip = DriverChip("TEST-GET-SEQUENCE-NO-RESET", reset_delay=999)
|
||||
|
||||
chip.get_sequence(_BASE_CONFIG, add_madctl=False, add_reset=False)
|
||||
@@ -0,0 +1,84 @@
|
||||
"""Tag-based sensor defaults, driven through the real CONFIG_SCHEMA."""
|
||||
|
||||
import pytest
|
||||
|
||||
from esphome.components import sensor
|
||||
from esphome.components.mk2pvrouter.sensor import CONFIG_SCHEMA, tag_kind
|
||||
from esphome.const import (
|
||||
CONF_ACCURACY_DECIMALS,
|
||||
CONF_DEVICE_CLASS,
|
||||
CONF_STATE_CLASS,
|
||||
CONF_UNIT_OF_MEASUREMENT,
|
||||
DEVICE_CLASS_EMPTY,
|
||||
STATE_CLASS_MEASUREMENT,
|
||||
)
|
||||
from esphome.types import ConfigType
|
||||
|
||||
|
||||
def _sensor(tag: str, **extra: object) -> ConfigType:
|
||||
return CONFIG_SCHEMA(
|
||||
{"tag": tag, "mk2pvrouter_id": "hub", "name": f"{tag} sensor", **extra}
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("tag", "unit", "device_class", "state_class", "decimals"),
|
||||
[
|
||||
("P", "W", "power", "measurement", 0),
|
||||
("P1", "W", "power", "measurement", 0),
|
||||
("D", "W", "power", "measurement", 0),
|
||||
("D1", "%", "", "measurement", 0),
|
||||
("V", "V", "voltage", "measurement", 2),
|
||||
("v1", "V", "voltage", "measurement", 2),
|
||||
("E", "Wh", "energy", "total_increasing", 0),
|
||||
("T1", "°C", "temperature", "measurement", 2),
|
||||
("R", "W", "power", "measurement", 0),
|
||||
("R1", "", "", "", 0),
|
||||
("R10", "", "", "", 0),
|
||||
],
|
||||
)
|
||||
def test_tag_defaults(
|
||||
tag: str, unit: str, device_class: str, state_class: str, decimals: int
|
||||
) -> None:
|
||||
config = _sensor(tag)
|
||||
assert config[CONF_UNIT_OF_MEASUREMENT] == unit
|
||||
assert config[CONF_DEVICE_CLASS] == device_class
|
||||
assert config[CONF_STATE_CLASS] == sensor.validate_state_class(state_class)
|
||||
assert config[CONF_ACCURACY_DECIMALS] == decimals
|
||||
|
||||
|
||||
@pytest.mark.parametrize("tag", ["S_MC", "STATUS", "X9", "Z", "T", "E1"])
|
||||
def test_unknown_tag_gets_only_the_schema_defaults(tag: str) -> None:
|
||||
config = _sensor(tag)
|
||||
assert CONF_UNIT_OF_MEASUREMENT not in config
|
||||
assert CONF_DEVICE_CLASS not in config
|
||||
assert config[CONF_STATE_CLASS] == sensor.validate_state_class(
|
||||
STATE_CLASS_MEASUREMENT
|
||||
)
|
||||
assert config[CONF_ACCURACY_DECIMALS] == 0
|
||||
|
||||
|
||||
def test_explicit_values_win_over_tag_defaults() -> None:
|
||||
config = _sensor("P", device_class=DEVICE_CLASS_EMPTY, accuracy_decimals=3)
|
||||
assert config[CONF_DEVICE_CLASS] == DEVICE_CLASS_EMPTY
|
||||
assert config[CONF_ACCURACY_DECIMALS] == 3
|
||||
assert config[CONF_UNIT_OF_MEASUREMENT] == "W"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("tag", "scale_centi"),
|
||||
[
|
||||
("V", True),
|
||||
("V1", True),
|
||||
("v2", True),
|
||||
("T1", True),
|
||||
("P", False),
|
||||
("D1", False),
|
||||
("E", False),
|
||||
("T", False),
|
||||
("X9", False),
|
||||
],
|
||||
)
|
||||
def test_centi_scale_follows_the_tag(tag: str, scale_centi: bool) -> None:
|
||||
kind = tag_kind(tag)
|
||||
assert (kind is not None and kind.scale_centi) is scale_centi
|
||||
@@ -18,7 +18,6 @@ from esphome.components.motion import (
|
||||
_build_calibrate_action,
|
||||
_transform_matrix,
|
||||
_validate_matrix_options,
|
||||
clear_calibration_to_code,
|
||||
)
|
||||
from esphome.components.motion.sensor import (
|
||||
_ACCELERATIONS,
|
||||
@@ -785,44 +784,6 @@ class TestClearActionSchema:
|
||||
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
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
"""Shared helpers for the network component tests."""
|
||||
|
||||
from esphome.components.esp32.const import KEY_ESP32, KEY_SDKCONFIG_OPTIONS
|
||||
from esphome.core import CORE
|
||||
|
||||
|
||||
def sdkconfig_option(name: str) -> int | bool | None:
|
||||
"""Return a generated sdkconfig value, or None when unset or not ESP32."""
|
||||
if KEY_ESP32 not in CORE.data:
|
||||
return None
|
||||
return CORE.data[KEY_ESP32][KEY_SDKCONFIG_OPTIONS].get(name)
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
esphome:
|
||||
name: test
|
||||
|
||||
esp32:
|
||||
board: esp32dev
|
||||
framework:
|
||||
type: esp-idf
|
||||
|
||||
ethernet:
|
||||
type: LAN8720
|
||||
mdc_pin: 23
|
||||
mdio_pin: 32
|
||||
clk:
|
||||
pin: 0
|
||||
mode: CLK_EXT_IN
|
||||
phy_addr: 0
|
||||
power_pin: 33
|
||||
|
||||
network:
|
||||
enable_high_performance: true
|
||||
@@ -0,0 +1,23 @@
|
||||
esphome:
|
||||
name: test
|
||||
|
||||
esp32:
|
||||
variant: esp32s3
|
||||
framework:
|
||||
type: esp-idf
|
||||
|
||||
psram:
|
||||
mode: octal
|
||||
ignore_not_found: false
|
||||
|
||||
ethernet:
|
||||
type: W5500
|
||||
clk_pin: GPIO12
|
||||
mosi_pin: GPIO11
|
||||
miso_pin: GPIO13
|
||||
cs_pin: GPIO10
|
||||
interrupt_pin: GPIO14
|
||||
reset_pin: GPIO9
|
||||
|
||||
network:
|
||||
enable_high_performance: true
|
||||
@@ -0,0 +1,30 @@
|
||||
esphome:
|
||||
name: test
|
||||
|
||||
esp32:
|
||||
variant: esp32s3
|
||||
framework:
|
||||
type: esp-idf
|
||||
|
||||
psram:
|
||||
mode: octal
|
||||
ignore_not_found: false
|
||||
|
||||
wifi:
|
||||
ssid: "test_ssid"
|
||||
password: "test_password"
|
||||
|
||||
ethernet:
|
||||
type: W5500
|
||||
clk_pin: GPIO12
|
||||
mosi_pin: GPIO11
|
||||
miso_pin: GPIO13
|
||||
cs_pin: GPIO10
|
||||
interrupt_pin: GPIO14
|
||||
reset_pin: GPIO9
|
||||
|
||||
network:
|
||||
priority:
|
||||
- wifi
|
||||
- ethernet
|
||||
enable_high_performance: true
|
||||
@@ -0,0 +1,14 @@
|
||||
esphome:
|
||||
name: test
|
||||
|
||||
esp32:
|
||||
board: esp32dev
|
||||
framework:
|
||||
type: esp-idf
|
||||
|
||||
wifi:
|
||||
ssid: "test_ssid"
|
||||
password: "test_password"
|
||||
|
||||
network:
|
||||
enable_high_performance: true
|
||||
@@ -0,0 +1,18 @@
|
||||
esphome:
|
||||
name: test
|
||||
|
||||
esp32:
|
||||
variant: esp32s3
|
||||
framework:
|
||||
type: esp-idf
|
||||
|
||||
psram:
|
||||
mode: octal
|
||||
ignore_not_found: false
|
||||
|
||||
wifi:
|
||||
ssid: "test_ssid"
|
||||
password: "test_password"
|
||||
|
||||
network:
|
||||
enable_high_performance: true
|
||||
@@ -0,0 +1,54 @@
|
||||
"""Tests for the ``network: enable_high_performance:`` lwip tier selection.
|
||||
|
||||
Ethernet drivers keep received frames in internal RAM, so an ethernet-only
|
||||
build must keep lwip off the PSRAM tier even when PSRAM is guaranteed.
|
||||
"""
|
||||
|
||||
from collections.abc import Callable
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from esphome.components.network import require_high_performance_networking
|
||||
from tests.component_tests.network import sdkconfig_option
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
(
|
||||
"fixture",
|
||||
"window",
|
||||
"tcp_mailbox",
|
||||
"tcpip_mailbox",
|
||||
"window_scale",
|
||||
"wifi_rx_buffers",
|
||||
),
|
||||
[
|
||||
("high_perf_wifi_psram.yaml", 512000, 512, 512, True, 512),
|
||||
("high_perf_wifi_no_psram.yaml", 65534, 64, 64, None, 64),
|
||||
# dual builds keep wifi's sizes; the ethernet component moves frames to PSRAM
|
||||
("high_perf_wifi_ethernet_psram.yaml", 512000, 512, 512, True, 512),
|
||||
("high_perf_ethernet_no_psram.yaml", 16384, 64, 32, None, None),
|
||||
# ethernet only with PSRAM guaranteed: the cell the guard exists for
|
||||
("high_perf_ethernet_psram.yaml", 16384, 64, 32, None, None),
|
||||
],
|
||||
)
|
||||
def test_lwip_tier(
|
||||
generate_main: Callable[[str | Path], str],
|
||||
component_config_path: Callable[[str], Path],
|
||||
fixture: str,
|
||||
window: int,
|
||||
tcp_mailbox: int,
|
||||
tcpip_mailbox: int,
|
||||
window_scale: bool | None,
|
||||
wifi_rx_buffers: int | None,
|
||||
) -> None:
|
||||
# The wifi component only reacts to a component request, so request it the
|
||||
# way sendspin does; the config key alone decides the lwip tier.
|
||||
require_high_performance_networking()
|
||||
generate_main(component_config_path(fixture))
|
||||
assert sdkconfig_option("CONFIG_LWIP_TCP_WND_DEFAULT") == window
|
||||
assert sdkconfig_option("CONFIG_LWIP_TCP_RECVMBOX_SIZE") == tcp_mailbox
|
||||
assert sdkconfig_option("CONFIG_LWIP_TCPIP_RECVMBOX_SIZE") == tcpip_mailbox
|
||||
assert sdkconfig_option("CONFIG_LWIP_WND_SCALE") is window_scale
|
||||
# Wifi RX buffers really go to PSRAM, so the wifi tier is never downgraded
|
||||
assert sdkconfig_option("CONFIG_ESP_WIFI_DYNAMIC_RX_BUFFER_NUM") == wifi_rx_buffers
|
||||
@@ -19,6 +19,7 @@ from esphome.components.network import (
|
||||
from esphome.const import CONF_PRIORITY, PlatformFramework
|
||||
from esphome.core import CORE
|
||||
import esphome.final_validate as fv
|
||||
from tests.component_tests.network import sdkconfig_option
|
||||
from tests.component_tests.types import SetCoreConfigCallable
|
||||
|
||||
|
||||
@@ -217,13 +218,7 @@ def test_no_primary_interface_define_without_priority(
|
||||
|
||||
|
||||
def _dns_per_default_netif_option() -> bool | None:
|
||||
from esphome.components.esp32.const import KEY_ESP32, KEY_SDKCONFIG_OPTIONS
|
||||
|
||||
if KEY_ESP32 not in CORE.data: # non-ESP32 configs have no sdkconfig at all
|
||||
return None
|
||||
return CORE.data[KEY_ESP32][KEY_SDKCONFIG_OPTIONS].get(
|
||||
"CONFIG_ESP_NETIF_SET_DNS_PER_DEFAULT_NETIF"
|
||||
)
|
||||
return sdkconfig_option("CONFIG_ESP_NETIF_SET_DNS_PER_DEFAULT_NETIF")
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
|
||||
@@ -14,32 +14,24 @@ import pytest
|
||||
from voluptuous import Invalid
|
||||
|
||||
from esphome import config_validation as cv
|
||||
from esphome.components.esp32.const import (
|
||||
KEY_SDKCONFIG_OPTIONS,
|
||||
KEY_VARIANT,
|
||||
VARIANT_ESP32,
|
||||
)
|
||||
from esphome.components.esp32.const import KEY_VARIANT, VARIANT_ESP32
|
||||
from esphome.components.network import (
|
||||
CONF_TCP_SEND_BUFFER,
|
||||
CONFIG_SCHEMA,
|
||||
TCP_SEND_BUFFER_MAX,
|
||||
TCP_SEND_BUFFER_MIN,
|
||||
)
|
||||
from esphome.const import KEY_ESP32, KEY_FRAMEWORK_VERSION, PlatformFramework
|
||||
from esphome.core import CORE
|
||||
from esphome.const import KEY_FRAMEWORK_VERSION, PlatformFramework
|
||||
from tests.component_tests.network import sdkconfig_option
|
||||
from tests.component_tests.types import SetCoreConfigCallable
|
||||
|
||||
|
||||
def _sdkconfig_option(name: str) -> int | None:
|
||||
return CORE.data[KEY_ESP32][KEY_SDKCONFIG_OPTIONS].get(name)
|
||||
|
||||
|
||||
def test_tcp_send_buffer_sets_sdkconfig(
|
||||
generate_main: Callable[[str | Path], str],
|
||||
component_config_path: Callable[[str], Path],
|
||||
) -> None:
|
||||
generate_main(component_config_path("tcp_send_buffer.yaml"))
|
||||
assert _sdkconfig_option("CONFIG_LWIP_TCP_SND_BUF_DEFAULT") == 32000
|
||||
assert sdkconfig_option("CONFIG_LWIP_TCP_SND_BUF_DEFAULT") == 32000
|
||||
|
||||
|
||||
def test_tcp_send_buffer_overrides_high_performance(
|
||||
@@ -48,7 +40,7 @@ def test_tcp_send_buffer_overrides_high_performance(
|
||||
) -> None:
|
||||
"""An explicit size wins over the high performance bundle's 65534."""
|
||||
generate_main(component_config_path("tcp_send_buffer_high_perf.yaml"))
|
||||
assert _sdkconfig_option("CONFIG_LWIP_TCP_SND_BUF_DEFAULT") == 16384
|
||||
assert sdkconfig_option("CONFIG_LWIP_TCP_SND_BUF_DEFAULT") == 16384
|
||||
|
||||
|
||||
@pytest.mark.parametrize("value", [TCP_SEND_BUFFER_MIN, TCP_SEND_BUFFER_MAX])
|
||||
|
||||
@@ -12,10 +12,11 @@ from esphome import config_validation as cv
|
||||
from esphome.components.esphome.ota import (
|
||||
AUTO_LOAD,
|
||||
FILTER_SOURCE_FILES,
|
||||
_encryption_schema,
|
||||
_validate_no_password_with_encryption,
|
||||
ota_esphome_final_validate,
|
||||
)
|
||||
from esphome.components.noise import static_encryption_key
|
||||
from esphome.components.noise import encryption_schema, static_encryption_key
|
||||
from esphome.const import (
|
||||
CONF_API,
|
||||
CONF_ENCRYPTION,
|
||||
@@ -29,6 +30,7 @@ from esphome.const import (
|
||||
CONF_VERSION,
|
||||
)
|
||||
from esphome.core import CORE, ID
|
||||
from esphome.espota2 import CONF_ALLOW_PLAINTEXT_UPLOAD
|
||||
import esphome.final_validate as fv
|
||||
|
||||
|
||||
@@ -198,6 +200,21 @@ def test_encryption_without_any_key_rejected() -> None:
|
||||
fv.full_config.reset(token)
|
||||
|
||||
|
||||
def test_encryption_schema_allow_plaintext_upload() -> None:
|
||||
"""The opt in is an ota only option with no default, so a merged block
|
||||
that does not mention it cannot clear it; the shared api schema does not
|
||||
know it."""
|
||||
assert _encryption_schema(None) == {}
|
||||
assert _encryption_schema({CONF_ALLOW_PLAINTEXT_UPLOAD: True}) == {
|
||||
CONF_ALLOW_PLAINTEXT_UPLOAD: True
|
||||
}
|
||||
assert _encryption_schema({CONF_KEY: API_KEY}) == {CONF_KEY: API_KEY}
|
||||
with pytest.raises(cv.Invalid):
|
||||
_encryption_schema(False)
|
||||
with pytest.raises(cv.Invalid):
|
||||
encryption_schema({CONF_ALLOW_PLAINTEXT_UPLOAD: True})
|
||||
|
||||
|
||||
def test_encryption_key_mismatch_between_merged_configs_rejected() -> None:
|
||||
"""Same-port configs with different encryption keys raise."""
|
||||
full_conf = {
|
||||
@@ -476,43 +493,43 @@ def test_static_encryption_key() -> None:
|
||||
("yaml_name", "defines_present", "defines_absent"),
|
||||
[
|
||||
# An api key alone compiles the transport in without requiring it;
|
||||
# the device uses the api server's key, not a copy
|
||||
# the ota keeps its own pointer to the key so safe mode, which never
|
||||
# constructs the api server, can still use it
|
||||
(
|
||||
"api_key_offer",
|
||||
{"USE_OTA_ENCRYPTION", "USE_OTA_ENCRYPTION_FROM_API"},
|
||||
{"USE_OTA_ENCRYPTION"},
|
||||
{"USE_OTA_ENCRYPTION_REQUIRED", "USE_OTA_ENCRYPTION_PROVISIONED"},
|
||||
),
|
||||
# A password still guards plaintext uploads on an offering device
|
||||
(
|
||||
"api_key_offer_password",
|
||||
{"USE_OTA_ENCRYPTION", "USE_OTA_ENCRYPTION_FROM_API", "USE_OTA_PASSWORD"},
|
||||
{"USE_OTA_ENCRYPTION", "USE_OTA_PASSWORD"},
|
||||
{"USE_OTA_ENCRYPTION_REQUIRED", "USE_OTA_ENCRYPTION_PROVISIONED"},
|
||||
),
|
||||
# The migration install keeps the password for the old firmware's
|
||||
# prompt but the build it sends is authenticated by the key alone
|
||||
(
|
||||
"migration_password",
|
||||
{"USE_OTA_ENCRYPTION", "USE_OTA_ENCRYPTION_REQUIRED"},
|
||||
{"USE_OTA_PASSWORD", "USE_OTA_ENCRYPTION_PROVISIONED"},
|
||||
),
|
||||
# The ota encryption block is what makes the device refuse plaintext
|
||||
(
|
||||
"encryption_required",
|
||||
{
|
||||
"USE_OTA_ENCRYPTION",
|
||||
"USE_OTA_ENCRYPTION_REQUIRED",
|
||||
"USE_OTA_ENCRYPTION_FROM_API",
|
||||
},
|
||||
{"USE_OTA_ENCRYPTION", "USE_OTA_ENCRYPTION_REQUIRED"},
|
||||
{"USE_OTA_ENCRYPTION_PROVISIONED"},
|
||||
),
|
||||
# Without api encryption the ota key is the device's own
|
||||
(
|
||||
"own_key",
|
||||
{"USE_OTA_ENCRYPTION", "USE_OTA_ENCRYPTION_REQUIRED"},
|
||||
{"USE_OTA_ENCRYPTION_FROM_API", "USE_OTA_ENCRYPTION_PROVISIONED"},
|
||||
{"USE_OTA_ENCRYPTION_PROVISIONED"},
|
||||
),
|
||||
# A key provisioned at runtime lives in the api server; the device
|
||||
# offers with it once provisioned and never requires it
|
||||
(
|
||||
"runtime_api_key",
|
||||
{
|
||||
"USE_OTA_ENCRYPTION",
|
||||
"USE_OTA_ENCRYPTION_FROM_API",
|
||||
"USE_OTA_ENCRYPTION_PROVISIONED",
|
||||
},
|
||||
{"USE_OTA_ENCRYPTION", "USE_OTA_ENCRYPTION_PROVISIONED"},
|
||||
{"USE_OTA_ENCRYPTION_REQUIRED"},
|
||||
),
|
||||
# No api encryption at all keeps the noise glue out of the build
|
||||
@@ -522,7 +539,6 @@ def test_static_encryption_key() -> None:
|
||||
{
|
||||
"USE_OTA_ENCRYPTION",
|
||||
"USE_OTA_ENCRYPTION_REQUIRED",
|
||||
"USE_OTA_ENCRYPTION_FROM_API",
|
||||
"USE_OTA_ENCRYPTION_PROVISIONED",
|
||||
},
|
||||
),
|
||||
@@ -541,8 +557,10 @@ def test_encryption_offer_codegen(
|
||||
assert defines_present <= defines
|
||||
assert not (defines_absent & defines)
|
||||
encrypted = "USE_OTA_ENCRYPTION" in defines_present
|
||||
own_key = encrypted and "USE_OTA_ENCRYPTION_FROM_API" not in defines_present
|
||||
own_key = encrypted and "USE_OTA_ENCRYPTION_PROVISIONED" not in defines_present
|
||||
assert ("esphome_esphomeotacomponent_id->set_noise_psk(" in main_cpp) is own_key
|
||||
# The api shares the ota's array instead of emitting the same key twice
|
||||
assert main_cpp.count("_psk[] PROGMEM") == (1 if own_key else 0)
|
||||
assert ("set_auth_password(" in main_cpp) is ("USE_OTA_PASSWORD" in defines_present)
|
||||
# The noise transport source compiles only when the define is set
|
||||
assert FILTER_SOURCE_FILES() == ([] if encrypted else ["ota_esphome_noise.cpp"])
|
||||
@@ -555,6 +573,16 @@ def test_password_with_encryption_rejected() -> None:
|
||||
_validate_no_password_with_encryption(config)
|
||||
|
||||
|
||||
def test_password_with_migration_install_accepted() -> None:
|
||||
"""The old firmware may still ask for the password on the plaintext
|
||||
leg of the migration install."""
|
||||
config = {
|
||||
CONF_PASSWORD: "pw",
|
||||
CONF_ENCRYPTION: {CONF_KEY: API_KEY, CONF_ALLOW_PLAINTEXT_UPLOAD: True},
|
||||
}
|
||||
assert _validate_no_password_with_encryption(config) is config
|
||||
|
||||
|
||||
def test_password_alone_accepted() -> None:
|
||||
"""A password without encryption still validates."""
|
||||
config = {CONF_PASSWORD: "pw"}
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
esphome:
|
||||
name: ota-migration-password
|
||||
|
||||
host:
|
||||
|
||||
api:
|
||||
encryption:
|
||||
key: "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8="
|
||||
|
||||
ota:
|
||||
- platform: esphome
|
||||
password: "superlongpasswordthatnoonewillknow"
|
||||
encryption:
|
||||
allow_plaintext_upload: true
|
||||
@@ -0,0 +1,28 @@
|
||||
"""The PZEM-6L24 answers unit addresses 1 to 247 only."""
|
||||
|
||||
import pytest
|
||||
|
||||
from esphome import config_validation as cv
|
||||
from esphome.components import modbus
|
||||
from esphome.components.pzem6l24.sensor import CONFIG_SCHEMA
|
||||
from esphome.const import CONF_ADDRESS
|
||||
from esphome.types import ConfigType
|
||||
|
||||
|
||||
def _sensor(**extra: object) -> ConfigType:
|
||||
return CONFIG_SCHEMA({modbus.CONF_MODBUS_ID: "bus", **extra})
|
||||
|
||||
|
||||
def test_address_defaults_to_one() -> None:
|
||||
assert _sensor()[CONF_ADDRESS] == 1
|
||||
|
||||
|
||||
@pytest.mark.parametrize("address", [1, 247])
|
||||
def test_address_in_unit_range_accepted(address: int) -> None:
|
||||
assert _sensor(**{CONF_ADDRESS: address})[CONF_ADDRESS] == address
|
||||
|
||||
|
||||
@pytest.mark.parametrize("address", [0, 248, 255])
|
||||
def test_address_outside_unit_range_rejected(address: int) -> None:
|
||||
with pytest.raises(cv.Invalid):
|
||||
_sensor(**{CONF_ADDRESS: address})
|
||||
@@ -37,12 +37,13 @@ def test_trigger_codegen(
|
||||
"set_manufacturer_uuid128((uint8_t*)(const uint8_t[16]){0xCD,0xAB,0xCD,0xAB,"
|
||||
"0xCD,0xAB,0xCD,0xAB,0xCD,0xAB,0xCD,0xAB,0xCD,0xAB,0xCD,0xAB})" in main_cpp
|
||||
)
|
||||
# scan-control actions: templatable continuous lambda + parented actions.
|
||||
# scan-control actions: templatable continuous lambda + stop_scan forwarded
|
||||
# straight to the tracker (register_apply_action, no action class).
|
||||
# Exactly one set_continuous: the bare start_scan emits none, pinning the
|
||||
# restore-configured-mode divergence from esp32 against a future default=.
|
||||
assert main_cpp.count("->set_continuous(") == 1
|
||||
assert "startscanaction_id->set_continuous(" in main_cpp
|
||||
assert "stopscanaction_id->set_parent(" in main_cpp
|
||||
assert main_cpp.count("->stop_scan();") == 1
|
||||
# scan_parameters continuous: false reaches the YAML-mode setter, not the
|
||||
# runtime override.
|
||||
assert "->set_configured_continuous(false)" in main_cpp
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
"""Tests for UART clock source validation and code generation."""
|
||||
|
||||
from collections.abc import Callable
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from esphome.components.esp32 import KEY_VARIANT, VARIANTS
|
||||
from esphome.components.uart import (
|
||||
CONF_CLOCK_SOURCE,
|
||||
CONFIG_SCHEMA,
|
||||
UART_CLOCK_SOURCES,
|
||||
UART_CLOCK_SOURCES_BY_VARIANT,
|
||||
)
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import PlatformFramework
|
||||
from tests.component_tests.types import SetCoreConfigCallable
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def clock_source_validator() -> cv.All:
|
||||
"""Find the clock validator without depending on pre-validator ordering."""
|
||||
schema = next(
|
||||
validator
|
||||
for validator in CONFIG_SCHEMA.validators
|
||||
if isinstance(validator, cv.Schema)
|
||||
)
|
||||
return schema.schema[cv.Optional(CONF_CLOCK_SOURCE)]
|
||||
|
||||
|
||||
def test_clock_source_table_covers_all_variants() -> None:
|
||||
"""New ESP32 variants must declare their supported UART clocks."""
|
||||
assert set(UART_CLOCK_SOURCES_BY_VARIANT) == set(VARIANTS)
|
||||
for sources in UART_CLOCK_SOURCES_BY_VARIANT.values():
|
||||
assert "DEFAULT" in sources
|
||||
assert set(sources) <= UART_CLOCK_SOURCES.keys()
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("variant", "source", "supported"),
|
||||
[
|
||||
("ESP32", "REF_TICK", True),
|
||||
("ESP32", "XTAL", False),
|
||||
("ESP32S2", "REF_TICK", True),
|
||||
("ESP32S2", "RTC", False),
|
||||
("ESP32C3", "APB", True),
|
||||
("ESP32C3", "xtal", True),
|
||||
("ESP32C3", "REF_TICK", False),
|
||||
("ESP32S3", "RTC", True),
|
||||
("ESP32S3", "REF_TICK", False),
|
||||
("ESP32C2", "APB", False),
|
||||
("ESP32C6", "APB", False),
|
||||
("ESP32C6", "DEFAULT", True),
|
||||
("ESP32C5", "RTC", True),
|
||||
("ESP32C61", "XTAL", True),
|
||||
("ESP32H4", "RTC", True),
|
||||
("ESP32H21", "XTAL", True),
|
||||
("ESP32S31", "RTC", True),
|
||||
("ESP32H2", "XTAL", True),
|
||||
("ESP32P4", "RTC", True),
|
||||
("ESP32C3", "INVALID", False),
|
||||
],
|
||||
)
|
||||
def test_clock_source_validation(
|
||||
variant: str,
|
||||
source: str,
|
||||
supported: bool,
|
||||
set_core_config: SetCoreConfigCallable,
|
||||
clock_source_validator: cv.All,
|
||||
) -> None:
|
||||
"""Reject unsupported clocks and normalize accepted names."""
|
||||
set_core_config(
|
||||
PlatformFramework.ESP32_IDF,
|
||||
platform_data={KEY_VARIANT: variant},
|
||||
)
|
||||
if supported:
|
||||
assert clock_source_validator(source) == source.upper()
|
||||
else:
|
||||
with pytest.raises(cv.Invalid):
|
||||
clock_source_validator(source)
|
||||
|
||||
|
||||
def test_clock_source_requires_esp32(
|
||||
set_core_config: SetCoreConfigCallable,
|
||||
clock_source_validator: cv.All,
|
||||
) -> None:
|
||||
"""Reject the ESP32-only option before accessing the chip variant."""
|
||||
set_core_config(PlatformFramework.ESP8266_ARDUINO)
|
||||
with pytest.raises(cv.Invalid, match="ESP32"):
|
||||
clock_source_validator("DEFAULT")
|
||||
|
||||
|
||||
@pytest.mark.parametrize("source", [None, "default", "xtal"])
|
||||
def test_clock_source_codegen(
|
||||
source: str | None,
|
||||
tmp_path: Path,
|
||||
generate_main: Callable[[str | Path], str],
|
||||
) -> None:
|
||||
"""Emit IDF constants directly and skip the setter when nothing changes."""
|
||||
config = tmp_path / "uart.yaml"
|
||||
config.write_text(
|
||||
"esphome:\n name: uart-clock-test\n"
|
||||
"esp32:\n variant: esp32c3\n framework:\n type: esp-idf\n"
|
||||
"uart:\n id: test_uart\n tx_pin: GPIO4\n baud_rate: 9600\n"
|
||||
+ (f" clock_source: {source}\n" if source else ""),
|
||||
encoding="utf-8",
|
||||
)
|
||||
main_cpp = generate_main(config)
|
||||
if source in (None, "default"):
|
||||
assert "set_clock_source(" not in main_cpp
|
||||
else:
|
||||
assert f"test_uart->set_clock_source(::UART_SCLK_{source.upper()});" in main_cpp
|
||||
@@ -0,0 +1,8 @@
|
||||
esphome:
|
||||
name: mtutest
|
||||
|
||||
nrf52:
|
||||
board: adafruit_itsybitsy_nrf52840
|
||||
bootloader: adafruit_nrf52_sd140_v6
|
||||
|
||||
zephyr_ble_server:
|
||||
@@ -0,0 +1,11 @@
|
||||
esphome:
|
||||
name: mtutest
|
||||
|
||||
nrf52:
|
||||
board: adafruit_itsybitsy_nrf52840
|
||||
bootloader: adafruit_nrf52_sd140_v6
|
||||
|
||||
logger:
|
||||
|
||||
ble_nus:
|
||||
type: logs
|
||||
@@ -0,0 +1,18 @@
|
||||
esphome:
|
||||
name: mtutest
|
||||
|
||||
nrf52:
|
||||
board: adafruit_itsybitsy_nrf52840
|
||||
bootloader: adafruit_nrf52_sd140_v6
|
||||
|
||||
logger:
|
||||
|
||||
zephyr_ble_server:
|
||||
|
||||
ble_nus:
|
||||
type: logs
|
||||
|
||||
ota:
|
||||
- platform: zephyr_mcumgr
|
||||
transport:
|
||||
ble: true
|
||||
@@ -0,0 +1,52 @@
|
||||
"""Tests for the BLE L2CAP MTU that zephyr_ble_server writes to prj.conf."""
|
||||
|
||||
from collections.abc import Callable
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from esphome.components.zephyr import zephyr_data
|
||||
from esphome.components.zephyr.const import KEY_PRJ_CONF
|
||||
from esphome.components.zephyr_ble_server import _emit_ble_mtu, request_ble_l2cap_mtu
|
||||
|
||||
MTU_KEYS = (
|
||||
"CONFIG_BT_L2CAP_TX_MTU",
|
||||
"CONFIG_BT_BUF_ACL_TX_SIZE",
|
||||
"CONFIG_BT_BUF_ACL_RX_SIZE",
|
||||
)
|
||||
|
||||
|
||||
def _mtu_values() -> tuple[int | None, ...]:
|
||||
prj_conf = zephyr_data()[KEY_PRJ_CONF][""]
|
||||
return tuple(prj_conf.get(key, (None,))[0] for key in MTU_KEYS)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("fixture", "expected"),
|
||||
[
|
||||
("no_request.yaml", (None, None, None)),
|
||||
("nus.yaml", (247, 251, 251)),
|
||||
# The largest request wins; only the TX buffer is capped at 251
|
||||
("nus_mcumgr.yaml", (498, 251, 502)),
|
||||
],
|
||||
)
|
||||
def test_l2cap_mtu_prj_conf(
|
||||
fixture: str,
|
||||
expected: tuple[int | None, ...],
|
||||
generate_main: Callable[[str | Path], str],
|
||||
component_config_path: Callable[[str], Path],
|
||||
) -> None:
|
||||
generate_main(component_config_path(fixture))
|
||||
assert _mtu_values() == expected
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_request_after_emit_raises(
|
||||
generate_main: Callable[[str | Path], str],
|
||||
component_config_path: Callable[[str], Path],
|
||||
) -> None:
|
||||
generate_main(component_config_path("no_request.yaml"))
|
||||
request_ble_l2cap_mtu(247)
|
||||
await _emit_ble_mtu()
|
||||
with pytest.raises(RuntimeError, match="after it was written"):
|
||||
request_ble_l2cap_mtu(498)
|
||||
Reference in New Issue
Block a user