mirror of
https://github.com/esphome/esphome.git
synced 2025-07-29 22:56:37 +00:00
[image] Add byte order option and unit tests (#9326)
This commit is contained in:
parent
51377b2625
commit
4648804db6
@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
CODEOWNERS = ["@esphome/core"]
|
CODEOWNERS = ["@esphome/core"]
|
||||||
|
|
||||||
|
CONF_BYTE_ORDER = "byte_order"
|
||||||
CONF_DRAW_ROUNDING = "draw_rounding"
|
CONF_DRAW_ROUNDING = "draw_rounding"
|
||||||
CONF_ON_STATE_CHANGE = "on_state_change"
|
CONF_ON_STATE_CHANGE = "on_state_change"
|
||||||
CONF_REQUEST_HEADERS = "request_headers"
|
CONF_REQUEST_HEADERS = "request_headers"
|
||||||
|
@ -10,8 +10,10 @@ from PIL import Image, UnidentifiedImageError
|
|||||||
|
|
||||||
from esphome import core, external_files
|
from esphome import core, external_files
|
||||||
import esphome.codegen as cg
|
import esphome.codegen as cg
|
||||||
|
from esphome.components.const import CONF_BYTE_ORDER
|
||||||
import esphome.config_validation as cv
|
import esphome.config_validation as cv
|
||||||
from esphome.const import (
|
from esphome.const import (
|
||||||
|
CONF_DEFAULTS,
|
||||||
CONF_DITHER,
|
CONF_DITHER,
|
||||||
CONF_FILE,
|
CONF_FILE,
|
||||||
CONF_ICON,
|
CONF_ICON,
|
||||||
@ -38,6 +40,7 @@ CONF_OPAQUE = "opaque"
|
|||||||
CONF_CHROMA_KEY = "chroma_key"
|
CONF_CHROMA_KEY = "chroma_key"
|
||||||
CONF_ALPHA_CHANNEL = "alpha_channel"
|
CONF_ALPHA_CHANNEL = "alpha_channel"
|
||||||
CONF_INVERT_ALPHA = "invert_alpha"
|
CONF_INVERT_ALPHA = "invert_alpha"
|
||||||
|
CONF_IMAGES = "images"
|
||||||
|
|
||||||
TRANSPARENCY_TYPES = (
|
TRANSPARENCY_TYPES = (
|
||||||
CONF_OPAQUE,
|
CONF_OPAQUE,
|
||||||
@ -188,6 +191,10 @@ class ImageRGB565(ImageEncoder):
|
|||||||
dither,
|
dither,
|
||||||
invert_alpha,
|
invert_alpha,
|
||||||
)
|
)
|
||||||
|
self.big_endian = True
|
||||||
|
|
||||||
|
def set_big_endian(self, big_endian: bool) -> None:
|
||||||
|
self.big_endian = big_endian
|
||||||
|
|
||||||
def convert(self, image, path):
|
def convert(self, image, path):
|
||||||
return image.convert("RGBA")
|
return image.convert("RGBA")
|
||||||
@ -205,10 +212,16 @@ class ImageRGB565(ImageEncoder):
|
|||||||
g = 1
|
g = 1
|
||||||
b = 0
|
b = 0
|
||||||
rgb = (r << 11) | (g << 5) | b
|
rgb = (r << 11) | (g << 5) | b
|
||||||
|
if self.big_endian:
|
||||||
self.data[self.index] = rgb >> 8
|
self.data[self.index] = rgb >> 8
|
||||||
self.index += 1
|
self.index += 1
|
||||||
self.data[self.index] = rgb & 0xFF
|
self.data[self.index] = rgb & 0xFF
|
||||||
self.index += 1
|
self.index += 1
|
||||||
|
else:
|
||||||
|
self.data[self.index] = rgb & 0xFF
|
||||||
|
self.index += 1
|
||||||
|
self.data[self.index] = rgb >> 8
|
||||||
|
self.index += 1
|
||||||
if self.transparency == CONF_ALPHA_CHANNEL:
|
if self.transparency == CONF_ALPHA_CHANNEL:
|
||||||
if self.invert_alpha:
|
if self.invert_alpha:
|
||||||
a ^= 0xFF
|
a ^= 0xFF
|
||||||
@ -364,7 +377,7 @@ def validate_file_shorthand(value):
|
|||||||
value = cv.string_strict(value)
|
value = cv.string_strict(value)
|
||||||
parts = value.strip().split(":")
|
parts = value.strip().split(":")
|
||||||
if len(parts) == 2 and parts[0] in MDI_SOURCES:
|
if len(parts) == 2 and parts[0] in MDI_SOURCES:
|
||||||
match = re.match(r"[a-zA-Z0-9\-]+", parts[1])
|
match = re.match(r"^[a-zA-Z0-9\-]+$", parts[1])
|
||||||
if match is None:
|
if match is None:
|
||||||
raise cv.Invalid(f"Could not parse mdi icon name from '{value}'.")
|
raise cv.Invalid(f"Could not parse mdi icon name from '{value}'.")
|
||||||
return download_gh_svg(parts[1], parts[0])
|
return download_gh_svg(parts[1], parts[0])
|
||||||
@ -434,20 +447,29 @@ def validate_type(image_types):
|
|||||||
|
|
||||||
|
|
||||||
def validate_settings(value):
|
def validate_settings(value):
|
||||||
type = value[CONF_TYPE]
|
"""
|
||||||
|
Validate the settings for a single image configuration.
|
||||||
|
"""
|
||||||
|
conf_type = value[CONF_TYPE]
|
||||||
|
type_class = IMAGE_TYPE[conf_type]
|
||||||
transparency = value[CONF_TRANSPARENCY].lower()
|
transparency = value[CONF_TRANSPARENCY].lower()
|
||||||
allow_config = IMAGE_TYPE[type].allow_config
|
if transparency not in type_class.allow_config:
|
||||||
if transparency not in allow_config:
|
|
||||||
raise cv.Invalid(
|
raise cv.Invalid(
|
||||||
f"Image format '{type}' cannot have transparency: {transparency}"
|
f"Image format '{conf_type}' cannot have transparency: {transparency}"
|
||||||
)
|
)
|
||||||
invert_alpha = value.get(CONF_INVERT_ALPHA, False)
|
invert_alpha = value.get(CONF_INVERT_ALPHA, False)
|
||||||
if (
|
if (
|
||||||
invert_alpha
|
invert_alpha
|
||||||
and transparency != CONF_ALPHA_CHANNEL
|
and transparency != CONF_ALPHA_CHANNEL
|
||||||
and CONF_INVERT_ALPHA not in allow_config
|
and CONF_INVERT_ALPHA not in type_class.allow_config
|
||||||
):
|
):
|
||||||
raise cv.Invalid("No alpha channel to invert")
|
raise cv.Invalid("No alpha channel to invert")
|
||||||
|
if value.get(CONF_BYTE_ORDER) is not None and not callable(
|
||||||
|
getattr(type_class, "set_big_endian", None)
|
||||||
|
):
|
||||||
|
raise cv.Invalid(
|
||||||
|
f"Image format '{conf_type}' does not support byte order configuration"
|
||||||
|
)
|
||||||
if file := value.get(CONF_FILE):
|
if file := value.get(CONF_FILE):
|
||||||
file = Path(file)
|
file = Path(file)
|
||||||
if is_svg_file(file):
|
if is_svg_file(file):
|
||||||
@ -456,31 +478,82 @@ def validate_settings(value):
|
|||||||
try:
|
try:
|
||||||
Image.open(file)
|
Image.open(file)
|
||||||
except UnidentifiedImageError as exc:
|
except UnidentifiedImageError as exc:
|
||||||
raise cv.Invalid(f"File can't be opened as image: {file}") from exc
|
raise cv.Invalid(
|
||||||
|
f"File can't be opened as image: {file.absolute()}"
|
||||||
|
) from exc
|
||||||
return value
|
return value
|
||||||
|
|
||||||
|
|
||||||
BASE_SCHEMA = cv.Schema(
|
IMAGE_ID_SCHEMA = {
|
||||||
{
|
|
||||||
cv.Required(CONF_ID): cv.declare_id(Image_),
|
cv.Required(CONF_ID): cv.declare_id(Image_),
|
||||||
cv.Required(CONF_FILE): cv.Any(validate_file_shorthand, TYPED_FILE_SCHEMA),
|
cv.Required(CONF_FILE): cv.Any(validate_file_shorthand, TYPED_FILE_SCHEMA),
|
||||||
|
cv.GenerateID(CONF_RAW_DATA_ID): cv.declare_id(cg.uint8),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
OPTIONS_SCHEMA = {
|
||||||
cv.Optional(CONF_RESIZE): cv.dimensions,
|
cv.Optional(CONF_RESIZE): cv.dimensions,
|
||||||
cv.Optional(CONF_DITHER, default="NONE"): cv.one_of(
|
cv.Optional(CONF_DITHER, default="NONE"): cv.one_of(
|
||||||
"NONE", "FLOYDSTEINBERG", upper=True
|
"NONE", "FLOYDSTEINBERG", upper=True
|
||||||
),
|
),
|
||||||
cv.Optional(CONF_INVERT_ALPHA, default=False): cv.boolean,
|
cv.Optional(CONF_INVERT_ALPHA, default=False): cv.boolean,
|
||||||
cv.GenerateID(CONF_RAW_DATA_ID): cv.declare_id(cg.uint8),
|
cv.Optional(CONF_BYTE_ORDER): cv.one_of("BIG_ENDIAN", "LITTLE_ENDIAN", upper=True),
|
||||||
|
cv.Optional(CONF_TRANSPARENCY, default=CONF_OPAQUE): validate_transparency(),
|
||||||
|
cv.Optional(CONF_TYPE): validate_type(IMAGE_TYPE),
|
||||||
|
}
|
||||||
|
|
||||||
|
OPTIONS = [key.schema for key in OPTIONS_SCHEMA]
|
||||||
|
|
||||||
|
# image schema with no defaults, used with `CONF_IMAGES` in the config
|
||||||
|
IMAGE_SCHEMA_NO_DEFAULTS = {
|
||||||
|
**IMAGE_ID_SCHEMA,
|
||||||
|
**{cv.Optional(key): OPTIONS_SCHEMA[key] for key in OPTIONS},
|
||||||
|
}
|
||||||
|
|
||||||
|
BASE_SCHEMA = cv.Schema(
|
||||||
|
{
|
||||||
|
**IMAGE_ID_SCHEMA,
|
||||||
|
**OPTIONS_SCHEMA,
|
||||||
}
|
}
|
||||||
).add_extra(validate_settings)
|
).add_extra(validate_settings)
|
||||||
|
|
||||||
IMAGE_SCHEMA = BASE_SCHEMA.extend(
|
IMAGE_SCHEMA = BASE_SCHEMA.extend(
|
||||||
{
|
{
|
||||||
cv.Required(CONF_TYPE): validate_type(IMAGE_TYPE),
|
cv.Required(CONF_TYPE): validate_type(IMAGE_TYPE),
|
||||||
cv.Optional(CONF_TRANSPARENCY, default=CONF_OPAQUE): validate_transparency(),
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def validate_defaults(value):
|
||||||
|
"""
|
||||||
|
Validate the options for images with defaults
|
||||||
|
"""
|
||||||
|
defaults = value[CONF_DEFAULTS]
|
||||||
|
result = []
|
||||||
|
for index, image in enumerate(value[CONF_IMAGES]):
|
||||||
|
type = image.get(CONF_TYPE, defaults.get(CONF_TYPE))
|
||||||
|
if type is None:
|
||||||
|
raise cv.Invalid(
|
||||||
|
"Type is required either in the image config or in the defaults",
|
||||||
|
path=[CONF_IMAGES, index],
|
||||||
|
)
|
||||||
|
type_class = IMAGE_TYPE[type]
|
||||||
|
# A default byte order should be simply ignored if the type does not support it
|
||||||
|
available_options = [*OPTIONS]
|
||||||
|
if (
|
||||||
|
not callable(getattr(type_class, "set_big_endian", None))
|
||||||
|
and CONF_BYTE_ORDER not in image
|
||||||
|
):
|
||||||
|
available_options.remove(CONF_BYTE_ORDER)
|
||||||
|
config = {
|
||||||
|
**{key: image.get(key, defaults.get(key)) for key in available_options},
|
||||||
|
**{key.schema: image[key.schema] for key in IMAGE_ID_SCHEMA},
|
||||||
|
}
|
||||||
|
validate_settings(config)
|
||||||
|
result.append(config)
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
def typed_image_schema(image_type):
|
def typed_image_schema(image_type):
|
||||||
"""
|
"""
|
||||||
Construct a schema for a specific image type, allowing transparency options
|
Construct a schema for a specific image type, allowing transparency options
|
||||||
@ -523,10 +596,33 @@ def typed_image_schema(image_type):
|
|||||||
|
|
||||||
# The config schema can be a (possibly empty) single list of images,
|
# The config schema can be a (possibly empty) single list of images,
|
||||||
# or a dictionary of image types each with a list of images
|
# or a dictionary of image types each with a list of images
|
||||||
CONFIG_SCHEMA = cv.Any(
|
# or a dictionary with keys `defaults:` and `images:`
|
||||||
cv.Schema({cv.Optional(t.lower()): typed_image_schema(t) for t in IMAGE_TYPE}),
|
|
||||||
cv.ensure_list(IMAGE_SCHEMA),
|
|
||||||
)
|
def _config_schema(config):
|
||||||
|
if isinstance(config, list):
|
||||||
|
return cv.Schema([IMAGE_SCHEMA])(config)
|
||||||
|
if not isinstance(config, dict):
|
||||||
|
raise cv.Invalid(
|
||||||
|
"Badly formed image configuration, expected a list or a dictionary"
|
||||||
|
)
|
||||||
|
if CONF_DEFAULTS in config or CONF_IMAGES in config:
|
||||||
|
return validate_defaults(
|
||||||
|
cv.Schema(
|
||||||
|
{
|
||||||
|
cv.Required(CONF_DEFAULTS): OPTIONS_SCHEMA,
|
||||||
|
cv.Required(CONF_IMAGES): cv.ensure_list(IMAGE_SCHEMA_NO_DEFAULTS),
|
||||||
|
}
|
||||||
|
)(config)
|
||||||
|
)
|
||||||
|
if CONF_ID in config or CONF_FILE in config:
|
||||||
|
return cv.ensure_list(IMAGE_SCHEMA)([config])
|
||||||
|
return cv.Schema(
|
||||||
|
{cv.Optional(t.lower()): typed_image_schema(t) for t in IMAGE_TYPE}
|
||||||
|
)(config)
|
||||||
|
|
||||||
|
|
||||||
|
CONFIG_SCHEMA = _config_schema
|
||||||
|
|
||||||
|
|
||||||
async def write_image(config, all_frames=False):
|
async def write_image(config, all_frames=False):
|
||||||
@ -585,6 +681,9 @@ async def write_image(config, all_frames=False):
|
|||||||
|
|
||||||
total_rows = height * frame_count
|
total_rows = height * frame_count
|
||||||
encoder = IMAGE_TYPE[type](width, total_rows, transparency, dither, invert_alpha)
|
encoder = IMAGE_TYPE[type](width, total_rows, transparency, dither, invert_alpha)
|
||||||
|
if byte_order := config.get(CONF_BYTE_ORDER):
|
||||||
|
# Check for valid type has already been done in validate_settings
|
||||||
|
encoder.set_big_endian(byte_order == "BIG_ENDIAN")
|
||||||
for frame_index in range(frame_count):
|
for frame_index in range(frame_count):
|
||||||
image.seek(frame_index)
|
image.seek(frame_index)
|
||||||
pixels = encoder.convert(image.resize((width, height)), path).getdata()
|
pixels = encoder.convert(image.resize((width, height)), path).getdata()
|
||||||
|
@ -1,29 +1,71 @@
|
|||||||
"""Fixtures for component tests."""
|
"""Fixtures for component tests."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from collections.abc import Callable, Generator
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
# Add package root to python path
|
# Add package root to python path
|
||||||
here = Path(__file__).parent
|
here = Path(__file__).parent
|
||||||
package_root = here.parent.parent
|
package_root = here.parent.parent
|
||||||
sys.path.insert(0, package_root.as_posix())
|
sys.path.insert(0, package_root.as_posix())
|
||||||
|
|
||||||
import pytest # noqa: E402
|
|
||||||
|
|
||||||
from esphome.__main__ import generate_cpp_contents # noqa: E402
|
from esphome.__main__ import generate_cpp_contents # noqa: E402
|
||||||
from esphome.config import read_config # noqa: E402
|
from esphome.config import read_config # noqa: E402
|
||||||
from esphome.core import CORE # noqa: E402
|
from esphome.core import CORE # noqa: E402
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(autouse=True)
|
||||||
|
def config_path(request: pytest.FixtureRequest) -> Generator[None]:
|
||||||
|
"""Set CORE.config_path to the component's config directory and reset it after the test."""
|
||||||
|
original_path = CORE.config_path
|
||||||
|
config_dir = Path(request.fspath).parent / "config"
|
||||||
|
|
||||||
|
# Check if config directory exists, if not use parent directory
|
||||||
|
if config_dir.exists():
|
||||||
|
# Set config_path to a dummy yaml file in the config directory
|
||||||
|
# This ensures CORE.config_dir points to the config directory
|
||||||
|
CORE.config_path = str(config_dir / "dummy.yaml")
|
||||||
|
else:
|
||||||
|
CORE.config_path = str(Path(request.fspath).parent / "dummy.yaml")
|
||||||
|
|
||||||
|
yield
|
||||||
|
CORE.config_path = original_path
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def generate_main():
|
def component_fixture_path(request: pytest.FixtureRequest) -> Callable[[str], Path]:
|
||||||
|
"""Return a function to get absolute paths relative to the component's fixtures directory."""
|
||||||
|
|
||||||
|
def _get_path(file_name: str) -> Path:
|
||||||
|
"""Get the absolute path of a file relative to the component's fixtures directory."""
|
||||||
|
return (Path(request.fspath).parent / "fixtures" / file_name).absolute()
|
||||||
|
|
||||||
|
return _get_path
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def component_config_path(request: pytest.FixtureRequest) -> Callable[[str], Path]:
|
||||||
|
"""Return a function to get absolute paths relative to the component's config directory."""
|
||||||
|
|
||||||
|
def _get_path(file_name: str) -> Path:
|
||||||
|
"""Get the absolute path of a file relative to the component's config directory."""
|
||||||
|
return (Path(request.fspath).parent / "config" / file_name).absolute()
|
||||||
|
|
||||||
|
return _get_path
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def generate_main() -> Generator[Callable[[str | Path], str]]:
|
||||||
"""Generates the C++ main.cpp file and returns it in string form."""
|
"""Generates the C++ main.cpp file and returns it in string form."""
|
||||||
|
|
||||||
def generator(path: str) -> str:
|
def generator(path: str | Path) -> str:
|
||||||
CORE.config_path = path
|
CORE.config_path = str(path)
|
||||||
CORE.config = read_config({})
|
CORE.config = read_config({})
|
||||||
generate_cpp_contents(CORE.config)
|
generate_cpp_contents(CORE.config)
|
||||||
print(CORE.cpp_main_section)
|
|
||||||
return CORE.cpp_main_section
|
return CORE.cpp_main_section
|
||||||
|
|
||||||
yield generator
|
yield generator
|
||||||
|
0
tests/component_tests/image/config/bad.png
Normal file
0
tests/component_tests/image/config/bad.png
Normal file
BIN
tests/component_tests/image/config/image.png
Normal file
BIN
tests/component_tests/image/config/image.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 685 B |
20
tests/component_tests/image/config/image_test.yaml
Normal file
20
tests/component_tests/image/config/image_test.yaml
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
esphome:
|
||||||
|
name: test
|
||||||
|
|
||||||
|
esp32:
|
||||||
|
board: esp32s3box
|
||||||
|
|
||||||
|
image:
|
||||||
|
- file: image.png
|
||||||
|
byte_order: little_endian
|
||||||
|
id: cat_img
|
||||||
|
type: rgb565
|
||||||
|
|
||||||
|
spi:
|
||||||
|
mosi_pin: 6
|
||||||
|
clk_pin: 7
|
||||||
|
|
||||||
|
display:
|
||||||
|
- platform: mipi_spi
|
||||||
|
id: lcd_display
|
||||||
|
model: s3box
|
183
tests/component_tests/image/test_init.py
Normal file
183
tests/component_tests/image/test_init.py
Normal file
@ -0,0 +1,183 @@
|
|||||||
|
"""Tests for image configuration validation."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from collections.abc import Callable
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from esphome import config_validation as cv
|
||||||
|
from esphome.components.image import CONFIG_SCHEMA
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("config", "error_match"),
|
||||||
|
[
|
||||||
|
pytest.param(
|
||||||
|
"a string",
|
||||||
|
"Badly formed image configuration, expected a list or a dictionary",
|
||||||
|
id="invalid_string_config",
|
||||||
|
),
|
||||||
|
pytest.param(
|
||||||
|
{"id": "image_id", "type": "rgb565"},
|
||||||
|
r"required key not provided @ data\[0\]\['file'\]",
|
||||||
|
id="missing_file",
|
||||||
|
),
|
||||||
|
pytest.param(
|
||||||
|
{"file": "image.png", "type": "rgb565"},
|
||||||
|
r"required key not provided @ data\[0\]\['id'\]",
|
||||||
|
id="missing_id",
|
||||||
|
),
|
||||||
|
pytest.param(
|
||||||
|
{"id": "mdi_id", "file": "mdi:weather-##", "type": "rgb565"},
|
||||||
|
"Could not parse mdi icon name",
|
||||||
|
id="invalid_mdi_icon",
|
||||||
|
),
|
||||||
|
pytest.param(
|
||||||
|
{
|
||||||
|
"id": "image_id",
|
||||||
|
"file": "image.png",
|
||||||
|
"type": "binary",
|
||||||
|
"transparency": "alpha_channel",
|
||||||
|
},
|
||||||
|
"Image format 'BINARY' cannot have transparency",
|
||||||
|
id="binary_with_transparency",
|
||||||
|
),
|
||||||
|
pytest.param(
|
||||||
|
{
|
||||||
|
"id": "image_id",
|
||||||
|
"file": "image.png",
|
||||||
|
"type": "rgb565",
|
||||||
|
"transparency": "chroma_key",
|
||||||
|
"invert_alpha": True,
|
||||||
|
},
|
||||||
|
"No alpha channel to invert",
|
||||||
|
id="invert_alpha_without_alpha_channel",
|
||||||
|
),
|
||||||
|
pytest.param(
|
||||||
|
{
|
||||||
|
"id": "image_id",
|
||||||
|
"file": "image.png",
|
||||||
|
"type": "binary",
|
||||||
|
"byte_order": "big_endian",
|
||||||
|
},
|
||||||
|
"Image format 'BINARY' does not support byte order configuration",
|
||||||
|
id="binary_with_byte_order",
|
||||||
|
),
|
||||||
|
pytest.param(
|
||||||
|
{"id": "image_id", "file": "bad.png", "type": "binary"},
|
||||||
|
"File can't be opened as image",
|
||||||
|
id="invalid_image_file",
|
||||||
|
),
|
||||||
|
pytest.param(
|
||||||
|
{"defaults": {}, "images": [{"id": "image_id", "file": "image.png"}]},
|
||||||
|
"Type is required either in the image config or in the defaults",
|
||||||
|
id="missing_type_in_defaults",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_image_configuration_errors(
|
||||||
|
config: Any,
|
||||||
|
error_match: str,
|
||||||
|
) -> None:
|
||||||
|
"""Test detection of invalid configuration."""
|
||||||
|
with pytest.raises(cv.Invalid, match=error_match):
|
||||||
|
CONFIG_SCHEMA(config)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"config",
|
||||||
|
[
|
||||||
|
pytest.param(
|
||||||
|
{
|
||||||
|
"id": "image_id",
|
||||||
|
"file": "image.png",
|
||||||
|
"type": "rgb565",
|
||||||
|
"transparency": "chroma_key",
|
||||||
|
"byte_order": "little_endian",
|
||||||
|
"dither": "FloydSteinberg",
|
||||||
|
"resize": "100x100",
|
||||||
|
"invert_alpha": False,
|
||||||
|
},
|
||||||
|
id="single_image_all_options",
|
||||||
|
),
|
||||||
|
pytest.param(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"id": "image_id",
|
||||||
|
"file": "image.png",
|
||||||
|
"type": "binary",
|
||||||
|
}
|
||||||
|
],
|
||||||
|
id="list_of_images",
|
||||||
|
),
|
||||||
|
pytest.param(
|
||||||
|
{
|
||||||
|
"defaults": {
|
||||||
|
"type": "rgb565",
|
||||||
|
"transparency": "chroma_key",
|
||||||
|
"byte_order": "little_endian",
|
||||||
|
"dither": "FloydSteinberg",
|
||||||
|
"resize": "100x100",
|
||||||
|
"invert_alpha": False,
|
||||||
|
},
|
||||||
|
"images": [
|
||||||
|
{
|
||||||
|
"id": "image_id",
|
||||||
|
"file": "image.png",
|
||||||
|
}
|
||||||
|
],
|
||||||
|
},
|
||||||
|
id="images_with_defaults",
|
||||||
|
),
|
||||||
|
pytest.param(
|
||||||
|
{
|
||||||
|
"rgb565": {
|
||||||
|
"alpha_channel": [
|
||||||
|
{
|
||||||
|
"id": "image_id",
|
||||||
|
"file": "image.png",
|
||||||
|
"transparency": "alpha_channel",
|
||||||
|
"byte_order": "little_endian",
|
||||||
|
"dither": "FloydSteinberg",
|
||||||
|
"resize": "100x100",
|
||||||
|
"invert_alpha": False,
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"binary": [
|
||||||
|
{
|
||||||
|
"id": "image_id",
|
||||||
|
"file": "image.png",
|
||||||
|
"transparency": "opaque",
|
||||||
|
"dither": "FloydSteinberg",
|
||||||
|
"resize": "100x100",
|
||||||
|
"invert_alpha": False,
|
||||||
|
}
|
||||||
|
],
|
||||||
|
},
|
||||||
|
id="type_based_organization",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_image_configuration_success(
|
||||||
|
config: dict[str, Any] | list[dict[str, Any]],
|
||||||
|
) -> None:
|
||||||
|
"""Test successful configuration validation."""
|
||||||
|
CONFIG_SCHEMA(config)
|
||||||
|
|
||||||
|
|
||||||
|
def test_image_generation(
|
||||||
|
generate_main: Callable[[str | Path], str],
|
||||||
|
component_config_path: Callable[[str], Path],
|
||||||
|
) -> None:
|
||||||
|
"""Test image generation configuration."""
|
||||||
|
|
||||||
|
main_cpp = generate_main(component_config_path("image_test.yaml"))
|
||||||
|
assert "uint8_t_id[] PROGMEM = {0x24, 0x21, 0x24, 0x21" in main_cpp
|
||||||
|
assert (
|
||||||
|
"cat_img = new image::Image(uint8_t_id, 32, 24, image::IMAGE_TYPE_RGB565, image::TRANSPARENCY_OPAQUE);"
|
||||||
|
in main_cpp
|
||||||
|
)
|
@ -1,17 +0,0 @@
|
|||||||
spi:
|
|
||||||
- id: spi_main_lcd
|
|
||||||
clk_pin: 16
|
|
||||||
mosi_pin: 17
|
|
||||||
miso_pin: 32
|
|
||||||
|
|
||||||
display:
|
|
||||||
- platform: ili9xxx
|
|
||||||
id: main_lcd
|
|
||||||
model: ili9342
|
|
||||||
cs_pin: 14
|
|
||||||
dc_pin: 13
|
|
||||||
reset_pin: 21
|
|
||||||
invert_colors: true
|
|
||||||
|
|
||||||
<<: !include common.yaml
|
|
||||||
|
|
@ -1,16 +0,0 @@
|
|||||||
spi:
|
|
||||||
- id: spi_main_lcd
|
|
||||||
clk_pin: 6
|
|
||||||
mosi_pin: 7
|
|
||||||
miso_pin: 5
|
|
||||||
|
|
||||||
display:
|
|
||||||
- platform: ili9xxx
|
|
||||||
id: main_lcd
|
|
||||||
model: ili9342
|
|
||||||
cs_pin: 3
|
|
||||||
dc_pin: 11
|
|
||||||
reset_pin: 10
|
|
||||||
invert_colors: true
|
|
||||||
|
|
||||||
<<: !include common.yaml
|
|
@ -1,16 +0,0 @@
|
|||||||
spi:
|
|
||||||
- id: spi_main_lcd
|
|
||||||
clk_pin: 6
|
|
||||||
mosi_pin: 7
|
|
||||||
miso_pin: 5
|
|
||||||
|
|
||||||
display:
|
|
||||||
- platform: ili9xxx
|
|
||||||
id: main_lcd
|
|
||||||
model: ili9342
|
|
||||||
cs_pin: 3
|
|
||||||
dc_pin: 11
|
|
||||||
reset_pin: 10
|
|
||||||
invert_colors: true
|
|
||||||
|
|
||||||
<<: !include common.yaml
|
|
@ -13,4 +13,13 @@ display:
|
|||||||
reset_pin: 16
|
reset_pin: 16
|
||||||
invert_colors: true
|
invert_colors: true
|
||||||
|
|
||||||
<<: !include common.yaml
|
image:
|
||||||
|
defaults:
|
||||||
|
type: rgb565
|
||||||
|
transparency: opaque
|
||||||
|
byte_order: little_endian
|
||||||
|
resize: 50x50
|
||||||
|
dither: FloydSteinberg
|
||||||
|
images:
|
||||||
|
- id: test_image
|
||||||
|
file: ../../pnglogo.png
|
||||||
|
Loading…
x
Reference in New Issue
Block a user