mirror of
https://github.com/esphome/esphome.git
synced 2026-06-02 03:02:19 +08:00
[image] Add define and core data (#13058)
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import contextlib
|
import contextlib
|
||||||
|
from dataclasses import dataclass
|
||||||
import hashlib
|
import hashlib
|
||||||
import io
|
import io
|
||||||
import logging
|
import logging
|
||||||
@@ -37,11 +38,21 @@ image_ns = cg.esphome_ns.namespace("image")
|
|||||||
|
|
||||||
ImageType = image_ns.enum("ImageType")
|
ImageType = image_ns.enum("ImageType")
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class ImageMetaData:
|
||||||
|
width: int
|
||||||
|
height: int
|
||||||
|
image_type: str
|
||||||
|
transparency: str
|
||||||
|
|
||||||
|
|
||||||
CONF_OPAQUE = "opaque"
|
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"
|
CONF_IMAGES = "images"
|
||||||
|
KEY_METADATA = "metadata"
|
||||||
|
|
||||||
TRANSPARENCY_TYPES = (
|
TRANSPARENCY_TYPES = (
|
||||||
CONF_OPAQUE,
|
CONF_OPAQUE,
|
||||||
@@ -723,10 +734,38 @@ async def write_image(config, all_frames=False):
|
|||||||
return prog_arr, width, height, image_type, trans_value, frame_count
|
return prog_arr, width, height, image_type, trans_value, frame_count
|
||||||
|
|
||||||
|
|
||||||
|
async def _image_to_code(entry):
|
||||||
|
"""
|
||||||
|
Convert a single image entry to code and return its metadata.
|
||||||
|
:param entry: The config entry for the image.
|
||||||
|
:return: An ImageMetaData object
|
||||||
|
"""
|
||||||
|
prog_arr, width, height, image_type, trans_value, _ = await write_image(entry)
|
||||||
|
cg.new_Pvariable(entry[CONF_ID], prog_arr, width, height, image_type, trans_value)
|
||||||
|
return ImageMetaData(
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
entry[CONF_TYPE],
|
||||||
|
entry[CONF_TRANSPARENCY],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
async def to_code(config):
|
async def to_code(config):
|
||||||
# By now the config should be a simple list.
|
cg.add_define("USE_IMAGE")
|
||||||
for entry in config:
|
# By now the config will be a simple list.
|
||||||
prog_arr, width, height, image_type, trans_value, _ = await write_image(entry)
|
# Use a subkey to allow for other data in the future
|
||||||
cg.new_Pvariable(
|
CORE.data[DOMAIN] = {
|
||||||
entry[CONF_ID], prog_arr, width, height, image_type, trans_value
|
KEY_METADATA: {
|
||||||
)
|
entry[CONF_ID].id: await _image_to_code(entry) for entry in config
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def get_all_image_metadata() -> dict[str, ImageMetaData]:
|
||||||
|
"""Get all image metadata."""
|
||||||
|
return CORE.data.get(DOMAIN, {}).get(KEY_METADATA, {})
|
||||||
|
|
||||||
|
|
||||||
|
def get_image_metadata(image_id: str) -> ImageMetaData | None:
|
||||||
|
"""Get image metadata by ID for use by other components."""
|
||||||
|
return get_all_image_metadata().get(image_id)
|
||||||
|
|||||||
@@ -9,8 +9,14 @@ from typing import Any
|
|||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from esphome import config_validation as cv
|
from esphome import config_validation as cv
|
||||||
from esphome.components.image import CONF_TRANSPARENCY, CONFIG_SCHEMA
|
from esphome.components.image import (
|
||||||
|
CONF_TRANSPARENCY,
|
||||||
|
CONFIG_SCHEMA,
|
||||||
|
get_all_image_metadata,
|
||||||
|
get_image_metadata,
|
||||||
|
)
|
||||||
from esphome.const import CONF_ID, CONF_RAW_DATA_ID, CONF_TYPE
|
from esphome.const import CONF_ID, CONF_RAW_DATA_ID, CONF_TYPE
|
||||||
|
from esphome.core import CORE
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
@@ -235,3 +241,112 @@ def test_image_generation(
|
|||||||
"cat_img = new image::Image(uint8_t_id, 32, 24, image::IMAGE_TYPE_RGB565, image::TRANSPARENCY_OPAQUE);"
|
"cat_img = new image::Image(uint8_t_id, 32, 24, image::IMAGE_TYPE_RGB565, image::TRANSPARENCY_OPAQUE);"
|
||||||
in main_cpp
|
in main_cpp
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_image_to_code_defines_and_core_data(
|
||||||
|
generate_main: Callable[[str | Path], str],
|
||||||
|
component_config_path: Callable[[str], Path],
|
||||||
|
) -> None:
|
||||||
|
"""Test that to_code() sets USE_IMAGE define and stores image metadata."""
|
||||||
|
# Generate the main cpp which will call to_code
|
||||||
|
generate_main(component_config_path("image_test.yaml"))
|
||||||
|
|
||||||
|
# Verify USE_IMAGE define was added
|
||||||
|
assert any(d.name == "USE_IMAGE" for d in CORE.defines), (
|
||||||
|
"USE_IMAGE define should be set when images are configured"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Use the public API to get image metadata
|
||||||
|
# The test config has an image with id 'cat_img'
|
||||||
|
cat_img_metadata = get_image_metadata("cat_img")
|
||||||
|
|
||||||
|
assert cat_img_metadata is not None, (
|
||||||
|
"Image metadata should be retrievable via get_image_metadata()"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Verify the metadata has the expected attributes
|
||||||
|
assert hasattr(cat_img_metadata, "width"), "Metadata should have width attribute"
|
||||||
|
assert hasattr(cat_img_metadata, "height"), "Metadata should have height attribute"
|
||||||
|
assert hasattr(cat_img_metadata, "image_type"), (
|
||||||
|
"Metadata should have image_type attribute"
|
||||||
|
)
|
||||||
|
assert hasattr(cat_img_metadata, "transparency"), (
|
||||||
|
"Metadata should have transparency attribute"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Verify the values are correct (from the test image)
|
||||||
|
assert cat_img_metadata.width == 32, "Width should be 32"
|
||||||
|
assert cat_img_metadata.height == 24, "Height should be 24"
|
||||||
|
assert cat_img_metadata.image_type == "RGB565", "Type should be RGB565"
|
||||||
|
assert cat_img_metadata.transparency == "opaque", "Transparency should be opaque"
|
||||||
|
|
||||||
|
|
||||||
|
def test_image_to_code_multiple_images(
|
||||||
|
generate_main: Callable[[str | Path], str],
|
||||||
|
component_config_path: Callable[[str], Path],
|
||||||
|
) -> None:
|
||||||
|
"""Test that to_code() stores metadata for multiple images."""
|
||||||
|
generate_main(component_config_path("image_test.yaml"))
|
||||||
|
|
||||||
|
# Use the public API to get all image metadata
|
||||||
|
all_metadata = get_all_image_metadata()
|
||||||
|
|
||||||
|
assert isinstance(all_metadata, dict), (
|
||||||
|
"get_all_image_metadata() should return a dictionary"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Verify that at least one image is present
|
||||||
|
assert len(all_metadata) > 0, "Should have at least one image metadata entry"
|
||||||
|
|
||||||
|
# Each image ID should map to an ImageMetaData object
|
||||||
|
for image_id, metadata in all_metadata.items():
|
||||||
|
assert isinstance(image_id, str), "Image IDs should be strings"
|
||||||
|
|
||||||
|
# Verify it's an ImageMetaData object with all required attributes
|
||||||
|
assert hasattr(metadata, "width"), (
|
||||||
|
f"Metadata for '{image_id}' should have width"
|
||||||
|
)
|
||||||
|
assert hasattr(metadata, "height"), (
|
||||||
|
f"Metadata for '{image_id}' should have height"
|
||||||
|
)
|
||||||
|
assert hasattr(metadata, "image_type"), (
|
||||||
|
f"Metadata for '{image_id}' should have image_type"
|
||||||
|
)
|
||||||
|
assert hasattr(metadata, "transparency"), (
|
||||||
|
f"Metadata for '{image_id}' should have transparency"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Verify values are valid
|
||||||
|
assert isinstance(metadata.width, int), (
|
||||||
|
f"Width for '{image_id}' should be an integer"
|
||||||
|
)
|
||||||
|
assert isinstance(metadata.height, int), (
|
||||||
|
f"Height for '{image_id}' should be an integer"
|
||||||
|
)
|
||||||
|
assert isinstance(metadata.image_type, str), (
|
||||||
|
f"Type for '{image_id}' should be a string"
|
||||||
|
)
|
||||||
|
assert isinstance(metadata.transparency, str), (
|
||||||
|
f"Transparency for '{image_id}' should be a string"
|
||||||
|
)
|
||||||
|
assert metadata.width > 0, f"Width for '{image_id}' should be positive"
|
||||||
|
assert metadata.height > 0, f"Height for '{image_id}' should be positive"
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_image_metadata_nonexistent() -> None:
|
||||||
|
"""Test that get_image_metadata returns None for non-existent image IDs."""
|
||||||
|
# This should return None when no images are configured or ID doesn't exist
|
||||||
|
metadata = get_image_metadata("nonexistent_image_id")
|
||||||
|
assert metadata is None, (
|
||||||
|
"get_image_metadata should return None for non-existent IDs"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_all_image_metadata_empty() -> None:
|
||||||
|
"""Test that get_all_image_metadata returns empty dict when no images configured."""
|
||||||
|
# When CORE hasn't been initialized with images, should return empty dict
|
||||||
|
all_metadata = get_all_image_metadata()
|
||||||
|
assert isinstance(all_metadata, dict), (
|
||||||
|
"get_all_image_metadata should always return a dict"
|
||||||
|
)
|
||||||
|
# Length could be 0 or more depending on what's in CORE at test time
|
||||||
|
|||||||
Reference in New Issue
Block a user