From 47c68c8aef9d657564fee40298d418e5a5f76d5f Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Tue, 20 Aug 2024 12:56:45 +1200 Subject: [PATCH] Add ``file`` component --- CODEOWNERS | 1 + esphome/components/file/__init__.py | 148 ++++++++++++++++++++++ tests/components/file/bloop.wav | Bin 0 -> 3854 bytes tests/components/file/test.esp32-idf.yaml | 7 + 4 files changed, 156 insertions(+) create mode 100644 esphome/components/file/__init__.py create mode 100644 tests/components/file/bloop.wav create mode 100644 tests/components/file/test.esp32-idf.yaml diff --git a/CODEOWNERS b/CODEOWNERS index 9159f5f843..a06f0620c6 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -139,6 +139,7 @@ esphome/components/ezo_pmp/* @carlos-sarmiento esphome/components/factory_reset/* @anatoly-savchenkov esphome/components/fastled_base/* @OttoWinter esphome/components/feedback/* @ianchi +esphome/components/file/* @jesserockz esphome/components/fingerprint_grow/* @OnFreund @alexborro @loongyh esphome/components/font/* @clydebarrow @esphome/core esphome/components/fs3000/* @kahrendt diff --git a/esphome/components/file/__init__.py b/esphome/components/file/__init__.py new file mode 100644 index 0000000000..b31fa94a2f --- /dev/null +++ b/esphome/components/file/__init__.py @@ -0,0 +1,148 @@ +import hashlib +import logging +from pathlib import Path + +from magic import Magic + +from esphome import external_files +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.const import ( + CONF_FILE, + CONF_FORMAT, + CONF_ID, + CONF_PATH, + CONF_TYPE, + CONF_URL, +) +from esphome.core import CORE, HexInt +from esphome.external_files import download_content + +_LOGGER = logging.getLogger(__name__) + + +CODEOWNERS = ["@jesserockz"] +DOMAIN = "file" +MULTI_CONF = True + +TYPE_LOCAL = "local" +TYPE_WEB = "web" + +FORMAT_RAW = "raw" +FORMAT_WAV = "wav" + +FORMATS = [FORMAT_RAW, FORMAT_WAV] + + +def _compute_local_file_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_file_path: base_dir=%s", base_dir / key) + return base_dir / key + + +def _download_web_file(value: dict) -> dict: + url = value[CONF_URL] + path = _compute_local_file_path(value) + + download_content(url, path) + _LOGGER.debug("download_web_file: path=%s", path) + return value + + +LOCAL_SCHEMA = cv.Schema( + { + cv.Required(CONF_PATH): cv.file_, + } +) + +WEB_SCHEMA = cv.All( + { + cv.Required(CONF_URL): cv.url, + }, + _download_web_file, +) + + +def _validate_file_shorthand(value): + value = cv.string_strict(value) + if value.startswith("http://") or value.startswith("https://"): + return _file_schema( + { + CONF_TYPE: TYPE_WEB, + CONF_URL: value, + } + ) + return _file_schema( + { + CONF_TYPE: TYPE_LOCAL, + CONF_PATH: value, + } + ) + + +TYPED_FILE_SCHEMA = cv.typed_schema( + { + TYPE_LOCAL: LOCAL_SCHEMA, + TYPE_WEB: WEB_SCHEMA, + }, +) + + +def _file_schema(value): + if isinstance(value, str): + return _validate_file_shorthand(value) + return TYPED_FILE_SCHEMA(value) + + +CONFIG_SCHEMA = cv.Schema( + { + cv.Required(CONF_ID): cv.declare_id(cg.uint8), + cv.Required(CONF_FILE): _file_schema, + cv.Optional(CONF_FORMAT): cv.one_of(*FORMATS, lower=True), + } +) + + +def _trim_wav_file(data: bytes) -> bytes: + header = [] + index = 0 + length = len(data) + while index < length: + byte = data[index : index + 1] + if byte == b"": + raise ValueError("Could not find data in wav file") + header.append(byte) + index += 1 + if header[-4:] == [b"d", b"a", b"t", b"a"] or index > 100: + break + index += 2 + return data[index:] + + +async def to_code(config: dict) -> None: + conf_file: dict = config[CONF_FILE] + file_source = conf_file[CONF_TYPE] + if file_source == TYPE_LOCAL: + path = CORE.relative_config_path(conf_file[CONF_PATH]) + elif file_source == TYPE_WEB: + path = _compute_local_file_path(conf_file) + + with open(path, "rb") as f: + data = f.read() + + # Get format from config or fallback to magic + if (format := config.get(CONF_FORMAT)) is None: + magic = Magic(mime=True) + file_type = magic.from_buffer(data) + if "wav" in file_type: + format = FORMAT_WAV + + if format == FORMAT_WAV: + data = _trim_wav_file(data) + + rhs = [HexInt(x) for x in data] + cg.progmem_array(config[CONF_ID], rhs) diff --git a/tests/components/file/bloop.wav b/tests/components/file/bloop.wav new file mode 100644 index 0000000000000000000000000000000000000000..85bdb2f783d43b89c804b2f57859c4b23748d484 GIT binary patch literal 3854 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 If~{u&05Uy^AOHXW literal 0 HcmV?d00001 diff --git a/tests/components/file/test.esp32-idf.yaml b/tests/components/file/test.esp32-idf.yaml new file mode 100644 index 0000000000..a663e12d46 --- /dev/null +++ b/tests/components/file/test.esp32-idf.yaml @@ -0,0 +1,7 @@ +file: + - id: bloop_local + file: ../../components/file/bloop.wav + - id: bloop_web + # TODO: Change to ESPHome URL after file is merged + # file: https://github.com/esphome/esphome/raw/dev/tests/components/file/bloop.wav + file: https://github.com/jesserockz/esphome-components/raw/main/tests/file/bloop.wav