1
0
mirror of https://github.com/esphome/esphome.git synced 2025-09-20 04:02:21 +01:00

Allow images to be downloaded from URLs (#5214)

Co-authored-by: guillempages <guillempages@users.noreply.github.com>
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
This commit is contained in:
Landon Rohatensky
2023-11-23 11:10:33 -08:00
committed by GitHub
parent 3ac59180ab
commit 49c09afb87
5 changed files with 184 additions and 26 deletions

View File

@@ -1,15 +1,23 @@
from __future__ import annotations
import logging
import hashlib
import io
from pathlib import Path
import re
import requests
from magic import Magic
from PIL import Image
from esphome import core
from esphome.components import font
from esphome import external_files
import esphome.config_validation as cv
import esphome.codegen as cg
from esphome.const import (
__version__,
CONF_DITHER,
CONF_FILE,
CONF_ICON,
@@ -19,6 +27,7 @@ from esphome.const import (
CONF_RESIZE,
CONF_SOURCE,
CONF_TYPE,
CONF_URL,
)
from esphome.core import CORE, HexInt
@@ -43,34 +52,74 @@ IMAGE_TYPE = {
CONF_USE_TRANSPARENCY = "use_transparency"
# If the MDI file cannot be downloaded within this time, abort.
MDI_DOWNLOAD_TIMEOUT = 30 # seconds
IMAGE_DOWNLOAD_TIMEOUT = 30 # seconds
SOURCE_LOCAL = "local"
SOURCE_MDI = "mdi"
SOURCE_WEB = "web"
Image_ = image_ns.class_("Image")
def _compute_local_icon_path(value) -> Path:
base_dir = Path(CORE.data_dir) / DOMAIN / "mdi"
def _compute_local_icon_path(value: dict) -> Path:
base_dir = external_files.compute_local_file_dir(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)
def _compute_local_image_path(value: dict) -> Path:
url = value[CONF_URL]
h = hashlib.new("sha256")
h.update(url.encode())
key = h.hexdigest()[:8]
base_dir = external_files.compute_local_file_dir(DOMAIN)
return base_dir / key
def download_content(url: str, path: Path) -> None:
if not external_files.has_remote_file_changed(url, path):
_LOGGER.debug("Remote file has not changed %s", url)
return
_LOGGER.debug(
"Remote file has changed, downloading from %s to %s",
url,
path,
)
try:
req = requests.get(url, timeout=MDI_DOWNLOAD_TIMEOUT)
req = requests.get(
url,
timeout=IMAGE_DOWNLOAD_TIMEOUT,
headers={"User-agent": f"ESPHome/{__version__} (https://esphome.io)"},
)
req.raise_for_status()
except requests.exceptions.RequestException as e:
raise cv.Invalid(f"Could not download MDI image {mdi_id} from {url}: {e}")
raise cv.Invalid(f"Could not download from {url}: {e}")
path.parent.mkdir(parents=True, exist_ok=True)
path.write_bytes(req.content)
def download_mdi(value):
validate_cairosvg_installed(value)
mdi_id = value[CONF_ICON]
path = _compute_local_icon_path(value)
url = f"https://raw.githubusercontent.com/Templarian/MaterialDesign/master/svg/{mdi_id}.svg"
download_content(url, path)
return value
def download_image(value):
url = value[CONF_URL]
path = _compute_local_image_path(value)
download_content(url, path)
return value
@@ -139,6 +188,13 @@ def validate_file_shorthand(value):
CONF_ICON: icon,
}
)
if value.startswith("http://") or value.startswith("https://"):
return FILE_SCHEMA(
{
CONF_SOURCE: SOURCE_WEB,
CONF_URL: value,
}
)
return FILE_SCHEMA(
{
CONF_SOURCE: SOURCE_LOCAL,
@@ -160,10 +216,18 @@ MDI_SCHEMA = cv.All(
download_mdi,
)
WEB_SCHEMA = cv.All(
{
cv.Required(CONF_URL): cv.string,
},
download_image,
)
TYPED_FILE_SCHEMA = cv.typed_schema(
{
SOURCE_LOCAL: LOCAL_SCHEMA,
SOURCE_MDI: MDI_SCHEMA,
SOURCE_WEB: WEB_SCHEMA,
},
key=CONF_SOURCE,
)
@@ -201,9 +265,7 @@ IMAGE_SCHEMA = cv.Schema(
CONFIG_SCHEMA = cv.All(font.validate_pillow_installed, IMAGE_SCHEMA)
def load_svg_image(file: str, resize: tuple[int, int]):
from PIL import Image
def load_svg_image(file: bytes, resize: tuple[int, int]):
# This import is only needed in case of SVG images; adding it
# to the top would force configurations not using SVG to also have it
# installed for no reason.
@@ -212,19 +274,17 @@ def load_svg_image(file: str, resize: tuple[int, int]):
if resize:
req_width, req_height = resize
svg_image = svg2png(
url=file,
file,
output_width=req_width,
output_height=req_height,
)
else:
svg_image = svg2png(url=file)
svg_image = svg2png(file)
return Image.open(io.BytesIO(svg_image))
async def to_code(config):
from PIL import Image
conf_file = config[CONF_FILE]
if conf_file[CONF_SOURCE] == SOURCE_LOCAL:
@@ -233,17 +293,26 @@ async def to_code(config):
elif conf_file[CONF_SOURCE] == SOURCE_MDI:
path = _compute_local_icon_path(conf_file).as_posix()
elif conf_file[CONF_SOURCE] == SOURCE_WEB:
path = _compute_local_image_path(conf_file).as_posix()
try:
resize = config.get(CONF_RESIZE)
if path.lower().endswith(".svg"):
image = load_svg_image(path, resize)
else:
image = Image.open(path)
if resize:
image.thumbnail(resize)
with open(path, "rb") as f:
file_contents = f.read()
except Exception as e:
raise core.EsphomeError(f"Could not load image file {path}: {e}")
mime = Magic(mime=True)
file_type = mime.from_buffer(file_contents)
resize = config.get(CONF_RESIZE)
if "svg" in file_type:
image = load_svg_image(file_contents, resize)
else:
image = Image.open(io.BytesIO(file_contents))
if resize:
image.thumbnail(resize)
width, height = image.size
if CONF_RESIZE not in config and (width > 500 or height > 500):