[lvgl] Allow line points as percentages (#16209)

This commit is contained in:
Clyde Stubbs
2026-05-06 21:22:43 +10:00
committed by GitHub
parent febf8815c7
commit 79786f1cc7
7 changed files with 271 additions and 22 deletions

View File

@@ -0,0 +1,84 @@
esphome:
name: test-line
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:
# Dict format
- line:
id: line_dict
points:
- x: 10
y: 20
- x: 100
y: 200
- x: 0
y: 0
# List format
- line:
id: line_list
points:
- [10, 20]
- [100, 200]
- [0, 0]
# String format
- line:
id: line_string
points:
- "10, 20"
- "100, 200"
- "0, 0"
# Percentage - dict format
- line:
id: line_pct_dict
points:
- x: "50%"
y: "75%"
# Percentage - list format
- line:
id: line_pct_list
points:
- ["50%", "75%"]
# Percentage - string format
- line:
id: line_pct_string
points:
- "50%, 75%"
# Mixed integer and percentage
- line:
id: line_mixed_dict
points:
- x: 10
y: "50%"
- x: "25%"
y: 200
- line:
id: line_mixed_list
points:
- [10, "50%"]
- ["25%", 200]

View File

@@ -0,0 +1,147 @@
"""Tests for the LVGL line widget point schema and code generation."""
from __future__ import annotations
import re
import pytest
from esphome.components.lvgl.schemas import point_schema
from esphome.config_validation import Invalid
from esphome.const import CONF_X, CONF_Y
# ---------------------------------------------------------------------------
# Validation: point_schema normalises dict / list / string to same result
# ---------------------------------------------------------------------------
class TestPointSchemaValidation:
"""Test that all point input formats normalise to the same dict."""
@pytest.mark.parametrize(
"dict_input,list_input,string_input",
[
({CONF_X: 10, CONF_Y: 20}, [10, 20], "10, 20"),
({CONF_X: 0, CONF_Y: 0}, [0, 0], "0, 0"),
({CONF_X: 100, CONF_Y: 200}, [100, 200], "100, 200"),
({CONF_X: -5, CONF_Y: -10}, [-5, -10], "-5, -10"),
],
)
def test_integer_formats_produce_same_result(
self, dict_input, list_input, string_input
):
result_dict = point_schema(dict_input)
result_list = point_schema(list_input)
result_string = point_schema(string_input)
assert result_dict == result_list
assert result_dict == result_string
def test_percentage_formats_produce_same_result(self):
result_dict = point_schema({CONF_X: "50%", CONF_Y: "75%"})
result_list = point_schema(["50%", "75%"])
result_string = point_schema("50%, 75%")
assert result_dict == result_list
assert result_dict == result_string
def test_pixel_suffix_matches_plain_integer(self):
result_px = point_schema({CONF_X: "10px", CONF_Y: "20px"})
result_int = point_schema({CONF_X: 10, CONF_Y: 20})
assert result_px == result_int
@pytest.mark.parametrize(
"value",
[
{CONF_X: 50, CONF_Y: 75},
[50, 75],
"50, 75",
],
)
def test_output_contains_x_and_y(self, value):
result = point_schema(value)
assert CONF_X in result
assert CONF_Y in result
def test_list_wrong_length_raises(self):
with pytest.raises(Invalid, match="Invalid point"):
point_schema([1])
with pytest.raises(Invalid, match="Invalid point"):
point_schema([1, 2, 3])
def test_string_without_comma_raises(self):
with pytest.raises(Invalid, match="Invalid point"):
point_schema("garbage")
def test_string_extra_commas_raises(self):
with pytest.raises(Invalid, match="Invalid point"):
point_schema("1,2,3")
# ---------------------------------------------------------------------------
# Code generation: different point formats produce identical C++ output
# ---------------------------------------------------------------------------
_SET_POINTS_RE = re.compile(r"(\w+)->set_points\((.+?)\);")
def _extract_set_points(main_cpp: str) -> dict[str, str]:
"""Return {var_name: args_text} for every set_points() call found."""
return {m.group(1): m.group(2) for m in _SET_POINTS_RE.finditer(main_cpp)}
class TestLineCodeGeneration:
"""Verify that alternative point formats generate identical C++ code."""
@pytest.fixture()
def main_cpp(self, generate_main, component_config_path) -> str:
return generate_main(component_config_path("line_points.yaml"))
@pytest.fixture()
def set_points_calls(self, main_cpp) -> dict[str, str]:
return _extract_set_points(main_cpp)
def test_integer_points_all_formats_match(self, set_points_calls):
"""Dict, list, and string formats with integer points produce same set_points call."""
assert set_points_calls["line_dict"] == set_points_calls["line_list"]
assert set_points_calls["line_dict"] == set_points_calls["line_string"]
def test_percentage_points_all_formats_match(self, set_points_calls):
"""Dict, list, and string formats with percentage points produce same set_points call."""
assert set_points_calls["line_pct_dict"] == set_points_calls["line_pct_list"]
assert set_points_calls["line_pct_dict"] == set_points_calls["line_pct_string"]
def test_mixed_points_formats_match(self, set_points_calls):
"""Dict and list formats with mixed int/percent points produce same set_points call."""
assert (
set_points_calls["line_mixed_dict"] == set_points_calls["line_mixed_list"]
)
def test_integer_points_contain_expected_values(self, set_points_calls):
"""Integer points appear literally in the generated code."""
args = set_points_calls["line_dict"]
for val in ("10", "20", "100", "200"):
assert val in args
def test_percentage_points_use_lv_pct(self, set_points_calls):
"""Percentage points are generated using the lv_pct() macro."""
args = set_points_calls["line_pct_dict"]
assert "lv_pct(50)" in args
assert "lv_pct(75)" in args
def test_all_lines_present(self, set_points_calls):
"""All expected line IDs have a set_points call."""
expected = {
"line_dict",
"line_list",
"line_string",
"line_pct_dict",
"line_pct_list",
"line_pct_string",
"line_mixed_dict",
"line_mixed_list",
}
assert expected.issubset(set_points_calls.keys())

View File

@@ -1038,7 +1038,10 @@ lvgl:
- 5, 5
- x: !lambda return random_uint32() % 100;
y: !lambda return random_uint32() % 100;
- 70, 70
- x: 10%
y: 50%
- 70%, 70%
- [75%, 75%]
- 120, 10
- 180, 60
- 240, 10