Add support for mdi images (#4654)

This commit is contained in:
guillempages 2023-06-06 23:32:21 +02:00 committed by GitHub
parent aeb94e166b
commit 6b00622329
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 169 additions and 17 deletions

View file

@ -29,6 +29,8 @@ RUN \
git=1:2.30.2-1+deb11u2 \ git=1:2.30.2-1+deb11u2 \
curl=7.74.0-1.3+deb11u7 \ curl=7.74.0-1.3+deb11u7 \
openssh-client=1:8.4p1-5+deb11u1 \ openssh-client=1:8.4p1-5+deb11u1 \
libcairo2=1.16.0-5 \
python3-cffi=1.14.5-1 \
&& rm -rf \ && rm -rf \
/tmp/* \ /tmp/* \
/var/{cache,log}/* \ /var/{cache,log}/* \

View file

@ -1,5 +1,10 @@
import logging import logging
import io
from pathlib import Path
import re
import requests
from esphome import core from esphome import core
from esphome.components import display, font from esphome.components import display, font
import esphome.config_validation as cv import esphome.config_validation as cv
@ -7,15 +12,19 @@ import esphome.codegen as cg
from esphome.const import ( from esphome.const import (
CONF_DITHER, CONF_DITHER,
CONF_FILE, CONF_FILE,
CONF_ICON,
CONF_ID, CONF_ID,
CONF_PATH,
CONF_RAW_DATA_ID, CONF_RAW_DATA_ID,
CONF_RESIZE, CONF_RESIZE,
CONF_SOURCE,
CONF_TYPE, CONF_TYPE,
) )
from esphome.core import CORE, HexInt from esphome.core import CORE, HexInt
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
DOMAIN = "image"
DEPENDENCIES = ["display"] DEPENDENCIES = ["display"]
MULTI_CONF = True MULTI_CONF = True
@ -31,9 +40,58 @@ IMAGE_TYPE = {
CONF_USE_TRANSPARENCY = "use_transparency" CONF_USE_TRANSPARENCY = "use_transparency"
# If the MDI file cannot be downloaded within this time, abort.
MDI_DOWNLOAD_TIMEOUT = 30 # seconds
SOURCE_LOCAL = "local"
SOURCE_MDI = "mdi"
Image_ = display.display_ns.class_("Image") Image_ = display.display_ns.class_("Image")
def _compute_local_icon_path(value) -> Path:
base_dir = Path(CORE.config_dir) / ".esphome" / DOMAIN / "mdi"
return base_dir / f"{value[CONF_ICON]}.svg"
def download_mdi(value):
mdi_id = value[CONF_ICON]
path = _compute_local_icon_path(value)
if path.is_file():
return value
url = f"https://raw.githubusercontent.com/Templarian/MaterialDesign/master/svg/{mdi_id}.svg"
_LOGGER.debug("Downloading %s MDI image from %s", mdi_id, url)
try:
req = requests.get(url, timeout=MDI_DOWNLOAD_TIMEOUT)
req.raise_for_status()
except requests.exceptions.RequestException as e:
raise cv.Invalid(f"Could not download MDI image {mdi_id} from {url}: {e}")
path.parent.mkdir(parents=True, exist_ok=True)
path.write_bytes(req.content)
return value
def validate_cairosvg_installed(value):
"""Validate that cairosvg is installed"""
try:
import cairosvg
except ImportError as err:
raise cv.Invalid(
"Please install the cairosvg python package to use this feature. "
"(pip install cairosvg)"
) from err
major, minor, _ = cairosvg.__version__.split(".")
if major < "2" or major == "2" and minor < "2":
raise cv.Invalid(
"Please update your cairosvg installation to at least 2.2.0. "
"(pip install -U cairosvg)"
)
return value
def validate_cross_dependencies(config): def validate_cross_dependencies(config):
""" """
Validate fields whose possible values depend on other fields. Validate fields whose possible values depend on other fields.
@ -41,6 +99,13 @@ def validate_cross_dependencies(config):
have "use_transparency" set to True. have "use_transparency" set to True.
Also set the default value for those kind of dependent fields. Also set the default value for those kind of dependent fields.
""" """
is_mdi = CONF_FILE in config and config[CONF_FILE][CONF_SOURCE] == SOURCE_MDI
if CONF_TYPE not in config:
if is_mdi:
config[CONF_TYPE] = "TRANSPARENT_BINARY"
else:
config[CONF_TYPE] = "BINARY"
image_type = config[CONF_TYPE] image_type = config[CONF_TYPE]
is_transparent_type = image_type in ["TRANSPARENT_BINARY", "RGBA"] is_transparent_type = image_type in ["TRANSPARENT_BINARY", "RGBA"]
@ -51,16 +116,74 @@ def validate_cross_dependencies(config):
if is_transparent_type and not config[CONF_USE_TRANSPARENCY]: if is_transparent_type and not config[CONF_USE_TRANSPARENCY]:
raise cv.Invalid(f"Image type {image_type} must always be transparent.") raise cv.Invalid(f"Image type {image_type} must always be transparent.")
if is_mdi and config[CONF_TYPE] not in ["BINARY", "TRANSPARENT_BINARY"]:
raise cv.Invalid("MDI images must be binary images.")
return config return config
def validate_file_shorthand(value):
value = cv.string_strict(value)
if value.startswith("mdi:"):
validate_cairosvg_installed(value)
match = re.search(r"mdi:([a-zA-Z0-9\-]+)", value)
if match is None:
raise cv.Invalid("Could not parse mdi icon name.")
icon = match.group(1)
return FILE_SCHEMA(
{
CONF_SOURCE: SOURCE_MDI,
CONF_ICON: icon,
}
)
return FILE_SCHEMA(
{
CONF_SOURCE: SOURCE_LOCAL,
CONF_PATH: value,
}
)
LOCAL_SCHEMA = cv.Schema(
{
cv.Required(CONF_PATH): cv.file_,
}
)
MDI_SCHEMA = cv.All(
{
cv.Required(CONF_ICON): cv.string,
},
download_mdi,
)
TYPED_FILE_SCHEMA = cv.typed_schema(
{
SOURCE_LOCAL: LOCAL_SCHEMA,
SOURCE_MDI: MDI_SCHEMA,
},
key=CONF_SOURCE,
)
def _file_schema(value):
if isinstance(value, str):
return validate_file_shorthand(value)
return TYPED_FILE_SCHEMA(value)
FILE_SCHEMA = cv.Schema(_file_schema)
IMAGE_SCHEMA = cv.Schema( IMAGE_SCHEMA = cv.Schema(
cv.All( cv.All(
{ {
cv.Required(CONF_ID): cv.declare_id(Image_), cv.Required(CONF_ID): cv.declare_id(Image_),
cv.Required(CONF_FILE): cv.file_, cv.Required(CONF_FILE): FILE_SCHEMA,
cv.Optional(CONF_RESIZE): cv.dimensions, cv.Optional(CONF_RESIZE): cv.dimensions,
cv.Optional(CONF_TYPE, default="BINARY"): cv.enum(IMAGE_TYPE, upper=True), # Not setting default here on purpose; the default depends on the source type
# (file or mdi), and will be set in the "validate_cross_dependencies" validator.
cv.Optional(CONF_TYPE): cv.enum(IMAGE_TYPE, upper=True),
# Not setting default here on purpose; the default depends on the image type, # Not setting default here on purpose; the default depends on the image type,
# and thus will be set in the "validate_cross_dependencies" validator. # and thus will be set in the "validate_cross_dependencies" validator.
cv.Optional(CONF_USE_TRANSPARENCY): cv.boolean, cv.Optional(CONF_USE_TRANSPARENCY): cv.boolean,
@ -79,24 +202,43 @@ CONFIG_SCHEMA = cv.All(font.validate_pillow_installed, IMAGE_SCHEMA)
async def to_code(config): async def to_code(config):
from PIL import Image from PIL import Image
path = CORE.relative_config_path(config[CONF_FILE]) conf_file = config[CONF_FILE]
try:
image = Image.open(path) if conf_file[CONF_SOURCE] == SOURCE_LOCAL:
except Exception as e: path = CORE.relative_config_path(conf_file[CONF_PATH])
raise core.EsphomeError(f"Could not load image file {path}: {e}") try:
image = Image.open(path)
except Exception as e:
raise core.EsphomeError(f"Could not load image file {path}: {e}")
if CONF_RESIZE in config:
image.thumbnail(config[CONF_RESIZE])
elif conf_file[CONF_SOURCE] == SOURCE_MDI:
# Those imports are only needed in case of MDI images; adding them
# to the top would force configurations not using MDI to also have them
# installed for no reason.
from cairosvg import svg2png
svg_file = _compute_local_icon_path(conf_file)
if CONF_RESIZE in config:
req_width, req_height = config[CONF_RESIZE]
svg_image = svg2png(
url=svg_file.as_posix(),
output_width=req_width,
output_height=req_height,
)
else:
svg_image = svg2png(url=svg_file.as_posix())
image = Image.open(io.BytesIO(svg_image))
width, height = image.size width, height = image.size
if CONF_RESIZE in config: if CONF_RESIZE not in config and (width > 500 or height > 500):
image.thumbnail(config[CONF_RESIZE]) _LOGGER.warning(
width, height = image.size 'The image "%s" you requested is very big. Please consider'
else: " using the resize parameter.",
if width > 500 or height > 500: path,
_LOGGER.warning( )
'The image "%s" you requested is very big. Please consider'
" using the resize parameter.",
path,
)
transparent = config[CONF_USE_TRANSPARENCY] transparent = config[CONF_USE_TRANSPARENCY]

View file

@ -1,2 +1,3 @@
pillow>4.0.0 pillow>4.0.0
cairosvg>=2.2.0
cryptography>=2.0.0,<4 cryptography>=2.0.0,<4

View file

@ -680,6 +680,13 @@ image:
type: RGB565 type: RGB565
use_transparency: no use_transparency: no
- id: mdi_alert
file: mdi:alert-circle-outline
resize: 50x50
- id: another_alert_icon
file: mdi:alert-outline
type: BINARY
cap1188: cap1188:
id: cap1188_component id: cap1188_component
address: 0x29 address: 0x29