mirror of
				https://github.com/esphome/esphome.git
				synced 2025-10-30 22:53:59 +00:00 
			
		
		
		
	download font from url on build (#5254)
Co-authored-by: guillempages <guillempages@users.noreply.github.com> Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Co-authored-by: clydebarrow <2366188+clydebarrow@users.noreply.github.com>
This commit is contained in:
		
				
					committed by
					
						 GitHub
						GitHub
					
				
			
			
				
	
			
			
			
						parent
						
							d3a028f7fa
						
					
				
				
					commit
					2df9c30446
				
			| @@ -1,13 +1,15 @@ | |||||||
|  | import hashlib | ||||||
|  | import logging | ||||||
|  |  | ||||||
| import functools | import functools | ||||||
| from pathlib import Path | from pathlib import Path | ||||||
| import hashlib |  | ||||||
| import os | import os | ||||||
| import re | import re | ||||||
| from packaging import version | from packaging import version | ||||||
|  |  | ||||||
| import requests | import requests | ||||||
|  |  | ||||||
| from esphome import core | from esphome import core | ||||||
|  | from esphome import external_files | ||||||
| import esphome.config_validation as cv | import esphome.config_validation as cv | ||||||
| import esphome.codegen as cg | import esphome.codegen as cg | ||||||
| from esphome.helpers import ( | from esphome.helpers import ( | ||||||
| @@ -15,21 +17,26 @@ from esphome.helpers import ( | |||||||
|     cpp_string_escape, |     cpp_string_escape, | ||||||
| ) | ) | ||||||
| from esphome.const import ( | from esphome.const import ( | ||||||
|  |     __version__, | ||||||
|     CONF_FAMILY, |     CONF_FAMILY, | ||||||
|     CONF_FILE, |     CONF_FILE, | ||||||
|     CONF_GLYPHS, |     CONF_GLYPHS, | ||||||
|     CONF_ID, |     CONF_ID, | ||||||
|     CONF_RAW_DATA_ID, |     CONF_RAW_DATA_ID, | ||||||
|     CONF_TYPE, |     CONF_TYPE, | ||||||
|  |     CONF_REFRESH, | ||||||
|     CONF_SIZE, |     CONF_SIZE, | ||||||
|     CONF_PATH, |     CONF_PATH, | ||||||
|     CONF_WEIGHT, |     CONF_WEIGHT, | ||||||
|  |     CONF_URL, | ||||||
| ) | ) | ||||||
| from esphome.core import ( | from esphome.core import ( | ||||||
|     CORE, |     CORE, | ||||||
|     HexInt, |     HexInt, | ||||||
| ) | ) | ||||||
|  |  | ||||||
|  | _LOGGER = logging.getLogger(__name__) | ||||||
|  |  | ||||||
| DOMAIN = "font" | DOMAIN = "font" | ||||||
| DEPENDENCIES = ["display"] | DEPENDENCIES = ["display"] | ||||||
| MULTI_CONF = True | MULTI_CONF = True | ||||||
| @@ -125,20 +132,10 @@ def validate_truetype_file(value): | |||||||
|     return cv.file_(value) |     return cv.file_(value) | ||||||
|  |  | ||||||
|  |  | ||||||
| def _compute_local_font_dir(name) -> Path: |  | ||||||
|     h = hashlib.new("sha256") |  | ||||||
|     h.update(name.encode()) |  | ||||||
|     return Path(CORE.data_dir) / DOMAIN / h.hexdigest()[:8] |  | ||||||
|  |  | ||||||
|  |  | ||||||
| def _compute_gfonts_local_path(value) -> Path: |  | ||||||
|     name = f"{value[CONF_FAMILY]}@{value[CONF_WEIGHT]}@{value[CONF_ITALIC]}@v1" |  | ||||||
|     return _compute_local_font_dir(name) / "font.ttf" |  | ||||||
|  |  | ||||||
|  |  | ||||||
| TYPE_LOCAL = "local" | TYPE_LOCAL = "local" | ||||||
| TYPE_LOCAL_BITMAP = "local_bitmap" | TYPE_LOCAL_BITMAP = "local_bitmap" | ||||||
| TYPE_GFONTS = "gfonts" | TYPE_GFONTS = "gfonts" | ||||||
|  | TYPE_WEB = "web" | ||||||
| LOCAL_SCHEMA = cv.Schema( | LOCAL_SCHEMA = cv.Schema( | ||||||
|     { |     { | ||||||
|         cv.Required(CONF_PATH): validate_truetype_file, |         cv.Required(CONF_PATH): validate_truetype_file, | ||||||
| @@ -169,21 +166,64 @@ def validate_weight_name(value): | |||||||
|     return FONT_WEIGHTS[cv.one_of(*FONT_WEIGHTS, lower=True, space="-")(value)] |     return FONT_WEIGHTS[cv.one_of(*FONT_WEIGHTS, lower=True, space="-")(value)] | ||||||
|  |  | ||||||
|  |  | ||||||
| def download_gfonts(value): | def _compute_local_font_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) | ||||||
|  |     _LOGGER.debug("_compute_local_font_path: base_dir=%s", base_dir / key) | ||||||
|  |     return base_dir / key | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def get_font_path(value, type) -> Path: | ||||||
|  |     if type == TYPE_GFONTS: | ||||||
|  |         name = f"{value[CONF_FAMILY]}@{value[CONF_WEIGHT]}@{value[CONF_ITALIC]}@v1" | ||||||
|  |         return external_files.compute_local_file_dir(DOMAIN) / f"{name}.ttf" | ||||||
|  |     if type == TYPE_WEB: | ||||||
|  |         return _compute_local_font_path(value) / "font.ttf" | ||||||
|  |     return None | ||||||
|  |  | ||||||
|  |  | ||||||
|  | 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=external_files.NETWORK_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 from {url}: {e}") | ||||||
|  |  | ||||||
|  |     path.parent.mkdir(parents=True, exist_ok=True) | ||||||
|  |     path.write_bytes(req.content) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def download_gfont(value): | ||||||
|     name = ( |     name = ( | ||||||
|         f"{value[CONF_FAMILY]}:ital,wght@{int(value[CONF_ITALIC])},{value[CONF_WEIGHT]}" |         f"{value[CONF_FAMILY]}:ital,wght@{int(value[CONF_ITALIC])},{value[CONF_WEIGHT]}" | ||||||
|     ) |     ) | ||||||
|     url = f"https://fonts.googleapis.com/css2?family={name}" |     url = f"https://fonts.googleapis.com/css2?family={name}" | ||||||
|  |     path = get_font_path(value, TYPE_GFONTS) | ||||||
|  |     _LOGGER.debug("download_gfont: path=%s", path) | ||||||
|  |  | ||||||
|     path = _compute_gfonts_local_path(value) |  | ||||||
|     if path.is_file(): |  | ||||||
|         return value |  | ||||||
|     try: |     try: | ||||||
|         req = requests.get(url, timeout=30) |         req = requests.get(url, timeout=external_files.NETWORK_TIMEOUT) | ||||||
|         req.raise_for_status() |         req.raise_for_status() | ||||||
|     except requests.exceptions.RequestException as e: |     except requests.exceptions.RequestException as e: | ||||||
|         raise cv.Invalid( |         raise cv.Invalid( | ||||||
|             f"Could not download font for {name}, please check the fonts exists " |             f"Could not download font at {url}, please check the fonts exists " | ||||||
|             f"at google fonts ({e})" |             f"at google fonts ({e})" | ||||||
|         ) |         ) | ||||||
|     match = re.search(r"src:\s+url\((.+)\)\s+format\('truetype'\);", req.text) |     match = re.search(r"src:\s+url\((.+)\)\s+format\('truetype'\);", req.text) | ||||||
| @@ -194,26 +234,48 @@ def download_gfonts(value): | |||||||
|         ) |         ) | ||||||
|  |  | ||||||
|     ttf_url = match.group(1) |     ttf_url = match.group(1) | ||||||
|     try: |     _LOGGER.debug("download_gfont: ttf_url=%s", ttf_url) | ||||||
|         req = requests.get(ttf_url, timeout=30) |  | ||||||
|         req.raise_for_status() |  | ||||||
|     except requests.exceptions.RequestException as e: |  | ||||||
|         raise cv.Invalid(f"Could not download ttf file for {name} ({ttf_url}): {e}") |  | ||||||
|  |  | ||||||
|     path.parent.mkdir(exist_ok=True, parents=True) |     download_content(ttf_url, path) | ||||||
|     path.write_bytes(req.content) |  | ||||||
|     return value |     return value | ||||||
|  |  | ||||||
|  |  | ||||||
| GFONTS_SCHEMA = cv.All( | def download_web_font(value): | ||||||
|  |     url = value[CONF_URL] | ||||||
|  |     path = get_font_path(value, TYPE_WEB) | ||||||
|  |  | ||||||
|  |     download_content(url, path) | ||||||
|  |     _LOGGER.debug("download_web_font: path=%s", path) | ||||||
|  |     return value | ||||||
|  |  | ||||||
|  |  | ||||||
|  | EXTERNAL_FONT_SCHEMA = cv.Schema( | ||||||
|     { |     { | ||||||
|         cv.Required(CONF_FAMILY): cv.string_strict, |  | ||||||
|         cv.Optional(CONF_WEIGHT, default="regular"): cv.Any( |         cv.Optional(CONF_WEIGHT, default="regular"): cv.Any( | ||||||
|             cv.int_, validate_weight_name |             cv.int_, validate_weight_name | ||||||
|         ), |         ), | ||||||
|         cv.Optional(CONF_ITALIC, default=False): cv.boolean, |         cv.Optional(CONF_ITALIC, default=False): cv.boolean, | ||||||
|     }, |         cv.Optional(CONF_REFRESH, default="1d"): cv.All(cv.string, cv.source_refresh), | ||||||
|     download_gfonts, |     } | ||||||
|  | ) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | GFONTS_SCHEMA = cv.All( | ||||||
|  |     EXTERNAL_FONT_SCHEMA.extend( | ||||||
|  |         { | ||||||
|  |             cv.Required(CONF_FAMILY): cv.string_strict, | ||||||
|  |         } | ||||||
|  |     ), | ||||||
|  |     download_gfont, | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | WEB_FONT_SCHEMA = cv.All( | ||||||
|  |     EXTERNAL_FONT_SCHEMA.extend( | ||||||
|  |         { | ||||||
|  |             cv.Required(CONF_URL): cv.string_strict, | ||||||
|  |         } | ||||||
|  |     ), | ||||||
|  |     download_web_font, | ||||||
| ) | ) | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -233,6 +295,14 @@ def validate_file_shorthand(value): | |||||||
|             data[CONF_WEIGHT] = weight[1:] |             data[CONF_WEIGHT] = weight[1:] | ||||||
|         return FILE_SCHEMA(data) |         return FILE_SCHEMA(data) | ||||||
|  |  | ||||||
|  |     if value.startswith("http://") or value.startswith("https://"): | ||||||
|  |         return FILE_SCHEMA( | ||||||
|  |             { | ||||||
|  |                 CONF_TYPE: TYPE_WEB, | ||||||
|  |                 CONF_URL: value, | ||||||
|  |             } | ||||||
|  |         ) | ||||||
|  |  | ||||||
|     if value.endswith(".pcf") or value.endswith(".bdf"): |     if value.endswith(".pcf") or value.endswith(".bdf"): | ||||||
|         return FILE_SCHEMA( |         return FILE_SCHEMA( | ||||||
|             { |             { | ||||||
| @@ -254,6 +324,7 @@ TYPED_FILE_SCHEMA = cv.typed_schema( | |||||||
|         TYPE_LOCAL: LOCAL_SCHEMA, |         TYPE_LOCAL: LOCAL_SCHEMA, | ||||||
|         TYPE_GFONTS: GFONTS_SCHEMA, |         TYPE_GFONTS: GFONTS_SCHEMA, | ||||||
|         TYPE_LOCAL_BITMAP: LOCAL_BITMAP_SCHEMA, |         TYPE_LOCAL_BITMAP: LOCAL_BITMAP_SCHEMA, | ||||||
|  |         TYPE_WEB: WEB_FONT_SCHEMA, | ||||||
|     } |     } | ||||||
| ) | ) | ||||||
|  |  | ||||||
| @@ -264,7 +335,7 @@ def _file_schema(value): | |||||||
|     return TYPED_FILE_SCHEMA(value) |     return TYPED_FILE_SCHEMA(value) | ||||||
|  |  | ||||||
|  |  | ||||||
| FILE_SCHEMA = cv.Schema(_file_schema) | FILE_SCHEMA = cv.All(_file_schema) | ||||||
|  |  | ||||||
| DEFAULT_GLYPHS = ( | DEFAULT_GLYPHS = ( | ||||||
|     ' !"%()+=,-.:/?0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz°' |     ' !"%()+=,-.:/?0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz°' | ||||||
| @@ -288,7 +359,7 @@ FONT_SCHEMA = cv.Schema( | |||||||
|         ), |         ), | ||||||
|         cv.GenerateID(CONF_RAW_DATA_ID): cv.declare_id(cg.uint8), |         cv.GenerateID(CONF_RAW_DATA_ID): cv.declare_id(cg.uint8), | ||||||
|         cv.GenerateID(CONF_RAW_GLYPH_ID): cv.declare_id(GlyphData), |         cv.GenerateID(CONF_RAW_GLYPH_ID): cv.declare_id(GlyphData), | ||||||
|     } |     }, | ||||||
| ) | ) | ||||||
|  |  | ||||||
| CONFIG_SCHEMA = cv.All(validate_pillow_installed, FONT_SCHEMA, merge_glyphs) | CONFIG_SCHEMA = cv.All(validate_pillow_installed, FONT_SCHEMA, merge_glyphs) | ||||||
| @@ -343,8 +414,8 @@ class EFont: | |||||||
|         elif ftype == TYPE_LOCAL: |         elif ftype == TYPE_LOCAL: | ||||||
|             path = CORE.relative_config_path(file[CONF_PATH]) |             path = CORE.relative_config_path(file[CONF_PATH]) | ||||||
|             font = load_ttf_font(path, size) |             font = load_ttf_font(path, size) | ||||||
|         elif ftype == TYPE_GFONTS: |         elif ftype in (TYPE_GFONTS, TYPE_WEB): | ||||||
|             path = _compute_gfonts_local_path(file) |             path = get_font_path(file, ftype) | ||||||
|             font = load_ttf_font(path, size) |             font = load_ttf_font(path, size) | ||||||
|         else: |         else: | ||||||
|             raise cv.Invalid(f"Could not load font: unknown type: {ftype}") |             raise cv.Invalid(f"Could not load font: unknown type: {ftype}") | ||||||
| @@ -361,9 +432,9 @@ def convert_bitmap_to_pillow_font(filepath): | |||||||
|         BdfFontFile, |         BdfFontFile, | ||||||
|     ) |     ) | ||||||
|  |  | ||||||
|     local_bitmap_font_file = _compute_local_font_dir(filepath) / os.path.basename( |     local_bitmap_font_file = external_files.compute_local_file_dir( | ||||||
|         filepath |         DOMAIN, | ||||||
|     ) |     ) / os.path.basename(filepath) | ||||||
|  |  | ||||||
|     copy_file_if_changed(filepath, local_bitmap_font_file) |     copy_file_if_changed(filepath, local_bitmap_font_file) | ||||||
|  |  | ||||||
|   | |||||||
| @@ -33,7 +33,9 @@ def has_remote_file_changed(url, local_file_path): | |||||||
|                 IF_MODIFIED_SINCE: local_modification_time_str, |                 IF_MODIFIED_SINCE: local_modification_time_str, | ||||||
|                 CACHE_CONTROL: CACHE_CONTROL_MAX_AGE + "3600", |                 CACHE_CONTROL: CACHE_CONTROL_MAX_AGE + "3600", | ||||||
|             } |             } | ||||||
|             response = requests.head(url, headers=headers, timeout=NETWORK_TIMEOUT) |             response = requests.head( | ||||||
|  |                 url, headers=headers, timeout=NETWORK_TIMEOUT, allow_redirects=True | ||||||
|  |             ) | ||||||
|  |  | ||||||
|             _LOGGER.debug( |             _LOGGER.debug( | ||||||
|                 "has_remote_file_changed: File %s, Local modified %s, response code %d", |                 "has_remote_file_changed: File %s, Local modified %s, response code %d", | ||||||
|   | |||||||
| @@ -6,6 +6,17 @@ font: | |||||||
|     extras: |     extras: | ||||||
|       - file: "gfonts://Roboto" |       - file: "gfonts://Roboto" | ||||||
|         glyphs: ["\u00C4", "\u00C5", "\U000000C7"] |         glyphs: ["\u00C4", "\u00C5", "\U000000C7"] | ||||||
|  |   - file: "gfonts://Roboto" | ||||||
|  |     id: roboto_web | ||||||
|  |     size: 20 | ||||||
|  |   - file: "https://github.com/IdreesInc/Monocraft/releases/download/v3.0/Monocraft.ttf" | ||||||
|  |     id: monocraft | ||||||
|  |     size: 20 | ||||||
|  |   - file: | ||||||
|  |       type: web | ||||||
|  |       url: "https://github.com/IdreesInc/Monocraft/releases/download/v3.0/Monocraft.ttf" | ||||||
|  |     id: monocraft2 | ||||||
|  |     size: 24 | ||||||
|  |  | ||||||
| spi: | spi: | ||||||
|   clk_pin: 14 |   clk_pin: 14 | ||||||
|   | |||||||
| @@ -812,11 +812,6 @@ image: | |||||||
|     file: mdi:alert-outline |     file: mdi:alert-outline | ||||||
|     type: BINARY |     type: BINARY | ||||||
|  |  | ||||||
| font: |  | ||||||
|   - file: "gfonts://Roboto" |  | ||||||
|     id: roboto |  | ||||||
|     size: 20 |  | ||||||
|  |  | ||||||
| graph: | graph: | ||||||
|   - id: my_graph |   - id: my_graph | ||||||
|     sensor: ha_hello_world_temperature |     sensor: ha_hello_world_temperature | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user