mirror of
https://github.com/esphome/esphome.git
synced 2026-02-10 09:42:01 +00:00
Compare commits
24 Commits
esphome_bu
...
api-dedup-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
39bde3eb73 | ||
|
|
c53baf70c7 | ||
|
|
041c43fb32 | ||
|
|
b4741ade0d | ||
|
|
2c3a92db97 | ||
|
|
fc91a4d7a3 | ||
|
|
9bf90eff01 | ||
|
|
dcbb020479 | ||
|
|
87ac263264 | ||
|
|
097901e9c8 | ||
|
|
01a90074ba | ||
|
|
57b85a8400 | ||
|
|
2edfcf278f | ||
|
|
bcd4a9fc39 | ||
|
|
78df8be31f | ||
|
|
dacc557a16 | ||
|
|
3767c5ec91 | ||
|
|
7c1327f96a | ||
|
|
475db750e0 | ||
|
|
8f74b027b4 | ||
|
|
b2b9e0cb0a | ||
|
|
dbf202bf0d | ||
|
|
b6fdd29953 | ||
|
|
00256e3ca0 |
@@ -965,38 +965,6 @@ def command_clean(args: ArgsProtocol, config: ConfigType) -> int | None:
|
||||
return 0
|
||||
|
||||
|
||||
def command_bundle(args: ArgsProtocol, config: ConfigType) -> int | None:
|
||||
from esphome.bundle import BUNDLE_EXTENSION, ConfigBundleCreator
|
||||
|
||||
creator = ConfigBundleCreator(config)
|
||||
|
||||
if args.list_only:
|
||||
files = creator.discover_files()
|
||||
for bf in sorted(files, key=lambda f: f.path):
|
||||
safe_print(f" {bf.path}")
|
||||
_LOGGER.info("Found %d files", len(files))
|
||||
return 0
|
||||
|
||||
result = creator.create_bundle()
|
||||
|
||||
if args.output:
|
||||
output_path = Path(args.output)
|
||||
else:
|
||||
stem = CORE.config_path.stem
|
||||
output_path = CORE.config_dir / f"{stem}{BUNDLE_EXTENSION}"
|
||||
|
||||
output_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
output_path.write_bytes(result.data)
|
||||
|
||||
_LOGGER.info(
|
||||
"Bundle created: %s (%d files, %.1f KB)",
|
||||
output_path,
|
||||
len(result.files),
|
||||
len(result.data) / 1024,
|
||||
)
|
||||
return 0
|
||||
|
||||
|
||||
def command_dashboard(args: ArgsProtocol) -> int | None:
|
||||
from esphome.dashboard import dashboard
|
||||
|
||||
@@ -1274,7 +1242,6 @@ POST_CONFIG_ACTIONS = {
|
||||
"rename": command_rename,
|
||||
"discover": command_discover,
|
||||
"analyze-memory": command_analyze_memory,
|
||||
"bundle": command_bundle,
|
||||
}
|
||||
|
||||
SIMPLE_CONFIG_ACTIONS = [
|
||||
@@ -1578,24 +1545,6 @@ def parse_args(argv):
|
||||
"configuration", help="Your YAML configuration file(s).", nargs="+"
|
||||
)
|
||||
|
||||
parser_bundle = subparsers.add_parser(
|
||||
"bundle",
|
||||
help="Create a self-contained config bundle for remote compilation.",
|
||||
)
|
||||
parser_bundle.add_argument(
|
||||
"configuration", help="Your YAML configuration file(s).", nargs="+"
|
||||
)
|
||||
parser_bundle.add_argument(
|
||||
"-o",
|
||||
"--output",
|
||||
help="Output path for the bundle archive.",
|
||||
)
|
||||
parser_bundle.add_argument(
|
||||
"--list-only",
|
||||
help="List discovered files without creating the archive.",
|
||||
action="store_true",
|
||||
)
|
||||
|
||||
# Keep backward compatibility with the old command line format of
|
||||
# esphome <config> <command>.
|
||||
#
|
||||
@@ -1674,16 +1623,6 @@ def run_esphome(argv):
|
||||
_LOGGER.warning("Skipping secrets file %s", conf_path)
|
||||
return 0
|
||||
|
||||
# Bundle support: if the configuration is a .esphomebundle, extract it
|
||||
# and rewrite conf_path to the extracted YAML config.
|
||||
from esphome.bundle import is_bundle_path, prepare_bundle_for_compile
|
||||
|
||||
if is_bundle_path(conf_path):
|
||||
_LOGGER.info("Extracting config bundle %s...", conf_path)
|
||||
conf_path = prepare_bundle_for_compile(conf_path)
|
||||
# Update the argument so downstream code sees the extracted path
|
||||
args.configuration[0] = str(conf_path)
|
||||
|
||||
CORE.config_path = conf_path
|
||||
CORE.dashboard = args.dashboard
|
||||
|
||||
|
||||
@@ -1,699 +0,0 @@
|
||||
"""Config bundle creator and extractor for ESPHome.
|
||||
|
||||
A bundle is a self-contained .tar.gz archive containing a YAML config
|
||||
and every local file it depends on. Bundles can be created from a config
|
||||
and compiled directly: ``esphome compile my_device.esphomebundle.tar.gz``
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from enum import StrEnum
|
||||
import io
|
||||
import json
|
||||
import logging
|
||||
from pathlib import Path
|
||||
import re
|
||||
import shutil
|
||||
import tarfile
|
||||
from typing import Any
|
||||
|
||||
from esphome import const, yaml_util
|
||||
from esphome.const import (
|
||||
CONF_ESPHOME,
|
||||
CONF_EXTERNAL_COMPONENTS,
|
||||
CONF_INCLUDES,
|
||||
CONF_INCLUDES_C,
|
||||
CONF_PATH,
|
||||
CONF_SOURCE,
|
||||
CONF_TYPE,
|
||||
)
|
||||
from esphome.core import CORE, EsphomeError
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
BUNDLE_EXTENSION = ".esphomebundle.tar.gz"
|
||||
MANIFEST_FILENAME = "manifest.json"
|
||||
CURRENT_MANIFEST_VERSION = 1
|
||||
MAX_DECOMPRESSED_SIZE = 500 * 1024 * 1024 # 500 MB
|
||||
MAX_MANIFEST_SIZE = 1024 * 1024 # 1 MB
|
||||
|
||||
# Directories preserved across bundle extractions (build caches)
|
||||
_PRESERVE_DIRS = (".esphome", ".pioenvs", ".pio")
|
||||
_BUNDLE_STAGING_DIR = ".bundle_staging"
|
||||
|
||||
|
||||
class ManifestKey(StrEnum):
|
||||
"""Keys used in bundle manifest.json."""
|
||||
|
||||
MANIFEST_VERSION = "manifest_version"
|
||||
ESPHOME_VERSION = "esphome_version"
|
||||
CONFIG_FILENAME = "config_filename"
|
||||
FILES = "files"
|
||||
HAS_SECRETS = "has_secrets"
|
||||
|
||||
|
||||
# String prefixes that are never local file paths
|
||||
_NON_PATH_PREFIXES = ("http://", "https://", "ftp://", "mdi:", "<")
|
||||
|
||||
# File extensions recognized when resolving relative path strings.
|
||||
# A relative string with one of these extensions is resolved against the
|
||||
# config directory and included if the file exists.
|
||||
_KNOWN_FILE_EXTENSIONS = frozenset(
|
||||
{
|
||||
# Fonts
|
||||
".ttf",
|
||||
".otf",
|
||||
".woff",
|
||||
".woff2",
|
||||
".pcf",
|
||||
".bdf",
|
||||
# Images
|
||||
".png",
|
||||
".jpg",
|
||||
".jpeg",
|
||||
".bmp",
|
||||
".gif",
|
||||
".svg",
|
||||
".ico",
|
||||
".webp",
|
||||
# Certificates
|
||||
".pem",
|
||||
".crt",
|
||||
".key",
|
||||
".der",
|
||||
".p12",
|
||||
".pfx",
|
||||
# C/C++ includes
|
||||
".h",
|
||||
".hpp",
|
||||
".c",
|
||||
".cpp",
|
||||
".ino",
|
||||
# Web assets
|
||||
".css",
|
||||
".js",
|
||||
".html",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
# Matches !secret references in YAML text. This is intentionally a simple
|
||||
# regex scan rather than a YAML parse — it may match inside comments or
|
||||
# multi-line strings, which is the conservative direction (include more
|
||||
# secrets rather than fewer).
|
||||
_SECRET_RE = re.compile(r"!secret\s+(\S+)")
|
||||
|
||||
|
||||
def _find_used_secret_keys(yaml_files: list[Path]) -> set[str]:
|
||||
"""Scan YAML files for ``!secret <key>`` references."""
|
||||
keys: set[str] = set()
|
||||
for fpath in yaml_files:
|
||||
try:
|
||||
text = fpath.read_text(encoding="utf-8")
|
||||
except (OSError, UnicodeDecodeError):
|
||||
continue
|
||||
for match in _SECRET_RE.finditer(text):
|
||||
keys.add(match.group(1))
|
||||
return keys
|
||||
|
||||
|
||||
@dataclass
|
||||
class BundleFile:
|
||||
"""A file to include in the bundle."""
|
||||
|
||||
path: str # Relative path inside the archive
|
||||
source: Path # Absolute path on disk
|
||||
|
||||
|
||||
@dataclass
|
||||
class BundleResult:
|
||||
"""Result of creating a bundle."""
|
||||
|
||||
data: bytes
|
||||
manifest: dict[str, Any]
|
||||
files: list[BundleFile]
|
||||
|
||||
|
||||
@dataclass
|
||||
class BundleManifest:
|
||||
"""Parsed and validated bundle manifest."""
|
||||
|
||||
manifest_version: int
|
||||
esphome_version: str
|
||||
config_filename: str
|
||||
files: list[str]
|
||||
has_secrets: bool
|
||||
|
||||
|
||||
class ConfigBundleCreator:
|
||||
"""Creates a self-contained bundle from an ESPHome config."""
|
||||
|
||||
def __init__(self, config: dict[str, Any]) -> None:
|
||||
self._config = config
|
||||
self._config_dir = CORE.config_dir
|
||||
self._config_path = CORE.config_path
|
||||
self._files: list[BundleFile] = []
|
||||
self._seen_paths: set[Path] = set()
|
||||
self._secrets_paths: set[Path] = set()
|
||||
|
||||
def discover_files(self) -> list[BundleFile]:
|
||||
"""Discover all files needed for the bundle."""
|
||||
self._files = []
|
||||
self._seen_paths = set()
|
||||
self._secrets_paths = set()
|
||||
|
||||
# The main config file
|
||||
self._add_file(self._config_path)
|
||||
|
||||
# Phase 1: YAML includes (tracked during config loading)
|
||||
self._discover_yaml_includes()
|
||||
|
||||
# Phase 2: Component-referenced files from validated config
|
||||
self._discover_component_files()
|
||||
|
||||
return list(self._files)
|
||||
|
||||
def create_bundle(self) -> BundleResult:
|
||||
"""Create the bundle archive."""
|
||||
files = self.discover_files()
|
||||
|
||||
# Determine which secret keys are actually referenced by the
|
||||
# bundled YAML files so we only ship those, not the entire
|
||||
# secrets.yaml which may contain secrets for other devices.
|
||||
yaml_sources = [
|
||||
bf.source for bf in files if bf.source.suffix in (".yaml", ".yml")
|
||||
]
|
||||
used_secret_keys = _find_used_secret_keys(yaml_sources)
|
||||
filtered_secrets = self._build_filtered_secrets(used_secret_keys)
|
||||
|
||||
has_secrets = bool(filtered_secrets)
|
||||
if has_secrets:
|
||||
_LOGGER.warning(
|
||||
"Bundle contains secrets (e.g. Wi-Fi passwords). "
|
||||
"Do not share it with untrusted parties."
|
||||
)
|
||||
|
||||
manifest = self._build_manifest(files, has_secrets=has_secrets)
|
||||
|
||||
buf = io.BytesIO()
|
||||
with tarfile.open(fileobj=buf, mode="w:gz") as tar:
|
||||
# Add manifest first
|
||||
manifest_data = json.dumps(manifest, indent=2).encode("utf-8")
|
||||
_add_bytes_to_tar(tar, MANIFEST_FILENAME, manifest_data)
|
||||
|
||||
# Add filtered secrets files
|
||||
for rel_path, data in sorted(filtered_secrets.items()):
|
||||
_add_bytes_to_tar(tar, rel_path, data)
|
||||
|
||||
# Add files in sorted order for determinism, skipping secrets
|
||||
# files which were already added above with filtered content
|
||||
for bf in sorted(files, key=lambda f: f.path):
|
||||
if bf.source in self._secrets_paths:
|
||||
continue
|
||||
self._add_to_tar(tar, bf)
|
||||
|
||||
return BundleResult(data=buf.getvalue(), manifest=manifest, files=files)
|
||||
|
||||
def _add_file(self, abs_path: Path) -> bool:
|
||||
"""Add a file to the bundle. Returns False if already added."""
|
||||
abs_path = abs_path.resolve()
|
||||
if abs_path in self._seen_paths:
|
||||
return False
|
||||
if not abs_path.is_file():
|
||||
_LOGGER.warning("Bundle: skipping missing file %s", abs_path)
|
||||
return False
|
||||
|
||||
rel_path = self._relative_to_config_dir(abs_path)
|
||||
if rel_path is None:
|
||||
_LOGGER.warning(
|
||||
"Bundle: skipping file outside config directory: %s", abs_path
|
||||
)
|
||||
return False
|
||||
|
||||
self._seen_paths.add(abs_path)
|
||||
self._files.append(BundleFile(path=rel_path, source=abs_path))
|
||||
return True
|
||||
|
||||
def _add_directory(self, abs_path: Path) -> None:
|
||||
"""Recursively add all files in a directory."""
|
||||
abs_path = abs_path.resolve()
|
||||
if not abs_path.is_dir():
|
||||
_LOGGER.warning("Bundle: skipping missing directory %s", abs_path)
|
||||
return
|
||||
for child in sorted(abs_path.rglob("*")):
|
||||
if child.is_file() and "__pycache__" not in child.parts:
|
||||
self._add_file(child)
|
||||
|
||||
def _relative_to_config_dir(self, abs_path: Path) -> str | None:
|
||||
"""Get a path relative to the config directory. Returns None if outside.
|
||||
|
||||
Always uses forward slashes for consistency in tar archives.
|
||||
"""
|
||||
try:
|
||||
return abs_path.relative_to(self._config_dir).as_posix()
|
||||
except ValueError:
|
||||
return None
|
||||
|
||||
def _discover_yaml_includes(self) -> None:
|
||||
"""Discover YAML files loaded during config parsing.
|
||||
|
||||
We track files by wrapping _load_yaml_internal. The config has already
|
||||
been loaded at this point (bundle is a POST_CONFIG_ACTION), so we
|
||||
re-load just to discover the file list.
|
||||
|
||||
Secrets files are tracked separately so we can filter them to
|
||||
only include the keys this config actually references.
|
||||
"""
|
||||
with yaml_util.track_yaml_loads() as loaded_files:
|
||||
try:
|
||||
yaml_util.load_yaml(self._config_path)
|
||||
except EsphomeError:
|
||||
_LOGGER.debug(
|
||||
"Bundle: re-loading YAML for include discovery failed, "
|
||||
"proceeding with partial file list"
|
||||
)
|
||||
|
||||
for fpath in loaded_files:
|
||||
if fpath == self._config_path.resolve():
|
||||
continue # Already added as config
|
||||
if fpath.name in const.SECRETS_FILES:
|
||||
self._secrets_paths.add(fpath)
|
||||
self._add_file(fpath)
|
||||
|
||||
def _discover_component_files(self) -> None:
|
||||
"""Walk the validated config for file references.
|
||||
|
||||
Uses a generic recursive walk to find file paths instead of
|
||||
hardcoding per-component knowledge about config dict formats.
|
||||
After validation, components typically resolve paths to absolute
|
||||
using CORE.relative_config_path() or cv.file_(). Relative paths
|
||||
with known file extensions are also resolved and checked.
|
||||
|
||||
Core ESPHome concepts that use relative paths or directories
|
||||
are handled explicitly.
|
||||
"""
|
||||
config = self._config
|
||||
|
||||
# Generic walk: find all file paths in the validated config
|
||||
self._walk_config_for_files(config)
|
||||
|
||||
# --- Core ESPHome concepts needing explicit handling ---
|
||||
|
||||
# esphome.includes / includes_c - can be relative paths and directories
|
||||
esphome_conf = config.get(CONF_ESPHOME, {})
|
||||
for include_path in esphome_conf.get(CONF_INCLUDES, []):
|
||||
resolved = _resolve_include_path(include_path)
|
||||
if resolved is None:
|
||||
continue
|
||||
if resolved.is_dir():
|
||||
self._add_directory(resolved)
|
||||
else:
|
||||
self._add_file(resolved)
|
||||
for include_path in esphome_conf.get(CONF_INCLUDES_C, []):
|
||||
resolved = _resolve_include_path(include_path)
|
||||
if resolved is not None:
|
||||
self._add_file(resolved)
|
||||
|
||||
# external_components with source: local - directories
|
||||
for ext_conf in config.get(CONF_EXTERNAL_COMPONENTS, []):
|
||||
source = ext_conf.get(CONF_SOURCE, {})
|
||||
if not isinstance(source, dict):
|
||||
continue
|
||||
if source.get(CONF_TYPE) != "local":
|
||||
continue
|
||||
path = source.get(CONF_PATH)
|
||||
if not path:
|
||||
continue
|
||||
p = Path(path)
|
||||
if not p.is_absolute():
|
||||
p = CORE.relative_config_path(p)
|
||||
self._add_directory(p)
|
||||
|
||||
def _walk_config_for_files(self, obj: Any) -> None:
|
||||
"""Recursively walk the config dict looking for file path references."""
|
||||
if isinstance(obj, dict):
|
||||
for value in obj.values():
|
||||
self._walk_config_for_files(value)
|
||||
elif isinstance(obj, (list, tuple)):
|
||||
for item in obj:
|
||||
self._walk_config_for_files(item)
|
||||
elif isinstance(obj, Path):
|
||||
if obj.is_absolute() and obj.is_file():
|
||||
self._add_file(obj)
|
||||
elif isinstance(obj, str):
|
||||
self._check_string_path(obj)
|
||||
|
||||
def _check_string_path(self, value: str) -> None:
|
||||
"""Check if a string value is a local file reference."""
|
||||
# Fast exits for strings that cannot be file paths
|
||||
if len(value) < 2 or "\n" in value:
|
||||
return
|
||||
if value.startswith(_NON_PATH_PREFIXES):
|
||||
return
|
||||
# File paths must contain a path separator or a dot (for extension)
|
||||
if "/" not in value and "\\" not in value and "." not in value:
|
||||
return
|
||||
|
||||
p = Path(value)
|
||||
|
||||
# Absolute path - check if it points to an existing file
|
||||
if p.is_absolute():
|
||||
if p.is_file():
|
||||
self._add_file(p)
|
||||
return
|
||||
|
||||
# Relative path with a known file extension - likely a component
|
||||
# validator that forgot to resolve to absolute via cv.file_() or
|
||||
# CORE.relative_config_path(). Warn and try to resolve.
|
||||
if p.suffix.lower() in _KNOWN_FILE_EXTENSIONS:
|
||||
_LOGGER.warning(
|
||||
"Bundle: non-absolute path in validated config: %s "
|
||||
"(component validator should return absolute paths)",
|
||||
value,
|
||||
)
|
||||
resolved = CORE.relative_config_path(p)
|
||||
if resolved.is_file():
|
||||
self._add_file(resolved)
|
||||
|
||||
def _build_filtered_secrets(self, used_keys: set[str]) -> dict[str, bytes]:
|
||||
"""Build filtered secrets files containing only the referenced keys.
|
||||
|
||||
Returns a dict mapping relative archive path to YAML bytes.
|
||||
"""
|
||||
if not used_keys or not self._secrets_paths:
|
||||
return {}
|
||||
|
||||
result: dict[str, bytes] = {}
|
||||
for secrets_path in self._secrets_paths:
|
||||
rel_path = self._relative_to_config_dir(secrets_path)
|
||||
if rel_path is None:
|
||||
continue
|
||||
try:
|
||||
all_secrets = yaml_util.load_yaml(secrets_path, clear_secrets=False)
|
||||
except EsphomeError:
|
||||
_LOGGER.warning("Bundle: failed to load secrets file %s", secrets_path)
|
||||
continue
|
||||
if not isinstance(all_secrets, dict):
|
||||
continue
|
||||
filtered = {k: v for k, v in all_secrets.items() if k in used_keys}
|
||||
if filtered:
|
||||
data = yaml_util.dump(filtered, show_secrets=True).encode("utf-8")
|
||||
result[rel_path] = data
|
||||
return result
|
||||
|
||||
def _build_manifest(
|
||||
self, files: list[BundleFile], *, has_secrets: bool
|
||||
) -> dict[str, Any]:
|
||||
"""Build the manifest.json content."""
|
||||
return {
|
||||
ManifestKey.MANIFEST_VERSION: CURRENT_MANIFEST_VERSION,
|
||||
ManifestKey.ESPHOME_VERSION: const.__version__,
|
||||
ManifestKey.CONFIG_FILENAME: self._config_path.name,
|
||||
ManifestKey.FILES: [f.path for f in files],
|
||||
ManifestKey.HAS_SECRETS: has_secrets,
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _add_to_tar(tar: tarfile.TarFile, bf: BundleFile) -> None:
|
||||
"""Add a BundleFile to the tar archive with deterministic metadata."""
|
||||
with open(bf.source, "rb") as f:
|
||||
_add_bytes_to_tar(tar, bf.path, f.read())
|
||||
|
||||
|
||||
def extract_bundle(
|
||||
bundle_path: Path,
|
||||
target_dir: Path | None = None,
|
||||
) -> Path:
|
||||
"""Extract a bundle archive and return the path to the config YAML.
|
||||
|
||||
Sanity checks reject path traversal, symlinks, absolute paths, and
|
||||
oversized archives to prevent accidental file overwrites or extraction
|
||||
outside the target directory. These are **not** a security boundary —
|
||||
bundles are assumed to come from the user's own machine or a trusted
|
||||
build pipeline.
|
||||
|
||||
Args:
|
||||
bundle_path: Path to the .tar.gz bundle file.
|
||||
target_dir: Directory to extract into. If None, extracts next to
|
||||
the bundle file in a directory named after it.
|
||||
|
||||
Returns:
|
||||
Absolute path to the extracted config YAML file.
|
||||
|
||||
Raises:
|
||||
EsphomeError: If the bundle is invalid or extraction fails.
|
||||
"""
|
||||
|
||||
bundle_path = bundle_path.resolve()
|
||||
if not bundle_path.is_file():
|
||||
raise EsphomeError(f"Bundle file not found: {bundle_path}")
|
||||
|
||||
if target_dir is None:
|
||||
target_dir = _default_target_dir(bundle_path)
|
||||
|
||||
target_dir = target_dir.resolve()
|
||||
target_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Read and validate the archive
|
||||
try:
|
||||
with tarfile.open(bundle_path, "r:gz") as tar:
|
||||
manifest = _read_manifest_from_tar(tar)
|
||||
_validate_tar_members(tar, target_dir)
|
||||
tar.extractall(path=target_dir, filter="data")
|
||||
except tarfile.TarError as err:
|
||||
raise EsphomeError(f"Failed to extract bundle: {err}") from err
|
||||
|
||||
config_filename = manifest[ManifestKey.CONFIG_FILENAME]
|
||||
config_path = target_dir / config_filename
|
||||
if not config_path.is_file():
|
||||
raise EsphomeError(
|
||||
f"Bundle manifest references config '{config_filename}' "
|
||||
f"but it was not found in the archive"
|
||||
)
|
||||
|
||||
return config_path
|
||||
|
||||
|
||||
def read_bundle_manifest(bundle_path: Path) -> BundleManifest:
|
||||
"""Read and validate the manifest from a bundle without full extraction.
|
||||
|
||||
Args:
|
||||
bundle_path: Path to the .tar.gz bundle file.
|
||||
|
||||
Returns:
|
||||
Parsed BundleManifest.
|
||||
|
||||
Raises:
|
||||
EsphomeError: If the manifest is missing, invalid, or version unsupported.
|
||||
"""
|
||||
|
||||
try:
|
||||
with tarfile.open(bundle_path, "r:gz") as tar:
|
||||
manifest = _read_manifest_from_tar(tar)
|
||||
except tarfile.TarError as err:
|
||||
raise EsphomeError(f"Failed to read bundle: {err}") from err
|
||||
|
||||
return BundleManifest(
|
||||
manifest_version=manifest[ManifestKey.MANIFEST_VERSION],
|
||||
esphome_version=manifest.get(ManifestKey.ESPHOME_VERSION, "unknown"),
|
||||
config_filename=manifest[ManifestKey.CONFIG_FILENAME],
|
||||
files=manifest.get(ManifestKey.FILES, []),
|
||||
has_secrets=manifest.get(ManifestKey.HAS_SECRETS, False),
|
||||
)
|
||||
|
||||
|
||||
def _read_manifest_from_tar(tar: tarfile.TarFile) -> dict[str, Any]:
|
||||
"""Read and validate manifest.json from an open tar archive."""
|
||||
|
||||
try:
|
||||
member = tar.getmember(MANIFEST_FILENAME)
|
||||
except KeyError:
|
||||
raise EsphomeError("Invalid bundle: missing manifest.json") from None
|
||||
|
||||
f = tar.extractfile(member)
|
||||
if f is None:
|
||||
raise EsphomeError("Invalid bundle: manifest.json is not a regular file")
|
||||
|
||||
if member.size > MAX_MANIFEST_SIZE:
|
||||
raise EsphomeError(
|
||||
f"Invalid bundle: manifest.json too large "
|
||||
f"({member.size} bytes, max {MAX_MANIFEST_SIZE})"
|
||||
)
|
||||
|
||||
try:
|
||||
manifest = json.loads(f.read())
|
||||
except (json.JSONDecodeError, UnicodeDecodeError) as err:
|
||||
raise EsphomeError(f"Invalid bundle: malformed manifest.json: {err}") from err
|
||||
|
||||
# Version check
|
||||
version = manifest.get(ManifestKey.MANIFEST_VERSION)
|
||||
if version is None:
|
||||
raise EsphomeError("Invalid bundle: manifest.json missing 'manifest_version'")
|
||||
if not isinstance(version, int) or version < 1:
|
||||
raise EsphomeError(
|
||||
f"Invalid bundle: manifest_version must be a positive integer, got {version!r}"
|
||||
)
|
||||
if version > CURRENT_MANIFEST_VERSION:
|
||||
raise EsphomeError(
|
||||
f"Bundle manifest version {version} is newer than this ESPHome "
|
||||
f"version supports (max {CURRENT_MANIFEST_VERSION}). "
|
||||
f"Please upgrade ESPHome to compile this bundle."
|
||||
)
|
||||
|
||||
# Required fields
|
||||
if ManifestKey.CONFIG_FILENAME not in manifest:
|
||||
raise EsphomeError("Invalid bundle: manifest.json missing 'config_filename'")
|
||||
|
||||
return manifest
|
||||
|
||||
|
||||
def _validate_tar_members(tar: tarfile.TarFile, target_dir: Path) -> None:
|
||||
"""Sanity-check tar members to prevent mistakes and accidental overwrites.
|
||||
|
||||
This is not a security boundary — bundles are created locally or come
|
||||
from a trusted build pipeline. The checks catch malformed archives
|
||||
and common mistakes (stray absolute paths, ``..`` components) that
|
||||
could silently overwrite unrelated files.
|
||||
"""
|
||||
|
||||
total_size = 0
|
||||
for member in tar.getmembers():
|
||||
# Reject absolute paths (Unix and Windows)
|
||||
if member.name.startswith(("/", "\\")):
|
||||
raise EsphomeError(
|
||||
f"Invalid bundle: absolute path in archive: {member.name}"
|
||||
)
|
||||
|
||||
# Reject path traversal (split on both / and \ for cross-platform)
|
||||
parts = re.split(r"[/\\]", member.name)
|
||||
if ".." in parts:
|
||||
raise EsphomeError(
|
||||
f"Invalid bundle: path traversal in archive: {member.name}"
|
||||
)
|
||||
|
||||
# Reject symlinks
|
||||
if member.issym() or member.islnk():
|
||||
raise EsphomeError(f"Invalid bundle: symlink in archive: {member.name}")
|
||||
|
||||
# Ensure extraction stays within target_dir
|
||||
target_path = (target_dir / member.name).resolve()
|
||||
if not target_path.is_relative_to(target_dir):
|
||||
raise EsphomeError(
|
||||
f"Invalid bundle: file would extract outside target: {member.name}"
|
||||
)
|
||||
|
||||
# Track total decompressed size
|
||||
total_size += member.size
|
||||
if total_size > MAX_DECOMPRESSED_SIZE:
|
||||
raise EsphomeError(
|
||||
f"Invalid bundle: decompressed size exceeds "
|
||||
f"{MAX_DECOMPRESSED_SIZE // (1024 * 1024)}MB limit"
|
||||
)
|
||||
|
||||
|
||||
def is_bundle_path(path: Path) -> bool:
|
||||
"""Check if a path looks like a bundle file."""
|
||||
return path.name.lower().endswith(BUNDLE_EXTENSION)
|
||||
|
||||
|
||||
def _add_bytes_to_tar(tar: tarfile.TarFile, name: str, data: bytes) -> None:
|
||||
"""Add in-memory bytes to a tar archive with deterministic metadata."""
|
||||
info = tarfile.TarInfo(name=name)
|
||||
info.size = len(data)
|
||||
info.mtime = 0
|
||||
info.uid = 0
|
||||
info.gid = 0
|
||||
info.mode = 0o644
|
||||
tar.addfile(info, io.BytesIO(data))
|
||||
|
||||
|
||||
def _resolve_include_path(include_path: Any) -> Path | None:
|
||||
"""Resolve an include path to absolute, skipping system includes."""
|
||||
if isinstance(include_path, str) and include_path.startswith("<"):
|
||||
return None # System include, not a local file
|
||||
p = Path(include_path)
|
||||
if not p.is_absolute():
|
||||
p = CORE.relative_config_path(p)
|
||||
return p
|
||||
|
||||
|
||||
def _default_target_dir(bundle_path: Path) -> Path:
|
||||
"""Compute the default extraction directory for a bundle."""
|
||||
name = bundle_path.name
|
||||
if name.lower().endswith(BUNDLE_EXTENSION):
|
||||
name = name[: -len(BUNDLE_EXTENSION)]
|
||||
return bundle_path.parent / name
|
||||
|
||||
|
||||
def _restore_preserved_dirs(preserved: dict[str, Path], target_dir: Path) -> None:
|
||||
"""Move preserved build cache directories back into target_dir.
|
||||
|
||||
If the bundle contained entries under a preserved directory name,
|
||||
the extracted copy is removed so the original cache always wins.
|
||||
"""
|
||||
for dirname, src in preserved.items():
|
||||
dst = target_dir / dirname
|
||||
if dst.exists():
|
||||
shutil.rmtree(dst)
|
||||
shutil.move(str(src), str(dst))
|
||||
|
||||
|
||||
def prepare_bundle_for_compile(
|
||||
bundle_path: Path,
|
||||
target_dir: Path | None = None,
|
||||
) -> Path:
|
||||
"""Extract a bundle for compilation, preserving build caches.
|
||||
|
||||
Unlike extract_bundle(), this preserves .esphome/ and .pioenvs/
|
||||
directories in the target if they already exist (for incremental builds).
|
||||
|
||||
Args:
|
||||
bundle_path: Path to the .tar.gz bundle file.
|
||||
target_dir: Directory to extract into. Must be specified for
|
||||
build server use.
|
||||
|
||||
Returns:
|
||||
Absolute path to the extracted config YAML file.
|
||||
"""
|
||||
|
||||
bundle_path = bundle_path.resolve()
|
||||
if not bundle_path.is_file():
|
||||
raise EsphomeError(f"Bundle file not found: {bundle_path}")
|
||||
|
||||
if target_dir is None:
|
||||
target_dir = _default_target_dir(bundle_path)
|
||||
|
||||
target_dir = target_dir.resolve()
|
||||
target_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
preserved: dict[str, Path] = {}
|
||||
|
||||
# Temporarily move preserved dirs out of the way
|
||||
staging = target_dir / _BUNDLE_STAGING_DIR
|
||||
for dirname in _PRESERVE_DIRS:
|
||||
src = target_dir / dirname
|
||||
if src.is_dir():
|
||||
dst = staging / dirname
|
||||
dst.parent.mkdir(parents=True, exist_ok=True)
|
||||
shutil.move(str(src), str(dst))
|
||||
preserved[dirname] = dst
|
||||
|
||||
try:
|
||||
# Clean non-preserved content and extract fresh
|
||||
for item in target_dir.iterdir():
|
||||
if item.name == _BUNDLE_STAGING_DIR:
|
||||
continue
|
||||
if item.is_dir():
|
||||
shutil.rmtree(item)
|
||||
else:
|
||||
item.unlink()
|
||||
|
||||
config_path = extract_bundle(bundle_path, target_dir)
|
||||
finally:
|
||||
# Restore preserved dirs (idempotent) and clean staging
|
||||
_restore_preserved_dirs(preserved, target_dir)
|
||||
if staging.is_dir():
|
||||
shutil.rmtree(staging)
|
||||
|
||||
return config_path
|
||||
@@ -219,35 +219,8 @@ void APIConnection::loop() {
|
||||
this->process_batch_();
|
||||
}
|
||||
|
||||
switch (this->active_iterator_) {
|
||||
case ActiveIterator::LIST_ENTITIES:
|
||||
if (this->iterator_storage_.list_entities.completed()) {
|
||||
this->destroy_active_iterator_();
|
||||
if (this->flags_.state_subscription) {
|
||||
this->begin_iterator_(ActiveIterator::INITIAL_STATE);
|
||||
}
|
||||
} else {
|
||||
this->process_iterator_batch_(this->iterator_storage_.list_entities);
|
||||
}
|
||||
break;
|
||||
case ActiveIterator::INITIAL_STATE:
|
||||
if (this->iterator_storage_.initial_state.completed()) {
|
||||
this->destroy_active_iterator_();
|
||||
// Process any remaining batched messages immediately
|
||||
if (!this->deferred_batch_.empty()) {
|
||||
this->process_batch_();
|
||||
}
|
||||
// Now that everything is sent, enable immediate sending for future state changes
|
||||
this->flags_.should_try_send_immediately = true;
|
||||
// Release excess memory from buffers that grew during initial sync
|
||||
this->deferred_batch_.release_buffer();
|
||||
this->helper_->release_buffers();
|
||||
} else {
|
||||
this->process_iterator_batch_(this->iterator_storage_.initial_state);
|
||||
}
|
||||
break;
|
||||
case ActiveIterator::NONE:
|
||||
break;
|
||||
if (this->active_iterator_ != ActiveIterator::NONE) {
|
||||
this->process_active_iterator_();
|
||||
}
|
||||
|
||||
if (this->flags_.sent_ping) {
|
||||
@@ -283,6 +256,49 @@ void APIConnection::loop() {
|
||||
#endif
|
||||
}
|
||||
|
||||
void APIConnection::process_active_iterator_() {
|
||||
// Caller ensures active_iterator_ != NONE
|
||||
if (this->active_iterator_ == ActiveIterator::LIST_ENTITIES) {
|
||||
if (this->iterator_storage_.list_entities.completed()) {
|
||||
this->destroy_active_iterator_();
|
||||
if (this->flags_.state_subscription) {
|
||||
this->begin_iterator_(ActiveIterator::INITIAL_STATE);
|
||||
}
|
||||
} else {
|
||||
this->process_iterator_batch_(this->iterator_storage_.list_entities);
|
||||
}
|
||||
} else { // INITIAL_STATE
|
||||
if (this->iterator_storage_.initial_state.completed()) {
|
||||
this->destroy_active_iterator_();
|
||||
// Process any remaining batched messages immediately
|
||||
if (!this->deferred_batch_.empty()) {
|
||||
this->process_batch_();
|
||||
}
|
||||
// Now that everything is sent, enable immediate sending for future state changes
|
||||
this->flags_.should_try_send_immediately = true;
|
||||
// Release excess memory from buffers that grew during initial sync
|
||||
this->deferred_batch_.release_buffer();
|
||||
this->helper_->release_buffers();
|
||||
} else {
|
||||
this->process_iterator_batch_(this->iterator_storage_.initial_state);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void APIConnection::process_iterator_batch_(ComponentIterator &iterator) {
|
||||
size_t initial_size = this->deferred_batch_.size();
|
||||
size_t max_batch = this->get_max_batch_size_();
|
||||
while (!iterator.completed() && (this->deferred_batch_.size() - initial_size) < max_batch) {
|
||||
iterator.advance();
|
||||
}
|
||||
|
||||
// If the batch is full, process it immediately
|
||||
// Note: iterator.advance() already calls schedule_batch_() via schedule_message_()
|
||||
if (this->deferred_batch_.size() >= max_batch) {
|
||||
this->process_batch_();
|
||||
}
|
||||
}
|
||||
|
||||
bool APIConnection::send_disconnect_response_() {
|
||||
// remote initiated disconnect_client
|
||||
// don't close yet, we still need to send the disconnect response
|
||||
|
||||
@@ -15,6 +15,10 @@
|
||||
#include <limits>
|
||||
#include <vector>
|
||||
|
||||
namespace esphome {
|
||||
class ComponentIterator;
|
||||
} // namespace esphome
|
||||
|
||||
namespace esphome::api {
|
||||
|
||||
// Keepalive timeout in milliseconds
|
||||
@@ -364,20 +368,13 @@ class APIConnection final : public APIServerConnectionBase {
|
||||
return this->client_supports_api_version(1, 14) ? MAX_INITIAL_PER_BATCH : MAX_INITIAL_PER_BATCH_LEGACY;
|
||||
}
|
||||
|
||||
// Helper method to process multiple entities from an iterator in a batch
|
||||
template<typename Iterator> void process_iterator_batch_(Iterator &iterator) {
|
||||
size_t initial_size = this->deferred_batch_.size();
|
||||
size_t max_batch = this->get_max_batch_size_();
|
||||
while (!iterator.completed() && (this->deferred_batch_.size() - initial_size) < max_batch) {
|
||||
iterator.advance();
|
||||
}
|
||||
// Process active iterator (list_entities/initial_state) during connection setup.
|
||||
// Extracted from loop() — only runs during initial handshake, NONE in steady state.
|
||||
void __attribute__((noinline)) process_active_iterator_();
|
||||
|
||||
// If the batch is full, process it immediately
|
||||
// Note: iterator.advance() already calls schedule_batch_() via schedule_message_()
|
||||
if (this->deferred_batch_.size() >= max_batch) {
|
||||
this->process_batch_();
|
||||
}
|
||||
}
|
||||
// Helper method to process multiple entities from an iterator in a batch.
|
||||
// Takes ComponentIterator base class reference to avoid duplicate template instantiations.
|
||||
void process_iterator_batch_(ComponentIterator &iterator);
|
||||
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
static uint16_t try_send_binary_sensor_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size);
|
||||
|
||||
@@ -94,7 +94,6 @@ class ListEntitiesIterator : public ComponentIterator {
|
||||
bool on_update(update::UpdateEntity *entity) override;
|
||||
#endif
|
||||
bool on_end() override;
|
||||
bool completed() { return this->state_ == IteratorState::NONE; }
|
||||
|
||||
protected:
|
||||
APIConnection *client_;
|
||||
|
||||
@@ -88,7 +88,6 @@ class InitialStateIterator : public ComponentIterator {
|
||||
#ifdef USE_UPDATE
|
||||
bool on_update(update::UpdateEntity *entity) override;
|
||||
#endif
|
||||
bool completed() { return this->state_ == IteratorState::NONE; }
|
||||
|
||||
protected:
|
||||
APIConnection *client_;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#pragma once
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <limits>
|
||||
#include "abstract_aqi_calculator.h"
|
||||
@@ -14,7 +15,11 @@ class AQICalculator : public AbstractAQICalculator {
|
||||
float pm2_5_index = calculate_index(pm2_5_value, PM2_5_GRID);
|
||||
float pm10_0_index = calculate_index(pm10_0_value, PM10_0_GRID);
|
||||
|
||||
return static_cast<uint16_t>(std::round((pm2_5_index < pm10_0_index) ? pm10_0_index : pm2_5_index));
|
||||
float aqi = std::max(pm2_5_index, pm10_0_index);
|
||||
if (aqi < 0.0f) {
|
||||
aqi = 0.0f;
|
||||
}
|
||||
return static_cast<uint16_t>(std::lround(aqi));
|
||||
}
|
||||
|
||||
protected:
|
||||
@@ -22,13 +27,27 @@ class AQICalculator : public AbstractAQICalculator {
|
||||
|
||||
static constexpr int INDEX_GRID[NUM_LEVELS][2] = {{0, 50}, {51, 100}, {101, 150}, {151, 200}, {201, 300}, {301, 500}};
|
||||
|
||||
static constexpr float PM2_5_GRID[NUM_LEVELS][2] = {{0.0f, 9.0f}, {9.1f, 35.4f},
|
||||
{35.5f, 55.4f}, {55.5f, 125.4f},
|
||||
{125.5f, 225.4f}, {225.5f, std::numeric_limits<float>::max()}};
|
||||
static constexpr float PM2_5_GRID[NUM_LEVELS][2] = {
|
||||
// clang-format off
|
||||
{0.0f, 9.1f},
|
||||
{9.1f, 35.5f},
|
||||
{35.5f, 55.5f},
|
||||
{55.5f, 125.5f},
|
||||
{125.5f, 225.5f},
|
||||
{225.5f, std::numeric_limits<float>::max()}
|
||||
// clang-format on
|
||||
};
|
||||
|
||||
static constexpr float PM10_0_GRID[NUM_LEVELS][2] = {{0.0f, 54.0f}, {55.0f, 154.0f},
|
||||
{155.0f, 254.0f}, {255.0f, 354.0f},
|
||||
{355.0f, 424.0f}, {425.0f, std::numeric_limits<float>::max()}};
|
||||
static constexpr float PM10_0_GRID[NUM_LEVELS][2] = {
|
||||
// clang-format off
|
||||
{0.0f, 55.0f},
|
||||
{55.0f, 155.0f},
|
||||
{155.0f, 255.0f},
|
||||
{255.0f, 355.0f},
|
||||
{355.0f, 425.0f},
|
||||
{425.0f, std::numeric_limits<float>::max()}
|
||||
// clang-format on
|
||||
};
|
||||
|
||||
static float calculate_index(float value, const float array[NUM_LEVELS][2]) {
|
||||
int grid_index = get_grid_index(value, array);
|
||||
@@ -45,7 +64,10 @@ class AQICalculator : public AbstractAQICalculator {
|
||||
|
||||
static int get_grid_index(float value, const float array[NUM_LEVELS][2]) {
|
||||
for (int i = 0; i < NUM_LEVELS; i++) {
|
||||
if (value >= array[i][0] && value <= array[i][1]) {
|
||||
const bool in_range =
|
||||
(value >= array[i][0]) && ((i == NUM_LEVELS - 1) ? (value <= array[i][1]) // last bucket inclusive
|
||||
: (value < array[i][1])); // others exclusive on hi
|
||||
if (in_range) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#pragma once
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <limits>
|
||||
#include "abstract_aqi_calculator.h"
|
||||
@@ -12,7 +13,11 @@ class CAQICalculator : public AbstractAQICalculator {
|
||||
float pm2_5_index = calculate_index(pm2_5_value, PM2_5_GRID);
|
||||
float pm10_0_index = calculate_index(pm10_0_value, PM10_0_GRID);
|
||||
|
||||
return static_cast<uint16_t>(std::round((pm2_5_index < pm10_0_index) ? pm10_0_index : pm2_5_index));
|
||||
float aqi = std::max(pm2_5_index, pm10_0_index);
|
||||
if (aqi < 0.0f) {
|
||||
aqi = 0.0f;
|
||||
}
|
||||
return static_cast<uint16_t>(std::lround(aqi));
|
||||
}
|
||||
|
||||
protected:
|
||||
@@ -21,10 +26,24 @@ class CAQICalculator : public AbstractAQICalculator {
|
||||
static constexpr int INDEX_GRID[NUM_LEVELS][2] = {{0, 25}, {26, 50}, {51, 75}, {76, 100}, {101, 400}};
|
||||
|
||||
static constexpr float PM2_5_GRID[NUM_LEVELS][2] = {
|
||||
{0.0f, 15.0f}, {15.1f, 30.0f}, {30.1f, 55.0f}, {55.1f, 110.0f}, {110.1f, std::numeric_limits<float>::max()}};
|
||||
// clang-format off
|
||||
{0.0f, 15.1f},
|
||||
{15.1f, 30.1f},
|
||||
{30.1f, 55.1f},
|
||||
{55.1f, 110.1f},
|
||||
{110.1f, std::numeric_limits<float>::max()}
|
||||
// clang-format on
|
||||
};
|
||||
|
||||
static constexpr float PM10_0_GRID[NUM_LEVELS][2] = {
|
||||
{0.0f, 25.0f}, {25.1f, 50.0f}, {50.1f, 90.0f}, {90.1f, 180.0f}, {180.1f, std::numeric_limits<float>::max()}};
|
||||
// clang-format off
|
||||
{0.0f, 25.1f},
|
||||
{25.1f, 50.1f},
|
||||
{50.1f, 90.1f},
|
||||
{90.1f, 180.1f},
|
||||
{180.1f, std::numeric_limits<float>::max()}
|
||||
// clang-format on
|
||||
};
|
||||
|
||||
static float calculate_index(float value, const float array[NUM_LEVELS][2]) {
|
||||
int grid_index = get_grid_index(value, array);
|
||||
@@ -42,7 +61,10 @@ class CAQICalculator : public AbstractAQICalculator {
|
||||
|
||||
static int get_grid_index(float value, const float array[NUM_LEVELS][2]) {
|
||||
for (int i = 0; i < NUM_LEVELS; i++) {
|
||||
if (value >= array[i][0] && value <= array[i][1]) {
|
||||
const bool in_range =
|
||||
(value >= array[i][0]) && ((i == NUM_LEVELS - 1) ? (value <= array[i][1]) // last bucket inclusive
|
||||
: (value < array[i][1])); // others exclusive on hi
|
||||
if (in_range) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,16 +46,16 @@ static const uint32_t PKT_TIMEOUT_MS = 200;
|
||||
|
||||
void BL0942::loop() {
|
||||
DataPacket buffer;
|
||||
int avail = this->available();
|
||||
size_t avail = this->available();
|
||||
|
||||
if (!avail) {
|
||||
return;
|
||||
}
|
||||
if (static_cast<size_t>(avail) < sizeof(buffer)) {
|
||||
if (avail < sizeof(buffer)) {
|
||||
if (!this->rx_start_) {
|
||||
this->rx_start_ = millis();
|
||||
} else if (millis() > this->rx_start_ + PKT_TIMEOUT_MS) {
|
||||
ESP_LOGW(TAG, "Junk on wire. Throwing away partial message (%d bytes)", avail);
|
||||
ESP_LOGW(TAG, "Junk on wire. Throwing away partial message (%zu bytes)", avail);
|
||||
this->read_array((uint8_t *) &buffer, avail);
|
||||
this->rx_start_ = 0;
|
||||
}
|
||||
|
||||
@@ -16,8 +16,8 @@ void CSE7766Component::loop() {
|
||||
}
|
||||
|
||||
// Early return prevents updating last_transmission_ when no data is available.
|
||||
int avail = this->available();
|
||||
if (avail <= 0) {
|
||||
size_t avail = this->available();
|
||||
if (avail == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ void CSE7766Component::loop() {
|
||||
// At 4800 baud (~480 bytes/sec) with ~122 Hz loop rate, typically ~4 bytes per call.
|
||||
uint8_t buf[CSE7766_RAW_DATA_SIZE];
|
||||
while (avail > 0) {
|
||||
size_t to_read = std::min(static_cast<size_t>(avail), sizeof(buf));
|
||||
size_t to_read = std::min(avail, sizeof(buf));
|
||||
if (!this->read_array(buf, to_read)) {
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -133,10 +133,10 @@ void DFPlayer::send_cmd_(uint8_t cmd, uint16_t argument) {
|
||||
|
||||
void DFPlayer::loop() {
|
||||
// Read all available bytes in batches to reduce UART call overhead.
|
||||
int avail = this->available();
|
||||
size_t avail = this->available();
|
||||
uint8_t buf[64];
|
||||
while (avail > 0) {
|
||||
size_t to_read = std::min(static_cast<size_t>(avail), sizeof(buf));
|
||||
size_t to_read = std::min(avail, sizeof(buf));
|
||||
if (!this->read_array(buf, to_read)) {
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -28,15 +28,28 @@ void DlmsMeterComponent::dump_config() {
|
||||
|
||||
void DlmsMeterComponent::loop() {
|
||||
// Read while data is available, netznoe uses two frames so allow 2x max frame length
|
||||
while (this->available()) {
|
||||
if (this->receive_buffer_.size() >= MBUS_MAX_FRAME_LENGTH * 2) {
|
||||
size_t avail = this->available();
|
||||
if (avail > 0) {
|
||||
size_t remaining = MBUS_MAX_FRAME_LENGTH * 2 - this->receive_buffer_.size();
|
||||
if (remaining == 0) {
|
||||
ESP_LOGW(TAG, "Receive buffer full, dropping remaining bytes");
|
||||
break;
|
||||
} else {
|
||||
// Read all available bytes in batches to reduce UART call overhead.
|
||||
// Cap reads to remaining buffer capacity.
|
||||
if (avail > remaining) {
|
||||
avail = remaining;
|
||||
}
|
||||
uint8_t buf[64];
|
||||
while (avail > 0) {
|
||||
size_t to_read = std::min(avail, sizeof(buf));
|
||||
if (!this->read_array(buf, to_read)) {
|
||||
break;
|
||||
}
|
||||
avail -= to_read;
|
||||
this->receive_buffer_.insert(this->receive_buffer_.end(), buf, buf + to_read);
|
||||
this->last_read_ = millis();
|
||||
}
|
||||
}
|
||||
uint8_t c;
|
||||
this->read_byte(&c);
|
||||
this->receive_buffer_.push_back(c);
|
||||
this->last_read_ = millis();
|
||||
}
|
||||
|
||||
if (!this->receive_buffer_.empty() && millis() - this->last_read_ > this->read_timeout_) {
|
||||
|
||||
@@ -40,9 +40,7 @@ bool Dsmr::ready_to_request_data_() {
|
||||
this->start_requesting_data_();
|
||||
}
|
||||
if (!this->requesting_data_) {
|
||||
while (this->available()) {
|
||||
this->read();
|
||||
}
|
||||
this->drain_rx_buffer_();
|
||||
}
|
||||
}
|
||||
return this->requesting_data_;
|
||||
@@ -115,138 +113,169 @@ void Dsmr::stop_requesting_data_() {
|
||||
} else {
|
||||
ESP_LOGV(TAG, "Stop reading data from P1 port");
|
||||
}
|
||||
while (this->available()) {
|
||||
this->read();
|
||||
}
|
||||
this->drain_rx_buffer_();
|
||||
this->requesting_data_ = false;
|
||||
}
|
||||
}
|
||||
|
||||
void Dsmr::drain_rx_buffer_() {
|
||||
uint8_t buf[64];
|
||||
size_t avail;
|
||||
while ((avail = this->available()) > 0) {
|
||||
if (!this->read_array(buf, std::min(avail, sizeof(buf)))) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Dsmr::reset_telegram_() {
|
||||
this->header_found_ = false;
|
||||
this->footer_found_ = false;
|
||||
this->bytes_read_ = 0;
|
||||
this->crypt_bytes_read_ = 0;
|
||||
this->crypt_telegram_len_ = 0;
|
||||
this->last_read_time_ = 0;
|
||||
}
|
||||
|
||||
void Dsmr::receive_telegram_() {
|
||||
while (this->available_within_timeout_()) {
|
||||
const char c = this->read();
|
||||
// Read all available bytes in batches to reduce UART call overhead.
|
||||
uint8_t buf[64];
|
||||
size_t avail = this->available();
|
||||
while (avail > 0) {
|
||||
size_t to_read = std::min(avail, sizeof(buf));
|
||||
if (!this->read_array(buf, to_read))
|
||||
return;
|
||||
avail -= to_read;
|
||||
|
||||
// Find a new telegram header, i.e. forward slash.
|
||||
if (c == '/') {
|
||||
ESP_LOGV(TAG, "Header of telegram found");
|
||||
this->reset_telegram_();
|
||||
this->header_found_ = true;
|
||||
}
|
||||
if (!this->header_found_)
|
||||
continue;
|
||||
for (size_t i = 0; i < to_read; i++) {
|
||||
const char c = static_cast<char>(buf[i]);
|
||||
|
||||
// Check for buffer overflow.
|
||||
if (this->bytes_read_ >= this->max_telegram_len_) {
|
||||
this->reset_telegram_();
|
||||
ESP_LOGE(TAG, "Error: telegram larger than buffer (%d bytes)", this->max_telegram_len_);
|
||||
return;
|
||||
}
|
||||
// Find a new telegram header, i.e. forward slash.
|
||||
if (c == '/') {
|
||||
ESP_LOGV(TAG, "Header of telegram found");
|
||||
this->reset_telegram_();
|
||||
this->header_found_ = true;
|
||||
}
|
||||
if (!this->header_found_)
|
||||
continue;
|
||||
|
||||
// Some v2.2 or v3 meters will send a new value which starts with '('
|
||||
// in a new line, while the value belongs to the previous ObisId. For
|
||||
// proper parsing, remove these new line characters.
|
||||
if (c == '(') {
|
||||
while (true) {
|
||||
auto previous_char = this->telegram_[this->bytes_read_ - 1];
|
||||
if (previous_char == '\n' || previous_char == '\r') {
|
||||
this->bytes_read_--;
|
||||
} else {
|
||||
break;
|
||||
// Check for buffer overflow.
|
||||
if (this->bytes_read_ >= this->max_telegram_len_) {
|
||||
this->reset_telegram_();
|
||||
ESP_LOGE(TAG, "Error: telegram larger than buffer (%d bytes)", this->max_telegram_len_);
|
||||
return;
|
||||
}
|
||||
|
||||
// Some v2.2 or v3 meters will send a new value which starts with '('
|
||||
// in a new line, while the value belongs to the previous ObisId. For
|
||||
// proper parsing, remove these new line characters.
|
||||
if (c == '(') {
|
||||
while (true) {
|
||||
auto previous_char = this->telegram_[this->bytes_read_ - 1];
|
||||
if (previous_char == '\n' || previous_char == '\r') {
|
||||
this->bytes_read_--;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Store the byte in the buffer.
|
||||
this->telegram_[this->bytes_read_] = c;
|
||||
this->bytes_read_++;
|
||||
|
||||
// Check for a footer, i.e. exclamation mark, followed by a hex checksum.
|
||||
if (c == '!') {
|
||||
ESP_LOGV(TAG, "Footer of telegram found");
|
||||
this->footer_found_ = true;
|
||||
continue;
|
||||
}
|
||||
// Check for the end of the hex checksum, i.e. a newline.
|
||||
if (this->footer_found_ && c == '\n') {
|
||||
// Parse the telegram and publish sensor values.
|
||||
this->parse_telegram();
|
||||
this->reset_telegram_();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Store the byte in the buffer.
|
||||
this->telegram_[this->bytes_read_] = c;
|
||||
this->bytes_read_++;
|
||||
|
||||
// Check for a footer, i.e. exclamation mark, followed by a hex checksum.
|
||||
if (c == '!') {
|
||||
ESP_LOGV(TAG, "Footer of telegram found");
|
||||
this->footer_found_ = true;
|
||||
continue;
|
||||
}
|
||||
// Check for the end of the hex checksum, i.e. a newline.
|
||||
if (this->footer_found_ && c == '\n') {
|
||||
// Parse the telegram and publish sensor values.
|
||||
this->parse_telegram();
|
||||
this->reset_telegram_();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Dsmr::receive_encrypted_telegram_() {
|
||||
while (this->available_within_timeout_()) {
|
||||
const char c = this->read();
|
||||
// Read all available bytes in batches to reduce UART call overhead.
|
||||
uint8_t buf[64];
|
||||
size_t avail = this->available();
|
||||
while (avail > 0) {
|
||||
size_t to_read = std::min(avail, sizeof(buf));
|
||||
if (!this->read_array(buf, to_read))
|
||||
return;
|
||||
avail -= to_read;
|
||||
|
||||
// Find a new telegram start byte.
|
||||
if (!this->header_found_) {
|
||||
if ((uint8_t) c != 0xDB) {
|
||||
continue;
|
||||
for (size_t i = 0; i < to_read; i++) {
|
||||
const char c = static_cast<char>(buf[i]);
|
||||
|
||||
// Find a new telegram start byte.
|
||||
if (!this->header_found_) {
|
||||
if ((uint8_t) c != 0xDB) {
|
||||
continue;
|
||||
}
|
||||
ESP_LOGV(TAG, "Start byte 0xDB of encrypted telegram found");
|
||||
this->reset_telegram_();
|
||||
this->header_found_ = true;
|
||||
}
|
||||
|
||||
// Check for buffer overflow.
|
||||
if (this->crypt_bytes_read_ >= this->max_telegram_len_) {
|
||||
this->reset_telegram_();
|
||||
ESP_LOGE(TAG, "Error: encrypted telegram larger than buffer (%d bytes)", this->max_telegram_len_);
|
||||
return;
|
||||
}
|
||||
|
||||
// Store the byte in the buffer.
|
||||
this->crypt_telegram_[this->crypt_bytes_read_] = c;
|
||||
this->crypt_bytes_read_++;
|
||||
|
||||
// Read the length of the incoming encrypted telegram.
|
||||
if (this->crypt_telegram_len_ == 0 && this->crypt_bytes_read_ > 20) {
|
||||
// Complete header + data bytes
|
||||
this->crypt_telegram_len_ = 13 + (this->crypt_telegram_[11] << 8 | this->crypt_telegram_[12]);
|
||||
ESP_LOGV(TAG, "Encrypted telegram length: %d bytes", this->crypt_telegram_len_);
|
||||
}
|
||||
|
||||
// Check for the end of the encrypted telegram.
|
||||
if (this->crypt_telegram_len_ == 0 || this->crypt_bytes_read_ != this->crypt_telegram_len_) {
|
||||
continue;
|
||||
}
|
||||
ESP_LOGV(TAG, "End of encrypted telegram found");
|
||||
|
||||
// Decrypt the encrypted telegram.
|
||||
GCM<AES128> *gcmaes128{new GCM<AES128>()};
|
||||
gcmaes128->setKey(this->decryption_key_.data(), gcmaes128->keySize());
|
||||
// the iv is 8 bytes of the system title + 4 bytes frame counter
|
||||
// system title is at byte 2 and frame counter at byte 15
|
||||
for (int i = 10; i < 14; i++)
|
||||
this->crypt_telegram_[i] = this->crypt_telegram_[i + 4];
|
||||
constexpr uint16_t iv_size{12};
|
||||
gcmaes128->setIV(&this->crypt_telegram_[2], iv_size);
|
||||
gcmaes128->decrypt(reinterpret_cast<uint8_t *>(this->telegram_),
|
||||
// the ciphertext start at byte 18
|
||||
&this->crypt_telegram_[18],
|
||||
// cipher size
|
||||
this->crypt_bytes_read_ - 17);
|
||||
delete gcmaes128; // NOLINT(cppcoreguidelines-owning-memory)
|
||||
|
||||
this->bytes_read_ = strnlen(this->telegram_, this->max_telegram_len_);
|
||||
ESP_LOGV(TAG, "Decrypted telegram size: %d bytes", this->bytes_read_);
|
||||
ESP_LOGVV(TAG, "Decrypted telegram: %s", this->telegram_);
|
||||
|
||||
// Parse the decrypted telegram and publish sensor values.
|
||||
this->parse_telegram();
|
||||
this->reset_telegram_();
|
||||
return;
|
||||
}
|
||||
ESP_LOGV(TAG, "Start byte 0xDB of encrypted telegram found");
|
||||
this->reset_telegram_();
|
||||
this->header_found_ = true;
|
||||
}
|
||||
|
||||
// Check for buffer overflow.
|
||||
if (this->crypt_bytes_read_ >= this->max_telegram_len_) {
|
||||
this->reset_telegram_();
|
||||
ESP_LOGE(TAG, "Error: encrypted telegram larger than buffer (%d bytes)", this->max_telegram_len_);
|
||||
return;
|
||||
}
|
||||
|
||||
// Store the byte in the buffer.
|
||||
this->crypt_telegram_[this->crypt_bytes_read_] = c;
|
||||
this->crypt_bytes_read_++;
|
||||
|
||||
// Read the length of the incoming encrypted telegram.
|
||||
if (this->crypt_telegram_len_ == 0 && this->crypt_bytes_read_ > 20) {
|
||||
// Complete header + data bytes
|
||||
this->crypt_telegram_len_ = 13 + (this->crypt_telegram_[11] << 8 | this->crypt_telegram_[12]);
|
||||
ESP_LOGV(TAG, "Encrypted telegram length: %d bytes", this->crypt_telegram_len_);
|
||||
}
|
||||
|
||||
// Check for the end of the encrypted telegram.
|
||||
if (this->crypt_telegram_len_ == 0 || this->crypt_bytes_read_ != this->crypt_telegram_len_) {
|
||||
continue;
|
||||
}
|
||||
ESP_LOGV(TAG, "End of encrypted telegram found");
|
||||
|
||||
// Decrypt the encrypted telegram.
|
||||
GCM<AES128> *gcmaes128{new GCM<AES128>()};
|
||||
gcmaes128->setKey(this->decryption_key_.data(), gcmaes128->keySize());
|
||||
// the iv is 8 bytes of the system title + 4 bytes frame counter
|
||||
// system title is at byte 2 and frame counter at byte 15
|
||||
for (int i = 10; i < 14; i++)
|
||||
this->crypt_telegram_[i] = this->crypt_telegram_[i + 4];
|
||||
constexpr uint16_t iv_size{12};
|
||||
gcmaes128->setIV(&this->crypt_telegram_[2], iv_size);
|
||||
gcmaes128->decrypt(reinterpret_cast<uint8_t *>(this->telegram_),
|
||||
// the ciphertext start at byte 18
|
||||
&this->crypt_telegram_[18],
|
||||
// cipher size
|
||||
this->crypt_bytes_read_ - 17);
|
||||
delete gcmaes128; // NOLINT(cppcoreguidelines-owning-memory)
|
||||
|
||||
this->bytes_read_ = strnlen(this->telegram_, this->max_telegram_len_);
|
||||
ESP_LOGV(TAG, "Decrypted telegram size: %d bytes", this->bytes_read_);
|
||||
ESP_LOGVV(TAG, "Decrypted telegram: %s", this->telegram_);
|
||||
|
||||
// Parse the decrypted telegram and publish sensor values.
|
||||
this->parse_telegram();
|
||||
this->reset_telegram_();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -85,6 +85,7 @@ class Dsmr : public Component, public uart::UARTDevice {
|
||||
void receive_telegram_();
|
||||
void receive_encrypted_telegram_();
|
||||
void reset_telegram_();
|
||||
void drain_rx_buffer_();
|
||||
|
||||
/// Wait for UART data to become available within the read timeout.
|
||||
///
|
||||
|
||||
@@ -1,20 +1,16 @@
|
||||
#include "hlk_fm22x.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include <array>
|
||||
#include <cinttypes>
|
||||
|
||||
namespace esphome::hlk_fm22x {
|
||||
|
||||
static const char *const TAG = "hlk_fm22x";
|
||||
|
||||
// Maximum response size is 36 bytes (VERIFY reply: face_id + 32-byte name)
|
||||
static constexpr size_t HLK_FM22X_MAX_RESPONSE_SIZE = 36;
|
||||
|
||||
void HlkFm22xComponent::setup() {
|
||||
ESP_LOGCONFIG(TAG, "Setting up HLK-FM22X...");
|
||||
this->set_enrolling_(false);
|
||||
while (this->available()) {
|
||||
while (this->available() > 0) {
|
||||
this->read();
|
||||
}
|
||||
this->defer([this]() { this->send_command_(HlkFm22xCommand::GET_STATUS); });
|
||||
@@ -35,7 +31,7 @@ void HlkFm22xComponent::update() {
|
||||
}
|
||||
|
||||
void HlkFm22xComponent::enroll_face(const std::string &name, HlkFm22xFaceDirection direction) {
|
||||
if (name.length() > 31) {
|
||||
if (name.length() > HLK_FM22X_NAME_SIZE - 1) {
|
||||
ESP_LOGE(TAG, "enroll_face(): name too long '%s'", name.c_str());
|
||||
return;
|
||||
}
|
||||
@@ -88,7 +84,7 @@ void HlkFm22xComponent::send_command_(HlkFm22xCommand command, const uint8_t *da
|
||||
}
|
||||
this->wait_cycles_ = 0;
|
||||
this->active_command_ = command;
|
||||
while (this->available())
|
||||
while (this->available() > 0)
|
||||
this->read();
|
||||
this->write((uint8_t) (START_CODE >> 8));
|
||||
this->write((uint8_t) (START_CODE & 0xFF));
|
||||
@@ -137,17 +133,24 @@ void HlkFm22xComponent::recv_command_() {
|
||||
checksum ^= byte;
|
||||
length |= byte;
|
||||
|
||||
std::vector<uint8_t> data;
|
||||
data.reserve(length);
|
||||
if (length > HLK_FM22X_MAX_RESPONSE_SIZE) {
|
||||
ESP_LOGE(TAG, "Response too large: %u bytes", length);
|
||||
// Discard exactly the remaining payload and checksum for this frame
|
||||
for (uint16_t i = 0; i < length + 1 && this->available() > 0; ++i)
|
||||
this->read();
|
||||
return;
|
||||
}
|
||||
|
||||
for (uint16_t idx = 0; idx < length; ++idx) {
|
||||
byte = this->read();
|
||||
checksum ^= byte;
|
||||
data.push_back(byte);
|
||||
this->recv_buf_[idx] = byte;
|
||||
}
|
||||
|
||||
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
|
||||
char hex_buf[format_hex_pretty_size(HLK_FM22X_MAX_RESPONSE_SIZE)];
|
||||
ESP_LOGV(TAG, "Recv type: 0x%.2X, data: %s", response_type, format_hex_pretty_to(hex_buf, data.data(), data.size()));
|
||||
ESP_LOGV(TAG, "Recv type: 0x%.2X, data: %s", response_type,
|
||||
format_hex_pretty_to(hex_buf, this->recv_buf_.data(), length));
|
||||
#endif
|
||||
|
||||
byte = this->read();
|
||||
@@ -157,10 +160,10 @@ void HlkFm22xComponent::recv_command_() {
|
||||
}
|
||||
switch (response_type) {
|
||||
case HlkFm22xResponseType::NOTE:
|
||||
this->handle_note_(data);
|
||||
this->handle_note_(this->recv_buf_.data(), length);
|
||||
break;
|
||||
case HlkFm22xResponseType::REPLY:
|
||||
this->handle_reply_(data);
|
||||
this->handle_reply_(this->recv_buf_.data(), length);
|
||||
break;
|
||||
default:
|
||||
ESP_LOGW(TAG, "Unexpected response type: 0x%.2X", response_type);
|
||||
@@ -168,11 +171,15 @@ void HlkFm22xComponent::recv_command_() {
|
||||
}
|
||||
}
|
||||
|
||||
void HlkFm22xComponent::handle_note_(const std::vector<uint8_t> &data) {
|
||||
void HlkFm22xComponent::handle_note_(const uint8_t *data, size_t length) {
|
||||
if (length < 1) {
|
||||
ESP_LOGE(TAG, "Empty note data");
|
||||
return;
|
||||
}
|
||||
switch (data[0]) {
|
||||
case HlkFm22xNoteType::FACE_STATE:
|
||||
if (data.size() < 17) {
|
||||
ESP_LOGE(TAG, "Invalid face note data size: %u", data.size());
|
||||
if (length < 17) {
|
||||
ESP_LOGE(TAG, "Invalid face note data size: %zu", length);
|
||||
break;
|
||||
}
|
||||
{
|
||||
@@ -209,9 +216,13 @@ void HlkFm22xComponent::handle_note_(const std::vector<uint8_t> &data) {
|
||||
}
|
||||
}
|
||||
|
||||
void HlkFm22xComponent::handle_reply_(const std::vector<uint8_t> &data) {
|
||||
void HlkFm22xComponent::handle_reply_(const uint8_t *data, size_t length) {
|
||||
auto expected = this->active_command_;
|
||||
this->active_command_ = HlkFm22xCommand::NONE;
|
||||
if (length < 2) {
|
||||
ESP_LOGE(TAG, "Reply too short: %zu bytes", length);
|
||||
return;
|
||||
}
|
||||
if (data[0] != (uint8_t) expected) {
|
||||
ESP_LOGE(TAG, "Unexpected response command. Expected: 0x%.2X, Received: 0x%.2X", expected, data[0]);
|
||||
return;
|
||||
@@ -238,16 +249,20 @@ void HlkFm22xComponent::handle_reply_(const std::vector<uint8_t> &data) {
|
||||
}
|
||||
switch (expected) {
|
||||
case HlkFm22xCommand::VERIFY: {
|
||||
if (length < 4 + HLK_FM22X_NAME_SIZE) {
|
||||
ESP_LOGE(TAG, "VERIFY response too short: %zu bytes", length);
|
||||
break;
|
||||
}
|
||||
int16_t face_id = ((int16_t) data[2] << 8) | data[3];
|
||||
std::string name(data.begin() + 4, data.begin() + 36);
|
||||
ESP_LOGD(TAG, "Face verified. ID: %d, name: %s", face_id, name.c_str());
|
||||
const char *name_ptr = reinterpret_cast<const char *>(data + 4);
|
||||
ESP_LOGD(TAG, "Face verified. ID: %d, name: %.*s", face_id, (int) HLK_FM22X_NAME_SIZE, name_ptr);
|
||||
if (this->last_face_id_sensor_ != nullptr) {
|
||||
this->last_face_id_sensor_->publish_state(face_id);
|
||||
}
|
||||
if (this->last_face_name_text_sensor_ != nullptr) {
|
||||
this->last_face_name_text_sensor_->publish_state(name);
|
||||
this->last_face_name_text_sensor_->publish_state(name_ptr, HLK_FM22X_NAME_SIZE);
|
||||
}
|
||||
this->face_scan_matched_callback_.call(face_id, name);
|
||||
this->face_scan_matched_callback_.call(face_id, std::string(name_ptr, HLK_FM22X_NAME_SIZE));
|
||||
break;
|
||||
}
|
||||
case HlkFm22xCommand::ENROLL: {
|
||||
@@ -266,9 +281,8 @@ void HlkFm22xComponent::handle_reply_(const std::vector<uint8_t> &data) {
|
||||
this->defer([this]() { this->send_command_(HlkFm22xCommand::GET_VERSION); });
|
||||
break;
|
||||
case HlkFm22xCommand::GET_VERSION:
|
||||
if (this->version_text_sensor_ != nullptr) {
|
||||
std::string version(data.begin() + 2, data.end());
|
||||
this->version_text_sensor_->publish_state(version);
|
||||
if (this->version_text_sensor_ != nullptr && length > 2) {
|
||||
this->version_text_sensor_->publish_state(reinterpret_cast<const char *>(data + 2), length - 2);
|
||||
}
|
||||
this->defer([this]() { this->get_face_count_(); });
|
||||
break;
|
||||
|
||||
@@ -7,12 +7,15 @@
|
||||
#include "esphome/components/text_sensor/text_sensor.h"
|
||||
#include "esphome/components/uart/uart.h"
|
||||
|
||||
#include <array>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace esphome::hlk_fm22x {
|
||||
|
||||
static const uint16_t START_CODE = 0xEFAA;
|
||||
static constexpr size_t HLK_FM22X_NAME_SIZE = 32;
|
||||
// Maximum response payload: command(1) + result(1) + face_id(2) + name(32) = 36
|
||||
static constexpr size_t HLK_FM22X_MAX_RESPONSE_SIZE = 36;
|
||||
enum HlkFm22xCommand {
|
||||
NONE = 0x00,
|
||||
RESET = 0x10,
|
||||
@@ -118,10 +121,11 @@ class HlkFm22xComponent : public PollingComponent, public uart::UARTDevice {
|
||||
void get_face_count_();
|
||||
void send_command_(HlkFm22xCommand command, const uint8_t *data = nullptr, size_t size = 0);
|
||||
void recv_command_();
|
||||
void handle_note_(const std::vector<uint8_t> &data);
|
||||
void handle_reply_(const std::vector<uint8_t> &data);
|
||||
void handle_note_(const uint8_t *data, size_t length);
|
||||
void handle_reply_(const uint8_t *data, size_t length);
|
||||
void set_enrolling_(bool enrolling);
|
||||
|
||||
std::array<uint8_t, HLK_FM22X_MAX_RESPONSE_SIZE> recv_buf_;
|
||||
HlkFm22xCommand active_command_ = HlkFm22xCommand::NONE;
|
||||
uint16_t wait_cycles_ = 0;
|
||||
sensor::Sensor *face_count_sensor_{nullptr};
|
||||
|
||||
@@ -276,10 +276,10 @@ void LD2410Component::restart_and_read_all_info() {
|
||||
|
||||
void LD2410Component::loop() {
|
||||
// Read all available bytes in batches to reduce UART call overhead.
|
||||
int avail = this->available();
|
||||
size_t avail = this->available();
|
||||
uint8_t buf[MAX_LINE_LENGTH];
|
||||
while (avail > 0) {
|
||||
size_t to_read = std::min(static_cast<size_t>(avail), sizeof(buf));
|
||||
size_t to_read = std::min(avail, sizeof(buf));
|
||||
if (!this->read_array(buf, to_read)) {
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -311,10 +311,10 @@ void LD2412Component::restart_and_read_all_info() {
|
||||
|
||||
void LD2412Component::loop() {
|
||||
// Read all available bytes in batches to reduce UART call overhead.
|
||||
int avail = this->available();
|
||||
size_t avail = this->available();
|
||||
uint8_t buf[MAX_LINE_LENGTH];
|
||||
while (avail > 0) {
|
||||
size_t to_read = std::min(static_cast<size_t>(avail), sizeof(buf));
|
||||
size_t to_read = std::min(avail, sizeof(buf));
|
||||
if (!this->read_array(buf, to_read)) {
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -335,9 +335,10 @@ void LD2420Component::revert_config_action() {
|
||||
|
||||
void LD2420Component::loop() {
|
||||
// If there is a active send command do not process it here, the send command call will handle it.
|
||||
while (!this->cmd_active_ && this->available()) {
|
||||
this->readline_(this->read(), this->buffer_data_, MAX_LINE_LENGTH);
|
||||
if (this->cmd_active_) {
|
||||
return;
|
||||
}
|
||||
this->read_batch_(this->buffer_data_);
|
||||
}
|
||||
|
||||
void LD2420Component::update_radar_data(uint16_t const *gate_energy, uint8_t sample_number) {
|
||||
@@ -539,6 +540,23 @@ void LD2420Component::handle_simple_mode_(const uint8_t *inbuf, int len) {
|
||||
}
|
||||
}
|
||||
|
||||
void LD2420Component::read_batch_(std::span<uint8_t, MAX_LINE_LENGTH> buffer) {
|
||||
// Read all available bytes in batches to reduce UART call overhead.
|
||||
size_t avail = this->available();
|
||||
uint8_t buf[MAX_LINE_LENGTH];
|
||||
while (avail > 0) {
|
||||
size_t to_read = std::min(avail, sizeof(buf));
|
||||
if (!this->read_array(buf, to_read)) {
|
||||
break;
|
||||
}
|
||||
avail -= to_read;
|
||||
|
||||
for (size_t i = 0; i < to_read; i++) {
|
||||
this->readline_(buf[i], buffer.data(), buffer.size());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void LD2420Component::handle_ack_data_(uint8_t *buffer, int len) {
|
||||
this->cmd_reply_.command = buffer[CMD_FRAME_COMMAND];
|
||||
this->cmd_reply_.length = buffer[CMD_FRAME_DATA_LENGTH];
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include "esphome/components/uart/uart.h"
|
||||
#include "esphome/core/automation.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include <span>
|
||||
#ifdef USE_TEXT_SENSOR
|
||||
#include "esphome/components/text_sensor/text_sensor.h"
|
||||
#endif
|
||||
@@ -165,6 +166,7 @@ class LD2420Component : public Component, public uart::UARTDevice {
|
||||
void handle_energy_mode_(uint8_t *buffer, int len);
|
||||
void handle_ack_data_(uint8_t *buffer, int len);
|
||||
void readline_(int rx_data, uint8_t *buffer, int len);
|
||||
void read_batch_(std::span<uint8_t, MAX_LINE_LENGTH> buffer);
|
||||
void set_calibration_(bool state) { this->calibration_ = state; };
|
||||
bool get_calibration_() { return this->calibration_; };
|
||||
|
||||
|
||||
@@ -277,10 +277,10 @@ void LD2450Component::dump_config() {
|
||||
|
||||
void LD2450Component::loop() {
|
||||
// Read all available bytes in batches to reduce UART call overhead.
|
||||
int avail = this->available();
|
||||
size_t avail = this->available();
|
||||
uint8_t buf[MAX_LINE_LENGTH];
|
||||
while (avail > 0) {
|
||||
size_t to_read = std::min(static_cast<size_t>(avail), sizeof(buf));
|
||||
size_t to_read = std::min(avail, sizeof(buf));
|
||||
if (!this->read_array(buf, to_read)) {
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -36,8 +36,9 @@ void HOT Logger::log_vprintf_(uint8_t level, const char *tag, int line, const ch
|
||||
#endif
|
||||
|
||||
// Fast path: main thread, no recursion (99.9% of all logs)
|
||||
// Pass nullptr for thread_name since we already know this is the main task
|
||||
if (is_main_task && !this->main_task_recursion_guard_) [[likely]] {
|
||||
this->log_message_to_buffer_and_send_(this->main_task_recursion_guard_, level, tag, line, format, args);
|
||||
this->log_message_to_buffer_and_send_(this->main_task_recursion_guard_, level, tag, line, format, args, nullptr);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -47,21 +48,23 @@ void HOT Logger::log_vprintf_(uint8_t level, const char *tag, int line, const ch
|
||||
}
|
||||
|
||||
// Non-main thread handling (~0.1% of logs)
|
||||
// Resolve thread name once and pass it through the logging chain.
|
||||
// ESP32/LibreTiny: use TaskHandle_t overload to avoid redundant xTaskGetCurrentTaskHandle()
|
||||
// (we already have the handle from the main task check above).
|
||||
// Host: pass a stack buffer for pthread_getname_np to write into.
|
||||
#if defined(USE_ESP32) || defined(USE_LIBRETINY)
|
||||
this->log_vprintf_non_main_thread_(level, tag, line, format, args, current_task);
|
||||
const char *thread_name = get_thread_name_(current_task);
|
||||
#else // USE_HOST
|
||||
this->log_vprintf_non_main_thread_(level, tag, line, format, args);
|
||||
char thread_name_buf[THREAD_NAME_BUF_SIZE];
|
||||
const char *thread_name = this->get_thread_name_(thread_name_buf);
|
||||
#endif
|
||||
this->log_vprintf_non_main_thread_(level, tag, line, format, args, thread_name);
|
||||
}
|
||||
|
||||
// Handles non-main thread logging only
|
||||
// Kept separate from hot path to improve instruction cache performance
|
||||
#if defined(USE_ESP32) || defined(USE_LIBRETINY)
|
||||
void Logger::log_vprintf_non_main_thread_(uint8_t level, const char *tag, int line, const char *format, va_list args,
|
||||
TaskHandle_t current_task) {
|
||||
#else // USE_HOST
|
||||
void Logger::log_vprintf_non_main_thread_(uint8_t level, const char *tag, int line, const char *format, va_list args) {
|
||||
#endif
|
||||
const char *thread_name) {
|
||||
// Check if already in recursion for this non-main thread/task
|
||||
if (this->is_non_main_task_recursive_()) {
|
||||
return;
|
||||
@@ -73,12 +76,8 @@ void Logger::log_vprintf_non_main_thread_(uint8_t level, const char *tag, int li
|
||||
bool message_sent = false;
|
||||
#ifdef USE_ESPHOME_TASK_LOG_BUFFER
|
||||
// For non-main threads/tasks, queue the message for callbacks
|
||||
#if defined(USE_ESP32) || defined(USE_LIBRETINY)
|
||||
message_sent =
|
||||
this->log_buffer_->send_message_thread_safe(level, tag, static_cast<uint16_t>(line), current_task, format, args);
|
||||
#else // USE_HOST
|
||||
message_sent = this->log_buffer_->send_message_thread_safe(level, tag, static_cast<uint16_t>(line), format, args);
|
||||
#endif
|
||||
this->log_buffer_->send_message_thread_safe(level, tag, static_cast<uint16_t>(line), thread_name, format, args);
|
||||
if (message_sent) {
|
||||
// Enable logger loop to process the buffered message
|
||||
// This is safe to call from any context including ISRs
|
||||
@@ -101,19 +100,27 @@ void Logger::log_vprintf_non_main_thread_(uint8_t level, const char *tag, int li
|
||||
#endif
|
||||
char console_buffer[MAX_CONSOLE_LOG_MSG_SIZE]; // MUST be stack allocated for thread safety
|
||||
LogBuffer buf{console_buffer, MAX_CONSOLE_LOG_MSG_SIZE};
|
||||
this->format_log_to_buffer_with_terminator_(level, tag, line, format, args, buf);
|
||||
this->format_log_to_buffer_with_terminator_(level, tag, line, format, args, buf, thread_name);
|
||||
this->write_to_console_(buf);
|
||||
}
|
||||
|
||||
// RAII guard automatically resets on return
|
||||
}
|
||||
#else
|
||||
// Implementation for all other platforms (single-task, no threading)
|
||||
// Implementation for single-task platforms (ESP8266, RP2040, Zephyr)
|
||||
// TODO: Zephyr may have multiple threads (work queues, etc.) but uses this single-task path.
|
||||
// Logging calls are NOT thread-safe: global_recursion_guard_ is a plain bool and tx_buffer_ has no locking.
|
||||
// Not a problem in practice yet since Zephyr has no API support (logs are console-only).
|
||||
void HOT Logger::log_vprintf_(uint8_t level, const char *tag, int line, const char *format, va_list args) { // NOLINT
|
||||
if (level > this->level_for(tag) || global_recursion_guard_)
|
||||
return;
|
||||
|
||||
this->log_message_to_buffer_and_send_(global_recursion_guard_, level, tag, line, format, args);
|
||||
#ifdef USE_ZEPHYR
|
||||
char tmp[MAX_POINTER_REPRESENTATION];
|
||||
this->log_message_to_buffer_and_send_(global_recursion_guard_, level, tag, line, format, args,
|
||||
this->get_thread_name_(tmp));
|
||||
#else // Other single-task platforms don't have thread names, so pass nullptr
|
||||
this->log_message_to_buffer_and_send_(global_recursion_guard_, level, tag, line, format, args, nullptr);
|
||||
#endif
|
||||
}
|
||||
#endif // USE_ESP32 / USE_HOST / USE_LIBRETINY
|
||||
|
||||
@@ -129,7 +136,7 @@ void Logger::log_vprintf_(uint8_t level, const char *tag, int line, const __Flas
|
||||
if (level > this->level_for(tag) || global_recursion_guard_)
|
||||
return;
|
||||
|
||||
this->log_message_to_buffer_and_send_(global_recursion_guard_, level, tag, line, format, args);
|
||||
this->log_message_to_buffer_and_send_(global_recursion_guard_, level, tag, line, format, args, nullptr);
|
||||
}
|
||||
#endif // USE_STORE_LOG_STR_IN_FLASH
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#include <cstdarg>
|
||||
#include <map>
|
||||
#include <span>
|
||||
#include <type_traits>
|
||||
#if defined(USE_ESP32) || defined(USE_HOST)
|
||||
#include <pthread.h>
|
||||
@@ -124,6 +125,10 @@ static constexpr uint16_t MAX_HEADER_SIZE = 128;
|
||||
// "0x" + 2 hex digits per byte + '\0'
|
||||
static constexpr size_t MAX_POINTER_REPRESENTATION = 2 + sizeof(void *) * 2 + 1;
|
||||
|
||||
// Stack buffer size for retrieving thread/task names from the OS
|
||||
// macOS allows up to 64 bytes, Linux up to 16
|
||||
static constexpr size_t THREAD_NAME_BUF_SIZE = 64;
|
||||
|
||||
// Buffer wrapper for log formatting functions
|
||||
struct LogBuffer {
|
||||
char *data;
|
||||
@@ -408,34 +413,24 @@ class Logger : public Component {
|
||||
|
||||
#if defined(USE_ESP32) || defined(USE_HOST) || defined(USE_LIBRETINY)
|
||||
// Handles non-main thread logging only (~0.1% of calls)
|
||||
#if defined(USE_ESP32) || defined(USE_LIBRETINY)
|
||||
// ESP32/LibreTiny: Pass task handle to avoid calling xTaskGetCurrentTaskHandle() twice
|
||||
// thread_name is resolved by the caller from the task handle, avoiding redundant lookups
|
||||
void log_vprintf_non_main_thread_(uint8_t level, const char *tag, int line, const char *format, va_list args,
|
||||
TaskHandle_t current_task);
|
||||
#else // USE_HOST
|
||||
// Host: No task handle parameter needed (not used in send_message_thread_safe)
|
||||
void log_vprintf_non_main_thread_(uint8_t level, const char *tag, int line, const char *format, va_list args);
|
||||
#endif
|
||||
const char *thread_name);
|
||||
#endif
|
||||
void process_messages_();
|
||||
void write_msg_(const char *msg, uint16_t len);
|
||||
|
||||
// Format a log message with printf-style arguments and write it to a buffer with header, footer, and null terminator
|
||||
// thread_name: name of the calling thread/task, or nullptr for main task (callers already know which task they're on)
|
||||
inline void HOT format_log_to_buffer_with_terminator_(uint8_t level, const char *tag, int line, const char *format,
|
||||
va_list args, LogBuffer &buf) {
|
||||
#if defined(USE_ESP32) || defined(USE_LIBRETINY) || defined(USE_HOST)
|
||||
buf.write_header(level, tag, line, this->get_thread_name_());
|
||||
#elif defined(USE_ZEPHYR)
|
||||
char tmp[MAX_POINTER_REPRESENTATION];
|
||||
buf.write_header(level, tag, line, this->get_thread_name_(tmp));
|
||||
#else
|
||||
buf.write_header(level, tag, line, nullptr);
|
||||
#endif
|
||||
va_list args, LogBuffer &buf, const char *thread_name) {
|
||||
buf.write_header(level, tag, line, thread_name);
|
||||
buf.format_body(format, args);
|
||||
}
|
||||
|
||||
#ifdef USE_STORE_LOG_STR_IN_FLASH
|
||||
// Format a log message with flash string format and write it to a buffer with header, footer, and null terminator
|
||||
// ESP8266-only (single-task), thread_name is always nullptr
|
||||
inline void HOT format_log_to_buffer_with_terminator_P_(uint8_t level, const char *tag, int line,
|
||||
const __FlashStringHelper *format, va_list args,
|
||||
LogBuffer &buf) {
|
||||
@@ -466,9 +461,10 @@ class Logger : public Component {
|
||||
|
||||
// Helper to format and send a log message to both console and listeners
|
||||
// Template handles both const char* (RAM) and __FlashStringHelper* (flash) format strings
|
||||
// thread_name: name of the calling thread/task, or nullptr for main task
|
||||
template<typename FormatType>
|
||||
inline void HOT log_message_to_buffer_and_send_(bool &recursion_guard, uint8_t level, const char *tag, int line,
|
||||
FormatType format, va_list args) {
|
||||
FormatType format, va_list args, const char *thread_name) {
|
||||
RecursionGuard guard(recursion_guard);
|
||||
LogBuffer buf{this->tx_buffer_, this->tx_buffer_size_};
|
||||
#ifdef USE_STORE_LOG_STR_IN_FLASH
|
||||
@@ -477,7 +473,7 @@ class Logger : public Component {
|
||||
} else
|
||||
#endif
|
||||
{
|
||||
this->format_log_to_buffer_with_terminator_(level, tag, line, format, args, buf);
|
||||
this->format_log_to_buffer_with_terminator_(level, tag, line, format, args, buf, thread_name);
|
||||
}
|
||||
this->notify_listeners_(level, tag, buf);
|
||||
this->write_log_buffer_to_console_(buf);
|
||||
@@ -565,37 +561,57 @@ class Logger : public Component {
|
||||
bool global_recursion_guard_{false}; // Simple global recursion guard for single-task platforms
|
||||
#endif
|
||||
|
||||
#if defined(USE_ESP32) || defined(USE_LIBRETINY) || defined(USE_ZEPHYR)
|
||||
const char *HOT get_thread_name_(
|
||||
#ifdef USE_ZEPHYR
|
||||
char *buff
|
||||
// --- get_thread_name_ overloads (per-platform) ---
|
||||
|
||||
#if defined(USE_ESP32) || defined(USE_LIBRETINY)
|
||||
// Primary overload - takes a task handle directly to avoid redundant xTaskGetCurrentTaskHandle() calls
|
||||
// when the caller already has the handle (e.g. from the main task check in log_vprintf_)
|
||||
const char *get_thread_name_(TaskHandle_t task) {
|
||||
if (task == this->main_task_) {
|
||||
return nullptr; // Main task
|
||||
}
|
||||
#if defined(USE_ESP32)
|
||||
return pcTaskGetName(task);
|
||||
#elif defined(USE_LIBRETINY)
|
||||
return pcTaskGetTaskName(task);
|
||||
#endif
|
||||
) {
|
||||
#ifdef USE_ZEPHYR
|
||||
}
|
||||
|
||||
// Convenience overload - gets the current task handle and delegates
|
||||
const char *HOT get_thread_name_() { return this->get_thread_name_(xTaskGetCurrentTaskHandle()); }
|
||||
|
||||
#elif defined(USE_HOST)
|
||||
// Takes a caller-provided buffer for the thread name (stack-allocated for thread safety)
|
||||
const char *HOT get_thread_name_(std::span<char> buff) {
|
||||
pthread_t current_thread = pthread_self();
|
||||
if (pthread_equal(current_thread, main_thread_)) {
|
||||
return nullptr; // Main thread
|
||||
}
|
||||
// For non-main threads, get the thread name into the caller-provided buffer
|
||||
if (pthread_getname_np(current_thread, buff.data(), buff.size()) == 0) {
|
||||
return buff.data();
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
#elif defined(USE_ZEPHYR)
|
||||
const char *HOT get_thread_name_(std::span<char> buff) {
|
||||
k_tid_t current_task = k_current_get();
|
||||
#else
|
||||
TaskHandle_t current_task = xTaskGetCurrentTaskHandle();
|
||||
#endif
|
||||
if (current_task == main_task_) {
|
||||
return nullptr; // Main task
|
||||
} else {
|
||||
#if defined(USE_ESP32)
|
||||
return pcTaskGetName(current_task);
|
||||
#elif defined(USE_LIBRETINY)
|
||||
return pcTaskGetTaskName(current_task);
|
||||
#elif defined(USE_ZEPHYR)
|
||||
const char *name = k_thread_name_get(current_task);
|
||||
if (name) {
|
||||
// zephyr print task names only if debug component is present
|
||||
return name;
|
||||
}
|
||||
std::snprintf(buff, MAX_POINTER_REPRESENTATION, "%p", current_task);
|
||||
return buff;
|
||||
#endif
|
||||
}
|
||||
const char *name = k_thread_name_get(current_task);
|
||||
if (name) {
|
||||
// zephyr print task names only if debug component is present
|
||||
return name;
|
||||
}
|
||||
std::snprintf(buff.data(), buff.size(), "%p", current_task);
|
||||
return buff.data();
|
||||
}
|
||||
#endif
|
||||
|
||||
// --- Non-main task recursion guards (per-platform) ---
|
||||
|
||||
#if defined(USE_ESP32) || defined(USE_HOST)
|
||||
// RAII guard for non-main task recursion using pthread TLS
|
||||
class NonMainTaskRecursionGuard {
|
||||
@@ -635,22 +651,6 @@ class Logger : public Component {
|
||||
inline RecursionGuard make_non_main_task_guard_() { return RecursionGuard(non_main_task_recursion_guard_); }
|
||||
#endif
|
||||
|
||||
#ifdef USE_HOST
|
||||
const char *HOT get_thread_name_() {
|
||||
pthread_t current_thread = pthread_self();
|
||||
if (pthread_equal(current_thread, main_thread_)) {
|
||||
return nullptr; // Main thread
|
||||
}
|
||||
// For non-main threads, return the thread name
|
||||
// We store it in thread-local storage to avoid allocation
|
||||
static thread_local char thread_name_buf[32];
|
||||
if (pthread_getname_np(current_thread, thread_name_buf, sizeof(thread_name_buf)) == 0) {
|
||||
return thread_name_buf;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
#endif
|
||||
|
||||
#if defined(USE_ESP32) || defined(USE_LIBRETINY)
|
||||
// Disable loop when task buffer is empty (with USB CDC check on ESP32)
|
||||
inline void disable_loop_when_buffer_empty_() {
|
||||
|
||||
@@ -59,7 +59,7 @@ void TaskLogBuffer::release_message_main_loop(void *token) {
|
||||
last_processed_counter_ = message_counter_.load(std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
bool TaskLogBuffer::send_message_thread_safe(uint8_t level, const char *tag, uint16_t line, TaskHandle_t task_handle,
|
||||
bool TaskLogBuffer::send_message_thread_safe(uint8_t level, const char *tag, uint16_t line, const char *thread_name,
|
||||
const char *format, va_list args) {
|
||||
// First, calculate the exact length needed using a null buffer (no actual writing)
|
||||
va_list args_copy;
|
||||
@@ -95,7 +95,6 @@ bool TaskLogBuffer::send_message_thread_safe(uint8_t level, const char *tag, uin
|
||||
// Store the thread name now instead of waiting until main loop processing
|
||||
// This avoids crashes if the task completes or is deleted between when this message
|
||||
// is enqueued and when it's processed by the main loop
|
||||
const char *thread_name = pcTaskGetName(task_handle);
|
||||
if (thread_name != nullptr) {
|
||||
strncpy(msg->thread_name, thread_name, sizeof(msg->thread_name) - 1);
|
||||
msg->thread_name[sizeof(msg->thread_name) - 1] = '\0'; // Ensure null termination
|
||||
|
||||
@@ -58,7 +58,7 @@ class TaskLogBuffer {
|
||||
void release_message_main_loop(void *token);
|
||||
|
||||
// Thread-safe - send a message to the ring buffer from any thread
|
||||
bool send_message_thread_safe(uint8_t level, const char *tag, uint16_t line, TaskHandle_t task_handle,
|
||||
bool send_message_thread_safe(uint8_t level, const char *tag, uint16_t line, const char *thread_name,
|
||||
const char *format, va_list args);
|
||||
|
||||
// Check if there are messages ready to be processed using an atomic counter for performance
|
||||
|
||||
@@ -70,8 +70,8 @@ void TaskLogBufferHost::commit_write_slot_(int slot_index) {
|
||||
}
|
||||
}
|
||||
|
||||
bool TaskLogBufferHost::send_message_thread_safe(uint8_t level, const char *tag, uint16_t line, const char *format,
|
||||
va_list args) {
|
||||
bool TaskLogBufferHost::send_message_thread_safe(uint8_t level, const char *tag, uint16_t line, const char *thread_name,
|
||||
const char *format, va_list args) {
|
||||
// Acquire a slot
|
||||
int slot_index = this->acquire_write_slot_();
|
||||
if (slot_index < 0) {
|
||||
@@ -85,11 +85,9 @@ bool TaskLogBufferHost::send_message_thread_safe(uint8_t level, const char *tag,
|
||||
msg.tag = tag;
|
||||
msg.line = line;
|
||||
|
||||
// Get thread name using pthread
|
||||
char thread_name_buf[LogMessage::MAX_THREAD_NAME_SIZE];
|
||||
// pthread_getname_np works the same on Linux and macOS
|
||||
if (pthread_getname_np(pthread_self(), thread_name_buf, sizeof(thread_name_buf)) == 0) {
|
||||
strncpy(msg.thread_name, thread_name_buf, sizeof(msg.thread_name) - 1);
|
||||
// Store the thread name now to avoid crashes if thread exits before processing
|
||||
if (thread_name != nullptr) {
|
||||
strncpy(msg.thread_name, thread_name, sizeof(msg.thread_name) - 1);
|
||||
msg.thread_name[sizeof(msg.thread_name) - 1] = '\0';
|
||||
} else {
|
||||
msg.thread_name[0] = '\0';
|
||||
|
||||
@@ -86,7 +86,8 @@ class TaskLogBufferHost {
|
||||
|
||||
// Thread-safe - send a message to the buffer from any thread
|
||||
// Returns true if message was queued, false if buffer is full
|
||||
bool send_message_thread_safe(uint8_t level, const char *tag, uint16_t line, const char *format, va_list args);
|
||||
bool send_message_thread_safe(uint8_t level, const char *tag, uint16_t line, const char *thread_name,
|
||||
const char *format, va_list args);
|
||||
|
||||
// Check if there are messages ready to be processed
|
||||
inline bool HOT has_messages() const {
|
||||
|
||||
@@ -101,7 +101,7 @@ void TaskLogBufferLibreTiny::release_message_main_loop() {
|
||||
}
|
||||
|
||||
bool TaskLogBufferLibreTiny::send_message_thread_safe(uint8_t level, const char *tag, uint16_t line,
|
||||
TaskHandle_t task_handle, const char *format, va_list args) {
|
||||
const char *thread_name, const char *format, va_list args) {
|
||||
// First, calculate the exact length needed using a null buffer (no actual writing)
|
||||
va_list args_copy;
|
||||
va_copy(args_copy, args);
|
||||
@@ -162,7 +162,6 @@ bool TaskLogBufferLibreTiny::send_message_thread_safe(uint8_t level, const char
|
||||
msg->line = line;
|
||||
|
||||
// Store the thread name now to avoid crashes if task is deleted before processing
|
||||
const char *thread_name = pcTaskGetTaskName(task_handle);
|
||||
if (thread_name != nullptr) {
|
||||
strncpy(msg->thread_name, thread_name, sizeof(msg->thread_name) - 1);
|
||||
msg->thread_name[sizeof(msg->thread_name) - 1] = '\0';
|
||||
|
||||
@@ -70,7 +70,7 @@ class TaskLogBufferLibreTiny {
|
||||
void release_message_main_loop();
|
||||
|
||||
// Thread-safe - send a message to the buffer from any thread
|
||||
bool send_message_thread_safe(uint8_t level, const char *tag, uint16_t line, TaskHandle_t task_handle,
|
||||
bool send_message_thread_safe(uint8_t level, const char *tag, uint16_t line, const char *thread_name,
|
||||
const char *format, va_list args);
|
||||
|
||||
// Fast check using volatile counter - no lock needed
|
||||
|
||||
@@ -120,3 +120,101 @@ DriverChip(
|
||||
(0xB2, 0x10),
|
||||
],
|
||||
)
|
||||
|
||||
DriverChip(
|
||||
"WAVESHARE-ESP32-P4-WIFI6-TOUCH-LCD-3.4C",
|
||||
height=800,
|
||||
width=800,
|
||||
hsync_back_porch=20,
|
||||
hsync_pulse_width=20,
|
||||
hsync_front_porch=40,
|
||||
vsync_back_porch=12,
|
||||
vsync_pulse_width=4,
|
||||
vsync_front_porch=24,
|
||||
pclk_frequency="80MHz",
|
||||
lane_bit_rate="1.5Gbps",
|
||||
swap_xy=cv.UNDEFINED,
|
||||
color_order="RGB",
|
||||
initsequence=[
|
||||
(0xE0, 0x00), # select userpage
|
||||
(0xE1, 0x93), (0xE2, 0x65), (0xE3, 0xF8),
|
||||
(0x80, 0x01), # Select number of lanes (2)
|
||||
(0xE0, 0x01), # select page 1
|
||||
(0x00, 0x00), (0x01, 0x41), (0x03, 0x10), (0x04, 0x44), (0x17, 0x00), (0x18, 0xD0), (0x19, 0x00), (0x1A, 0x00),
|
||||
(0x1B, 0xD0), (0x1C, 0x00), (0x24, 0xFE), (0x35, 0x26), (0x37, 0x09), (0x38, 0x04), (0x39, 0x08), (0x3A, 0x0A),
|
||||
(0x3C, 0x78), (0x3D, 0xFF), (0x3E, 0xFF), (0x3F, 0xFF), (0x40, 0x00), (0x41, 0x64), (0x42, 0xC7), (0x43, 0x18),
|
||||
(0x44, 0x0B), (0x45, 0x14), (0x55, 0x02), (0x57, 0x49), (0x59, 0x0A), (0x5A, 0x1B), (0x5B, 0x19), (0x5D, 0x7F),
|
||||
(0x5E, 0x56), (0x5F, 0x43), (0x60, 0x37), (0x61, 0x33), (0x62, 0x25), (0x63, 0x2A), (0x64, 0x16), (0x65, 0x30),
|
||||
(0x66, 0x2F), (0x67, 0x32), (0x68, 0x53), (0x69, 0x43), (0x6A, 0x4C), (0x6B, 0x40), (0x6C, 0x3D), (0x6D, 0x31),
|
||||
(0x6E, 0x20), (0x6F, 0x0F), (0x70, 0x7F), (0x71, 0x56), (0x72, 0x43), (0x73, 0x37), (0x74, 0x33), (0x75, 0x25),
|
||||
(0x76, 0x2A), (0x77, 0x16), (0x78, 0x30), (0x79, 0x2F), (0x7A, 0x32), (0x7B, 0x53), (0x7C, 0x43), (0x7D, 0x4C),
|
||||
(0x7E, 0x40), (0x7F, 0x3D), (0x80, 0x31), (0x81, 0x20), (0x82, 0x0F),
|
||||
(0xE0, 0x02), # select page 2
|
||||
(0x00, 0x5F), (0x01, 0x5F), (0x02, 0x5E), (0x03, 0x5E), (0x04, 0x50), (0x05, 0x48), (0x06, 0x48), (0x07, 0x4A),
|
||||
(0x08, 0x4A), (0x09, 0x44), (0x0A, 0x44), (0x0B, 0x46), (0x0C, 0x46), (0x0D, 0x5F), (0x0E, 0x5F), (0x0F, 0x57),
|
||||
(0x10, 0x57), (0x11, 0x77), (0x12, 0x77), (0x13, 0x40), (0x14, 0x42), (0x15, 0x5F), (0x16, 0x5F), (0x17, 0x5F),
|
||||
(0x18, 0x5E), (0x19, 0x5E), (0x1A, 0x50), (0x1B, 0x49), (0x1C, 0x49), (0x1D, 0x4B), (0x1E, 0x4B), (0x1F, 0x45),
|
||||
(0x20, 0x45), (0x21, 0x47), (0x22, 0x47), (0x23, 0x5F), (0x24, 0x5F), (0x25, 0x57), (0x26, 0x57), (0x27, 0x77),
|
||||
(0x28, 0x77), (0x29, 0x41), (0x2A, 0x43), (0x2B, 0x5F), (0x2C, 0x1E), (0x2D, 0x1E), (0x2E, 0x1F), (0x2F, 0x1F),
|
||||
(0x30, 0x10), (0x31, 0x07), (0x32, 0x07), (0x33, 0x05), (0x34, 0x05), (0x35, 0x0B), (0x36, 0x0B), (0x37, 0x09),
|
||||
(0x38, 0x09), (0x39, 0x1F), (0x3A, 0x1F), (0x3B, 0x17), (0x3C, 0x17), (0x3D, 0x17), (0x3E, 0x17), (0x3F, 0x03),
|
||||
(0x40, 0x01), (0x41, 0x1F), (0x42, 0x1E), (0x43, 0x1E), (0x44, 0x1F), (0x45, 0x1F), (0x46, 0x10), (0x47, 0x06),
|
||||
(0x48, 0x06), (0x49, 0x04), (0x4A, 0x04), (0x4B, 0x0A), (0x4C, 0x0A), (0x4D, 0x08), (0x4E, 0x08), (0x4F, 0x1F),
|
||||
(0x50, 0x1F), (0x51, 0x17), (0x52, 0x17), (0x53, 0x17), (0x54, 0x17), (0x55, 0x02), (0x56, 0x00), (0x57, 0x1F),
|
||||
(0xE0, 0x02), # select page 2
|
||||
(0x58, 0x40), (0x59, 0x00), (0x5A, 0x00), (0x5B, 0x30), (0x5C, 0x01), (0x5D, 0x30), (0x5E, 0x01), (0x5F, 0x02),
|
||||
(0x60, 0x30), (0x61, 0x03), (0x62, 0x04), (0x63, 0x04), (0x64, 0xA6), (0x65, 0x43), (0x66, 0x30), (0x67, 0x73),
|
||||
(0x68, 0x05), (0x69, 0x04), (0x6A, 0x7F), (0x6B, 0x08), (0x6C, 0x00), (0x6D, 0x04), (0x6E, 0x04), (0x6F, 0x88),
|
||||
(0x75, 0xD9), (0x76, 0x00), (0x77, 0x33), (0x78, 0x43),
|
||||
(0xE0, 0x00), # select userpage
|
||||
],
|
||||
)
|
||||
|
||||
DriverChip(
|
||||
"WAVESHARE-ESP32-P4-WIFI6-TOUCH-LCD-4C",
|
||||
height=720,
|
||||
width=720,
|
||||
hsync_back_porch=20,
|
||||
hsync_pulse_width=20,
|
||||
hsync_front_porch=40,
|
||||
vsync_back_porch=12,
|
||||
vsync_pulse_width=4,
|
||||
vsync_front_porch=24,
|
||||
pclk_frequency="80MHz",
|
||||
lane_bit_rate="1.5Gbps",
|
||||
swap_xy=cv.UNDEFINED,
|
||||
color_order="RGB",
|
||||
initsequence=[
|
||||
(0xE0, 0x00), # select userpage
|
||||
(0xE1, 0x93), (0xE2, 0x65), (0xE3, 0xF8),
|
||||
(0x80, 0x01), # Select number of lanes (2)
|
||||
(0xE0, 0x01), # select page 1
|
||||
(0x00, 0x00), (0x01, 0x41), (0x03, 0x10), (0x04, 0x44), (0x17, 0x00), (0x18, 0xD0), (0x19, 0x00), (0x1A, 0x00),
|
||||
(0x1B, 0xD0), (0x1C, 0x00), (0x24, 0xFE), (0x35, 0x26), (0x37, 0x09), (0x38, 0x04), (0x39, 0x08), (0x3A, 0x0A),
|
||||
(0x3C, 0x78), (0x3D, 0xFF), (0x3E, 0xFF), (0x3F, 0xFF), (0x40, 0x04), (0x41, 0x64), (0x42, 0xC7), (0x43, 0x18),
|
||||
(0x44, 0x0B), (0x45, 0x14), (0x55, 0x02), (0x57, 0x49), (0x59, 0x0A), (0x5A, 0x1B), (0x5B, 0x19), (0x5D, 0x7F),
|
||||
(0x5E, 0x56), (0x5F, 0x43), (0x60, 0x37), (0x61, 0x33), (0x62, 0x25), (0x63, 0x2A), (0x64, 0x16), (0x65, 0x30),
|
||||
(0x66, 0x2F), (0x67, 0x32), (0x68, 0x53), (0x69, 0x43), (0x6A, 0x4C), (0x6B, 0x40), (0x6C, 0x3D), (0x6D, 0x31),
|
||||
(0x6E, 0x20), (0x6F, 0x0F), (0x70, 0x7F), (0x71, 0x56), (0x72, 0x43), (0x73, 0x37), (0x74, 0x33), (0x75, 0x25),
|
||||
(0x76, 0x2A), (0x77, 0x16), (0x78, 0x30), (0x79, 0x2F), (0x7A, 0x32), (0x7B, 0x53), (0x7C, 0x43), (0x7D, 0x4C),
|
||||
(0x7E, 0x40), (0x7F, 0x3D), (0x80, 0x31), (0x81, 0x20), (0x82, 0x0F),
|
||||
(0xE0, 0x02), # select page 2
|
||||
(0x00, 0x5F), (0x01, 0x5F), (0x02, 0x5E), (0x03, 0x5E), (0x04, 0x50), (0x05, 0x48), (0x06, 0x48), (0x07, 0x4A),
|
||||
(0x08, 0x4A), (0x09, 0x44), (0x0A, 0x44), (0x0B, 0x46), (0x0C, 0x46), (0x0D, 0x5F), (0x0E, 0x5F), (0x0F, 0x57),
|
||||
(0x10, 0x57), (0x11, 0x77), (0x12, 0x77), (0x13, 0x40), (0x14, 0x42), (0x15, 0x5F), (0x16, 0x5F), (0x17, 0x5F),
|
||||
(0x18, 0x5E), (0x19, 0x5E), (0x1A, 0x50), (0x1B, 0x49), (0x1C, 0x49), (0x1D, 0x4B), (0x1E, 0x4B), (0x1F, 0x45),
|
||||
(0x20, 0x45), (0x21, 0x47), (0x22, 0x47), (0x23, 0x5F), (0x24, 0x5F), (0x25, 0x57), (0x26, 0x57), (0x27, 0x77),
|
||||
(0x28, 0x77), (0x29, 0x41), (0x2A, 0x43), (0x2B, 0x5F), (0x2C, 0x1E), (0x2D, 0x1E), (0x2E, 0x1F), (0x2F, 0x1F),
|
||||
(0x30, 0x10), (0x31, 0x07), (0x32, 0x07), (0x33, 0x05), (0x34, 0x05), (0x35, 0x0B), (0x36, 0x0B), (0x37, 0x09),
|
||||
(0x38, 0x09), (0x39, 0x1F), (0x3A, 0x1F), (0x3B, 0x17), (0x3C, 0x17), (0x3D, 0x17), (0x3E, 0x17), (0x3F, 0x03),
|
||||
(0x40, 0x01), (0x41, 0x1F), (0x42, 0x1E), (0x43, 0x1E), (0x44, 0x1F), (0x45, 0x1F), (0x46, 0x10), (0x47, 0x06),
|
||||
(0x48, 0x06), (0x49, 0x04), (0x4A, 0x04), (0x4B, 0x0A), (0x4C, 0x0A), (0x4D, 0x08), (0x4E, 0x08), (0x4F, 0x1F),
|
||||
(0x50, 0x1F), (0x51, 0x17), (0x52, 0x17), (0x53, 0x17), (0x54, 0x17), (0x55, 0x02), (0x56, 0x00), (0x57, 0x1F),
|
||||
(0xE0, 0x02), # select page 2
|
||||
(0x58, 0x40), (0x59, 0x00), (0x5A, 0x00), (0x5B, 0x30), (0x5C, 0x01), (0x5D, 0x30), (0x5E, 0x01), (0x5F, 0x02),
|
||||
(0x60, 0x30), (0x61, 0x03), (0x62, 0x04), (0x63, 0x04), (0x64, 0xA6), (0x65, 0x43), (0x66, 0x30), (0x67, 0x73),
|
||||
(0x68, 0x05), (0x69, 0x04), (0x6A, 0x7F), (0x6B, 0x08), (0x6C, 0x00), (0x6D, 0x04), (0x6E, 0x04), (0x6F, 0x88),
|
||||
(0x75, 0xD9), (0x76, 0x00), (0x77, 0x33), (0x78, 0x43),
|
||||
(0xE0, 0x00), # select userpage
|
||||
]
|
||||
)
|
||||
|
||||
@@ -11,7 +11,7 @@ from esphome.components.const import (
|
||||
CONF_DRAW_ROUNDING,
|
||||
)
|
||||
from esphome.components.display import CONF_SHOW_TEST_CARD
|
||||
from esphome.components.esp32 import VARIANT_ESP32S3, only_on_variant
|
||||
from esphome.components.esp32 import VARIANT_ESP32P4, VARIANT_ESP32S3, only_on_variant
|
||||
from esphome.components.mipi import (
|
||||
COLOR_ORDERS,
|
||||
CONF_DE_PIN,
|
||||
@@ -225,7 +225,7 @@ def _config_schema(config):
|
||||
return cv.All(
|
||||
schema,
|
||||
cv.only_on_esp32,
|
||||
only_on_variant(supported=[VARIANT_ESP32S3]),
|
||||
only_on_variant(supported=[VARIANT_ESP32S3, VARIANT_ESP32P4]),
|
||||
)(config)
|
||||
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#ifdef USE_ESP32_VARIANT_ESP32S3
|
||||
#if defined(USE_ESP32_VARIANT_ESP32S3) || defined(USE_ESP32_VARIANT_ESP32P4)
|
||||
#include "mipi_rgb.h"
|
||||
#include "esphome/core/gpio.h"
|
||||
#include "esphome/core/hal.h"
|
||||
@@ -401,4 +401,4 @@ void MipiRgb::dump_config() {
|
||||
|
||||
} // namespace mipi_rgb
|
||||
} // namespace esphome
|
||||
#endif // USE_ESP32_VARIANT_ESP32S3
|
||||
#endif // defined(USE_ESP32_VARIANT_ESP32S3) || defined(USE_ESP32_VARIANT_ESP32P4)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#pragma once
|
||||
|
||||
#ifdef USE_ESP32_VARIANT_ESP32S3
|
||||
#if defined(USE_ESP32_VARIANT_ESP32S3) || defined(USE_ESP32_VARIANT_ESP32P4)
|
||||
#include "esphome/core/gpio.h"
|
||||
#include "esphome/components/display/display.h"
|
||||
#include "esp_lcd_panel_ops.h"
|
||||
@@ -28,7 +28,7 @@ class MipiRgb : public display::Display {
|
||||
void setup() override;
|
||||
void loop() override;
|
||||
void update() override;
|
||||
void fill(Color color);
|
||||
void fill(Color color) override;
|
||||
void draw_pixels_at(int x_start, int y_start, int w, int h, const uint8_t *ptr, display::ColorOrder order,
|
||||
display::ColorBitness bitness, bool big_endian, int x_offset, int y_offset, int x_pad) override;
|
||||
void write_to_display_(int x_start, int y_start, int w, int h, const uint8_t *ptr, int x_offset, int y_offset,
|
||||
@@ -115,7 +115,7 @@ class MipiRgbSpi : public MipiRgb,
|
||||
void write_command_(uint8_t value);
|
||||
void write_data_(uint8_t value);
|
||||
void write_init_sequence_();
|
||||
void dump_config();
|
||||
void dump_config() override;
|
||||
|
||||
GPIOPin *dc_pin_{nullptr};
|
||||
std::vector<uint8_t> init_sequence_;
|
||||
|
||||
@@ -20,10 +20,10 @@ void Modbus::loop() {
|
||||
const uint32_t now = App.get_loop_component_start_time();
|
||||
|
||||
// Read all available bytes in batches to reduce UART call overhead.
|
||||
int avail = this->available();
|
||||
size_t avail = this->available();
|
||||
uint8_t buf[64];
|
||||
while (avail > 0) {
|
||||
size_t to_read = std::min(static_cast<size_t>(avail), sizeof(buf));
|
||||
size_t to_read = std::min(avail, sizeof(buf));
|
||||
if (!this->read_array(buf, to_read)) {
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -398,10 +398,10 @@ bool Nextion::remove_from_q_(bool report_empty) {
|
||||
|
||||
void Nextion::process_serial_() {
|
||||
// Read all available bytes in batches to reduce UART call overhead.
|
||||
int avail = this->available();
|
||||
size_t avail = this->available();
|
||||
uint8_t buf[64];
|
||||
while (avail > 0) {
|
||||
size_t to_read = std::min(static_cast<size_t>(avail), sizeof(buf));
|
||||
size_t to_read = std::min(avail, sizeof(buf));
|
||||
if (!this->read_array(buf, to_read)) {
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -14,9 +14,9 @@ void Pipsolar::setup() {
|
||||
|
||||
void Pipsolar::empty_uart_buffer_() {
|
||||
uint8_t buf[64];
|
||||
int avail;
|
||||
size_t avail;
|
||||
while ((avail = this->available()) > 0) {
|
||||
if (!this->read_array(buf, std::min(static_cast<size_t>(avail), sizeof(buf)))) {
|
||||
if (!this->read_array(buf, std::min(avail, sizeof(buf)))) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -97,10 +97,10 @@ void Pipsolar::loop() {
|
||||
}
|
||||
|
||||
if (this->state_ == STATE_COMMAND || this->state_ == STATE_POLL) {
|
||||
int avail = this->available();
|
||||
size_t avail = this->available();
|
||||
while (avail > 0) {
|
||||
uint8_t buf[64];
|
||||
size_t to_read = std::min(static_cast<size_t>(avail), sizeof(buf));
|
||||
size_t to_read = std::min(avail, sizeof(buf));
|
||||
if (!this->read_array(buf, to_read)) {
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -56,17 +56,23 @@ void PylontechComponent::setup() {
|
||||
void PylontechComponent::update() { this->write_str("pwr\n"); }
|
||||
|
||||
void PylontechComponent::loop() {
|
||||
if (this->available() > 0) {
|
||||
size_t avail = this->available();
|
||||
if (avail > 0) {
|
||||
// pylontech sends a lot of data very suddenly
|
||||
// we need to quickly put it all into our own buffer, otherwise the uart's buffer will overflow
|
||||
uint8_t data;
|
||||
int recv = 0;
|
||||
while (this->available() > 0) {
|
||||
if (this->read_byte(&data)) {
|
||||
buffer_[buffer_index_write_] += (char) data;
|
||||
recv++;
|
||||
if (buffer_[buffer_index_write_].back() == static_cast<char>(ASCII_LF) ||
|
||||
buffer_[buffer_index_write_].length() >= MAX_DATA_LENGTH_BYTES) {
|
||||
uint8_t buf[64];
|
||||
while (avail > 0) {
|
||||
size_t to_read = std::min(avail, sizeof(buf));
|
||||
if (!this->read_array(buf, to_read)) {
|
||||
break;
|
||||
}
|
||||
avail -= to_read;
|
||||
recv += to_read;
|
||||
|
||||
for (size_t i = 0; i < to_read; i++) {
|
||||
buffer_[buffer_index_write_] += (char) buf[i];
|
||||
if (buf[i] == ASCII_LF || buffer_[buffer_index_write_].length() >= MAX_DATA_LENGTH_BYTES) {
|
||||
// complete line received
|
||||
buffer_index_write_ = (buffer_index_write_ + 1) % NUM_BUFFERS;
|
||||
}
|
||||
|
||||
@@ -82,10 +82,10 @@ void RD03DComponent::dump_config() {
|
||||
|
||||
void RD03DComponent::loop() {
|
||||
// Read all available bytes in batches to reduce UART call overhead.
|
||||
int avail = this->available();
|
||||
size_t avail = this->available();
|
||||
uint8_t buf[64];
|
||||
while (avail > 0) {
|
||||
size_t to_read = std::min(static_cast<size_t>(avail), sizeof(buf));
|
||||
size_t to_read = std::min(avail, sizeof(buf));
|
||||
if (!this->read_array(buf, to_read)) {
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -136,10 +136,10 @@ void RFBridgeComponent::loop() {
|
||||
this->last_bridge_byte_ = now;
|
||||
}
|
||||
|
||||
int avail = this->available();
|
||||
size_t avail = this->available();
|
||||
while (avail > 0) {
|
||||
uint8_t buf[64];
|
||||
size_t to_read = std::min(static_cast<size_t>(avail), sizeof(buf));
|
||||
size_t to_read = std::min(avail, sizeof(buf));
|
||||
if (!this->read_array(buf, to_read)) {
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -107,10 +107,10 @@ void MR24HPC1Component::update_() {
|
||||
// main loop
|
||||
void MR24HPC1Component::loop() {
|
||||
// Read all available bytes in batches to reduce UART call overhead.
|
||||
int avail = this->available();
|
||||
size_t avail = this->available();
|
||||
uint8_t buf[64];
|
||||
while (avail > 0) {
|
||||
size_t to_read = std::min(static_cast<size_t>(avail), sizeof(buf));
|
||||
size_t to_read = std::min(avail, sizeof(buf));
|
||||
if (!this->read_array(buf, to_read)) {
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -31,10 +31,10 @@ void MR60BHA2Component::dump_config() {
|
||||
// main loop
|
||||
void MR60BHA2Component::loop() {
|
||||
// Read all available bytes in batches to reduce UART call overhead.
|
||||
int avail = this->available();
|
||||
size_t avail = this->available();
|
||||
uint8_t buf[64];
|
||||
while (avail > 0) {
|
||||
size_t to_read = std::min(static_cast<size_t>(avail), sizeof(buf));
|
||||
size_t to_read = std::min(avail, sizeof(buf));
|
||||
if (!this->read_array(buf, to_read)) {
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -50,10 +50,10 @@ void MR60FDA2Component::setup() {
|
||||
// main loop
|
||||
void MR60FDA2Component::loop() {
|
||||
// Read all available bytes in batches to reduce UART call overhead.
|
||||
int avail = this->available();
|
||||
size_t avail = this->available();
|
||||
uint8_t buf[64];
|
||||
while (avail > 0) {
|
||||
size_t to_read = std::min(static_cast<size_t>(avail), sizeof(buf));
|
||||
size_t to_read = std::min(avail, sizeof(buf));
|
||||
if (!this->read_array(buf, to_read)) {
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -251,7 +251,7 @@ void Tormatic::stop_at_target_() {
|
||||
// Read a GateStatus from the unit. The unit only sends messages in response to
|
||||
// status requests or commands, so a message needs to be sent first.
|
||||
optional<GateStatus> Tormatic::read_gate_status_() {
|
||||
if (this->available() < static_cast<int>(sizeof(MessageHeader))) {
|
||||
if (this->available() < sizeof(MessageHeader)) {
|
||||
return {};
|
||||
}
|
||||
|
||||
|
||||
@@ -32,10 +32,10 @@ void Tuya::setup() {
|
||||
|
||||
void Tuya::loop() {
|
||||
// Read all available bytes in batches to reduce UART call overhead.
|
||||
int avail = this->available();
|
||||
size_t avail = this->available();
|
||||
uint8_t buf[64];
|
||||
while (avail > 0) {
|
||||
size_t to_read = std::min(static_cast<size_t>(avail), sizeof(buf));
|
||||
size_t to_read = std::min(avail, sizeof(buf));
|
||||
if (!this->read_array(buf, to_read)) {
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -3,12 +3,16 @@
|
||||
#include "esphome/core/defines.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/progmem.h"
|
||||
#include <cinttypes>
|
||||
|
||||
namespace esphome::uart {
|
||||
|
||||
static const char *const TAG = "uart";
|
||||
|
||||
// UART parity strings indexed by UARTParityOptions enum (0-2): NONE, EVEN, ODD
|
||||
PROGMEM_STRING_TABLE(UARTParityStrings, "NONE", "EVEN", "ODD", "UNKNOWN");
|
||||
|
||||
void UARTDevice::check_uart_settings(uint32_t baud_rate, uint8_t stop_bits, UARTParityOptions parity,
|
||||
uint8_t data_bits) {
|
||||
if (this->parent_->get_baud_rate() != baud_rate) {
|
||||
@@ -30,16 +34,7 @@ void UARTDevice::check_uart_settings(uint32_t baud_rate, uint8_t stop_bits, UART
|
||||
}
|
||||
|
||||
const LogString *parity_to_str(UARTParityOptions parity) {
|
||||
switch (parity) {
|
||||
case UART_CONFIG_PARITY_NONE:
|
||||
return LOG_STR("NONE");
|
||||
case UART_CONFIG_PARITY_EVEN:
|
||||
return LOG_STR("EVEN");
|
||||
case UART_CONFIG_PARITY_ODD:
|
||||
return LOG_STR("ODD");
|
||||
default:
|
||||
return LOG_STR("UNKNOWN");
|
||||
}
|
||||
return UARTParityStrings::get_log_str(static_cast<uint8_t>(parity), UARTParityStrings::LAST_INDEX);
|
||||
}
|
||||
|
||||
} // namespace esphome::uart
|
||||
|
||||
@@ -43,7 +43,7 @@ class UARTDevice {
|
||||
return res;
|
||||
}
|
||||
|
||||
int available() { return this->parent_->available(); }
|
||||
size_t available() { return this->parent_->available(); }
|
||||
|
||||
void flush() { this->parent_->flush(); }
|
||||
|
||||
|
||||
@@ -5,13 +5,13 @@ namespace esphome::uart {
|
||||
static const char *const TAG = "uart";
|
||||
|
||||
bool UARTComponent::check_read_timeout_(size_t len) {
|
||||
if (this->available() >= int(len))
|
||||
if (this->available() >= len)
|
||||
return true;
|
||||
|
||||
uint32_t start_time = millis();
|
||||
while (this->available() < int(len)) {
|
||||
while (this->available() < len) {
|
||||
if (millis() - start_time > 100) {
|
||||
ESP_LOGE(TAG, "Reading from UART timed out at byte %u!", this->available());
|
||||
ESP_LOGE(TAG, "Reading from UART timed out at byte %zu!", this->available());
|
||||
return false;
|
||||
}
|
||||
yield();
|
||||
|
||||
@@ -69,7 +69,7 @@ class UARTComponent {
|
||||
|
||||
// Pure virtual method to return the number of bytes available for reading.
|
||||
// @return Number of available bytes.
|
||||
virtual int available() = 0;
|
||||
virtual size_t available() = 0;
|
||||
|
||||
// Pure virtual method to block until all bytes have been written to the UART bus.
|
||||
virtual void flush() = 0;
|
||||
|
||||
@@ -206,7 +206,7 @@ bool ESP8266UartComponent::read_array(uint8_t *data, size_t len) {
|
||||
#endif
|
||||
return true;
|
||||
}
|
||||
int ESP8266UartComponent::available() {
|
||||
size_t ESP8266UartComponent::available() {
|
||||
if (this->hw_serial_ != nullptr) {
|
||||
return this->hw_serial_->available();
|
||||
} else {
|
||||
@@ -329,11 +329,14 @@ uint8_t ESP8266SoftwareSerial::peek_byte() {
|
||||
void ESP8266SoftwareSerial::flush() {
|
||||
// Flush is a NO-OP with software serial, all bytes are written immediately.
|
||||
}
|
||||
int ESP8266SoftwareSerial::available() {
|
||||
int avail = int(this->rx_in_pos_) - int(this->rx_out_pos_);
|
||||
if (avail < 0)
|
||||
return avail + this->rx_buffer_size_;
|
||||
return avail;
|
||||
size_t ESP8266SoftwareSerial::available() {
|
||||
// Read volatile rx_in_pos_ once to avoid TOCTOU race with ISR.
|
||||
// When in >= out, data is contiguous: [out..in).
|
||||
// When in < out, data wraps: [out..buf_size) + [0..in).
|
||||
size_t in = this->rx_in_pos_;
|
||||
if (in >= this->rx_out_pos_)
|
||||
return in - this->rx_out_pos_;
|
||||
return this->rx_buffer_size_ - this->rx_out_pos_ + in;
|
||||
}
|
||||
|
||||
} // namespace esphome::uart
|
||||
|
||||
@@ -23,7 +23,7 @@ class ESP8266SoftwareSerial {
|
||||
|
||||
void write_byte(uint8_t data);
|
||||
|
||||
int available();
|
||||
size_t available();
|
||||
|
||||
protected:
|
||||
static void gpio_intr(ESP8266SoftwareSerial *arg);
|
||||
@@ -57,7 +57,7 @@ class ESP8266UartComponent : public UARTComponent, public Component {
|
||||
bool peek_byte(uint8_t *data) override;
|
||||
bool read_array(uint8_t *data, size_t len) override;
|
||||
|
||||
int available() override;
|
||||
size_t available() override;
|
||||
void flush() override;
|
||||
|
||||
uint32_t get_config();
|
||||
|
||||
@@ -338,7 +338,7 @@ bool IDFUARTComponent::read_array(uint8_t *data, size_t len) {
|
||||
return read_len == (int32_t) length_to_read;
|
||||
}
|
||||
|
||||
int IDFUARTComponent::available() {
|
||||
size_t IDFUARTComponent::available() {
|
||||
size_t available = 0;
|
||||
esp_err_t err;
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ class IDFUARTComponent : public UARTComponent, public Component {
|
||||
bool peek_byte(uint8_t *data) override;
|
||||
bool read_array(uint8_t *data, size_t len) override;
|
||||
|
||||
int available() override;
|
||||
size_t available() override;
|
||||
void flush() override;
|
||||
|
||||
uint8_t get_hw_serial_number() { return this->uart_num_; }
|
||||
|
||||
@@ -265,7 +265,7 @@ bool HostUartComponent::read_array(uint8_t *data, size_t len) {
|
||||
return true;
|
||||
}
|
||||
|
||||
int HostUartComponent::available() {
|
||||
size_t HostUartComponent::available() {
|
||||
if (this->file_descriptor_ == -1) {
|
||||
return 0;
|
||||
}
|
||||
@@ -275,9 +275,10 @@ int HostUartComponent::available() {
|
||||
this->update_error_(strerror(errno));
|
||||
return 0;
|
||||
}
|
||||
size_t result = available;
|
||||
if (this->has_peek_)
|
||||
available++;
|
||||
return available;
|
||||
result++;
|
||||
return result;
|
||||
};
|
||||
|
||||
void HostUartComponent::flush() {
|
||||
|
||||
@@ -17,7 +17,7 @@ class HostUartComponent : public UARTComponent, public Component {
|
||||
void write_array(const uint8_t *data, size_t len) override;
|
||||
bool peek_byte(uint8_t *data) override;
|
||||
bool read_array(uint8_t *data, size_t len) override;
|
||||
int available() override;
|
||||
size_t available() override;
|
||||
void flush() override;
|
||||
void set_name(std::string port_name) { port_name_ = port_name; };
|
||||
|
||||
|
||||
@@ -169,7 +169,7 @@ bool LibreTinyUARTComponent::read_array(uint8_t *data, size_t len) {
|
||||
return true;
|
||||
}
|
||||
|
||||
int LibreTinyUARTComponent::available() { return this->serial_->available(); }
|
||||
size_t LibreTinyUARTComponent::available() { return this->serial_->available(); }
|
||||
void LibreTinyUARTComponent::flush() {
|
||||
ESP_LOGVV(TAG, " Flushing");
|
||||
this->serial_->flush();
|
||||
|
||||
@@ -21,7 +21,7 @@ class LibreTinyUARTComponent : public UARTComponent, public Component {
|
||||
bool peek_byte(uint8_t *data) override;
|
||||
bool read_array(uint8_t *data, size_t len) override;
|
||||
|
||||
int available() override;
|
||||
size_t available() override;
|
||||
void flush() override;
|
||||
|
||||
uint16_t get_config();
|
||||
|
||||
@@ -186,7 +186,7 @@ bool RP2040UartComponent::read_array(uint8_t *data, size_t len) {
|
||||
#endif
|
||||
return true;
|
||||
}
|
||||
int RP2040UartComponent::available() { return this->serial_->available(); }
|
||||
size_t RP2040UartComponent::available() { return this->serial_->available(); }
|
||||
void RP2040UartComponent::flush() {
|
||||
ESP_LOGVV(TAG, " Flushing");
|
||||
this->serial_->flush();
|
||||
|
||||
@@ -24,7 +24,7 @@ class RP2040UartComponent : public UARTComponent, public Component {
|
||||
bool peek_byte(uint8_t *data) override;
|
||||
bool read_array(uint8_t *data, size_t len) override;
|
||||
|
||||
int available() override;
|
||||
size_t available() override;
|
||||
void flush() override;
|
||||
|
||||
uint16_t get_config();
|
||||
|
||||
@@ -81,7 +81,7 @@ class USBCDCACMInstance : public uart::UARTComponent, public Parented<USBCDCACMC
|
||||
void write_array(const uint8_t *data, size_t len) override;
|
||||
bool peek_byte(uint8_t *data) override;
|
||||
bool read_array(uint8_t *data, size_t len) override;
|
||||
int available() override;
|
||||
size_t available() override;
|
||||
void flush() override;
|
||||
|
||||
protected:
|
||||
|
||||
@@ -318,12 +318,12 @@ bool USBCDCACMInstance::read_array(uint8_t *data, size_t len) {
|
||||
return bytes_read == original_len;
|
||||
}
|
||||
|
||||
int USBCDCACMInstance::available() {
|
||||
size_t USBCDCACMInstance::available() {
|
||||
UBaseType_t waiting = 0;
|
||||
if (this->usb_rx_ringbuf_ != nullptr) {
|
||||
vRingbufferGetInfo(this->usb_rx_ringbuf_, nullptr, nullptr, nullptr, nullptr, &waiting);
|
||||
}
|
||||
return static_cast<int>(waiting) + (this->has_peek_ ? 1 : 0);
|
||||
return waiting + (this->has_peek_ ? 1 : 0);
|
||||
}
|
||||
|
||||
void USBCDCACMInstance::flush() {
|
||||
|
||||
@@ -97,7 +97,7 @@ class USBUartChannel : public uart::UARTComponent, public Parented<USBUartCompon
|
||||
bool peek_byte(uint8_t *data) override;
|
||||
;
|
||||
bool read_array(uint8_t *data, size_t len) override;
|
||||
int available() override { return static_cast<int>(this->input_buffer_.get_available()); }
|
||||
size_t available() override { return this->input_buffer_.get_available(); }
|
||||
void flush() override {}
|
||||
void check_logger_conflict() override {}
|
||||
void set_parity(UARTParityOptions parity) { this->parity_ = parity; }
|
||||
|
||||
@@ -371,7 +371,12 @@ async def to_code(config):
|
||||
if on_timer_tick := config.get(CONF_ON_TIMER_TICK):
|
||||
await automation.build_automation(
|
||||
var.get_timer_tick_trigger(),
|
||||
[(cg.std_vector.template(Timer), "timers")],
|
||||
[
|
||||
(
|
||||
cg.std_vector.template(Timer).operator("const").operator("ref"),
|
||||
"timers",
|
||||
)
|
||||
],
|
||||
on_timer_tick,
|
||||
)
|
||||
has_timers = True
|
||||
|
||||
@@ -859,35 +859,43 @@ void VoiceAssistant::on_audio(const api::VoiceAssistantAudio &msg) {
|
||||
}
|
||||
|
||||
void VoiceAssistant::on_timer_event(const api::VoiceAssistantTimerEventResponse &msg) {
|
||||
Timer timer = {
|
||||
.id = msg.timer_id,
|
||||
.name = msg.name,
|
||||
.total_seconds = msg.total_seconds,
|
||||
.seconds_left = msg.seconds_left,
|
||||
.is_active = msg.is_active,
|
||||
};
|
||||
this->timers_[timer.id] = timer;
|
||||
// Find existing timer or add a new one
|
||||
auto it = this->timers_.begin();
|
||||
for (; it != this->timers_.end(); ++it) {
|
||||
if (it->id == msg.timer_id)
|
||||
break;
|
||||
}
|
||||
if (it == this->timers_.end()) {
|
||||
this->timers_.push_back({});
|
||||
it = this->timers_.end() - 1;
|
||||
}
|
||||
it->id = msg.timer_id;
|
||||
it->name = msg.name;
|
||||
it->total_seconds = msg.total_seconds;
|
||||
it->seconds_left = msg.seconds_left;
|
||||
it->is_active = msg.is_active;
|
||||
|
||||
char timer_buf[Timer::TO_STR_BUFFER_SIZE];
|
||||
ESP_LOGD(TAG,
|
||||
"Timer Event\n"
|
||||
" Type: %" PRId32 "\n"
|
||||
" %s",
|
||||
msg.event_type, timer.to_str(timer_buf));
|
||||
msg.event_type, it->to_str(timer_buf));
|
||||
|
||||
switch (msg.event_type) {
|
||||
case api::enums::VOICE_ASSISTANT_TIMER_STARTED:
|
||||
this->timer_started_trigger_.trigger(timer);
|
||||
this->timer_started_trigger_.trigger(*it);
|
||||
break;
|
||||
case api::enums::VOICE_ASSISTANT_TIMER_UPDATED:
|
||||
this->timer_updated_trigger_.trigger(timer);
|
||||
this->timer_updated_trigger_.trigger(*it);
|
||||
break;
|
||||
case api::enums::VOICE_ASSISTANT_TIMER_CANCELLED:
|
||||
this->timer_cancelled_trigger_.trigger(timer);
|
||||
this->timers_.erase(timer.id);
|
||||
this->timer_cancelled_trigger_.trigger(*it);
|
||||
this->timers_.erase(it);
|
||||
break;
|
||||
case api::enums::VOICE_ASSISTANT_TIMER_FINISHED:
|
||||
this->timer_finished_trigger_.trigger(timer);
|
||||
this->timers_.erase(timer.id);
|
||||
this->timer_finished_trigger_.trigger(*it);
|
||||
this->timers_.erase(it);
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -901,16 +909,12 @@ void VoiceAssistant::on_timer_event(const api::VoiceAssistantTimerEventResponse
|
||||
}
|
||||
|
||||
void VoiceAssistant::timer_tick_() {
|
||||
std::vector<Timer> res;
|
||||
res.reserve(this->timers_.size());
|
||||
for (auto &pair : this->timers_) {
|
||||
auto &timer = pair.second;
|
||||
for (auto &timer : this->timers_) {
|
||||
if (timer.is_active && timer.seconds_left > 0) {
|
||||
timer.seconds_left--;
|
||||
}
|
||||
res.push_back(timer);
|
||||
}
|
||||
this->timer_tick_trigger_.trigger(res);
|
||||
this->timer_tick_trigger_.trigger(this->timers_);
|
||||
}
|
||||
|
||||
void VoiceAssistant::on_announce(const api::VoiceAssistantAnnounceRequest &msg) {
|
||||
|
||||
@@ -24,7 +24,6 @@
|
||||
#include "esphome/components/socket/socket.h"
|
||||
|
||||
#include <span>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
namespace esphome {
|
||||
@@ -226,9 +225,9 @@ class VoiceAssistant : public Component {
|
||||
Trigger<Timer> *get_timer_updated_trigger() { return &this->timer_updated_trigger_; }
|
||||
Trigger<Timer> *get_timer_cancelled_trigger() { return &this->timer_cancelled_trigger_; }
|
||||
Trigger<Timer> *get_timer_finished_trigger() { return &this->timer_finished_trigger_; }
|
||||
Trigger<std::vector<Timer>> *get_timer_tick_trigger() { return &this->timer_tick_trigger_; }
|
||||
Trigger<const std::vector<Timer> &> *get_timer_tick_trigger() { return &this->timer_tick_trigger_; }
|
||||
void set_has_timers(bool has_timers) { this->has_timers_ = has_timers; }
|
||||
const std::unordered_map<std::string, Timer> &get_timers() const { return this->timers_; }
|
||||
const std::vector<Timer> &get_timers() const { return this->timers_; }
|
||||
|
||||
protected:
|
||||
bool allocate_buffers_();
|
||||
@@ -267,13 +266,13 @@ class VoiceAssistant : public Component {
|
||||
|
||||
api::APIConnection *api_client_{nullptr};
|
||||
|
||||
std::unordered_map<std::string, Timer> timers_;
|
||||
std::vector<Timer> timers_;
|
||||
void timer_tick_();
|
||||
Trigger<Timer> timer_started_trigger_;
|
||||
Trigger<Timer> timer_finished_trigger_;
|
||||
Trigger<Timer> timer_updated_trigger_;
|
||||
Trigger<Timer> timer_cancelled_trigger_;
|
||||
Trigger<std::vector<Timer>> timer_tick_trigger_;
|
||||
Trigger<const std::vector<Timer> &> timer_tick_trigger_;
|
||||
bool has_timers_{false};
|
||||
bool timer_tick_running_{false};
|
||||
|
||||
|
||||
@@ -90,9 +90,22 @@ class WaterHeaterCall {
|
||||
float get_target_temperature_low() const { return this->target_temperature_low_; }
|
||||
float get_target_temperature_high() const { return this->target_temperature_high_; }
|
||||
/// Get state flags value
|
||||
ESPDEPRECATED("get_state() is deprecated, use get_away() and get_on() instead. (Removed in 2026.8.0)", "2026.2.0")
|
||||
uint32_t get_state() const { return this->state_; }
|
||||
/// Get mask of state flags that are being changed
|
||||
uint32_t get_state_mask() const { return this->state_mask_; }
|
||||
|
||||
optional<bool> get_away() const {
|
||||
if (this->state_mask_ & WATER_HEATER_STATE_AWAY) {
|
||||
return (this->state_ & WATER_HEATER_STATE_AWAY) != 0;
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
optional<bool> get_on() const {
|
||||
if (this->state_mask_ & WATER_HEATER_STATE_ON) {
|
||||
return (this->state_ & WATER_HEATER_STATE_ON) != 0;
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
protected:
|
||||
void validate_();
|
||||
|
||||
@@ -401,7 +401,7 @@ bool WeikaiChannel::peek_byte(uint8_t *buffer) {
|
||||
return this->receive_buffer_.peek(*buffer);
|
||||
}
|
||||
|
||||
int WeikaiChannel::available() {
|
||||
size_t WeikaiChannel::available() {
|
||||
size_t available = this->receive_buffer_.count();
|
||||
if (!available)
|
||||
available = xfer_fifo_to_buffer_();
|
||||
|
||||
@@ -374,7 +374,7 @@ class WeikaiChannel : public uart::UARTComponent {
|
||||
|
||||
/// @brief Returns the number of bytes in the receive buffer
|
||||
/// @return the number of bytes available in the receiver fifo
|
||||
int available() override;
|
||||
size_t available() override;
|
||||
|
||||
/// @brief Flush the output fifo.
|
||||
/// @details If we refer to Serial.flush() in Arduino it says: ** Waits for the transmission of outgoing serial data
|
||||
|
||||
@@ -71,11 +71,9 @@ def _validate_load_certificate(value):
|
||||
|
||||
|
||||
def validate_certificate(value):
|
||||
# _validate_load_certificate already calls cv.file_() internally,
|
||||
# but returns the parsed certificate object. We re-call cv.file_()
|
||||
# to get the resolved path string that the bundle walker can discover.
|
||||
_validate_load_certificate(value)
|
||||
return str(cv.file_(value))
|
||||
# Validation result should be the path, not the loaded certificate
|
||||
return value
|
||||
|
||||
|
||||
def _validate_load_private_key(key, cert_pw):
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#include "esphome/core/log.h"
|
||||
#include <zephyr/settings/settings.h>
|
||||
#include <zephyr/storage/flash_map.h>
|
||||
#include "esphome/core/hal.h"
|
||||
|
||||
extern "C" {
|
||||
#include <zboss_api.h>
|
||||
@@ -223,6 +224,7 @@ void ZigbeeComponent::dump_config() {
|
||||
get_wipe_on_boot(), YESNO(zb_zdo_joined()), zb_get_current_channel(), zb_get_current_page(),
|
||||
zb_get_sleep_threshold(), role(), ieee_addr_buf, zb_get_short_address(), extended_pan_id_buf,
|
||||
zb_get_pan_id());
|
||||
dump_reporting_();
|
||||
}
|
||||
|
||||
static void send_attribute_report(zb_bufid_t bufid, zb_uint16_t cmd_id) {
|
||||
@@ -244,6 +246,33 @@ void ZigbeeComponent::factory_reset() {
|
||||
ZB_SCHEDULE_APP_CALLBACK(zb_bdb_reset_via_local_action, 0);
|
||||
}
|
||||
|
||||
void ZigbeeComponent::dump_reporting_() {
|
||||
#ifdef ESPHOME_LOG_HAS_VERBOSE
|
||||
auto now = millis();
|
||||
bool first = true;
|
||||
for (zb_uint8_t j = 0; j < ZCL_CTX().device_ctx->ep_count; j++) {
|
||||
if (ZCL_CTX().device_ctx->ep_desc_list[j]->reporting_info) {
|
||||
zb_zcl_reporting_info_t *rep_info = ZCL_CTX().device_ctx->ep_desc_list[j]->reporting_info;
|
||||
for (zb_uint8_t i = 0; i < ZCL_CTX().device_ctx->ep_desc_list[j]->rep_info_count; i++) {
|
||||
if (!first) {
|
||||
ESP_LOGV(TAG, "");
|
||||
}
|
||||
first = false;
|
||||
ESP_LOGV(TAG, "Endpoint: %d, cluster_id %d, attr_id %d, flags %d, report in %ums", rep_info->ep,
|
||||
rep_info->cluster_id, rep_info->attr_id, rep_info->flags,
|
||||
ZB_ZCL_GET_REPORTING_FLAG(rep_info, ZB_ZCL_REPORT_TIMER_STARTED)
|
||||
? ZB_TIME_BEACON_INTERVAL_TO_MSEC(rep_info->run_time) - now
|
||||
: 0);
|
||||
ESP_LOGV(TAG, "Min_interval %ds, max_interval %ds, def_min_interval %ds, def_max_interval %ds",
|
||||
rep_info->u.send_info.min_interval, rep_info->u.send_info.max_interval,
|
||||
rep_info->u.send_info.def_min_interval, rep_info->u.send_info.def_max_interval);
|
||||
rep_info++;
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
} // namespace esphome::zigbee
|
||||
|
||||
extern "C" void zboss_signal_handler(zb_uint8_t param) {
|
||||
|
||||
@@ -87,6 +87,7 @@ class ZigbeeComponent : public Component {
|
||||
#ifdef USE_ZIGBEE_WIPE_ON_BOOT
|
||||
void erase_flash_(int area);
|
||||
#endif
|
||||
void dump_reporting_();
|
||||
std::array<std::function<void(zb_bufid_t bufid)>, ZIGBEE_ENDPOINTS_COUNT> callbacks_{};
|
||||
CallbackManager<void()> join_cb_;
|
||||
Trigger<> join_trigger_;
|
||||
|
||||
@@ -26,6 +26,7 @@ class ComponentIterator {
|
||||
public:
|
||||
void begin(bool include_internal = false);
|
||||
void advance();
|
||||
bool completed() const { return this->state_ == IteratorState::NONE; }
|
||||
virtual bool on_begin();
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
virtual bool on_binary_sensor(binary_sensor::BinarySensor *binary_sensor) = 0;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable, Generator
|
||||
from contextlib import contextmanager, suppress
|
||||
from collections.abc import Callable
|
||||
from contextlib import suppress
|
||||
import functools
|
||||
import inspect
|
||||
from io import BytesIO, TextIOBase, TextIOWrapper
|
||||
@@ -43,27 +43,6 @@ _LOGGER = logging.getLogger(__name__)
|
||||
SECRET_YAML = "secrets.yaml"
|
||||
_SECRET_CACHE = {}
|
||||
_SECRET_VALUES = {}
|
||||
# Not thread-safe — config processing is single-threaded today.
|
||||
_load_listeners: list[Callable[[Path], None]] = []
|
||||
|
||||
|
||||
@contextmanager
|
||||
def track_yaml_loads() -> Generator[list[Path]]:
|
||||
"""Context manager that records every file loaded by the YAML loader.
|
||||
|
||||
Yields a list that is populated with resolved Path objects for every
|
||||
file loaded through ``_load_yaml_internal`` while the context is active.
|
||||
"""
|
||||
loaded: list[Path] = []
|
||||
|
||||
def _on_load(fname: Path) -> None:
|
||||
loaded.append(Path(fname).resolve())
|
||||
|
||||
_load_listeners.append(_on_load)
|
||||
try:
|
||||
yield loaded
|
||||
finally:
|
||||
_load_listeners.remove(_on_load)
|
||||
|
||||
|
||||
class ESPHomeDataBase:
|
||||
@@ -449,8 +428,6 @@ def load_yaml(fname: Path, clear_secrets: bool = True) -> Any:
|
||||
|
||||
def _load_yaml_internal(fname: Path) -> Any:
|
||||
"""Load a YAML file."""
|
||||
for listener in _load_listeners:
|
||||
listener(fname)
|
||||
try:
|
||||
with fname.open(encoding="utf-8") as f_handle:
|
||||
return parse_yaml(fname, f_handle)
|
||||
@@ -458,10 +435,10 @@ def _load_yaml_internal(fname: Path) -> Any:
|
||||
raise EsphomeError(f"Error reading file {fname}: {err}") from err
|
||||
|
||||
|
||||
def parse_yaml(file_name: Path, file_handle: TextIOWrapper, yaml_loader=None) -> Any:
|
||||
def parse_yaml(
|
||||
file_name: Path, file_handle: TextIOWrapper, yaml_loader=_load_yaml_internal
|
||||
) -> Any:
|
||||
"""Parse a YAML file."""
|
||||
if yaml_loader is None:
|
||||
yaml_loader = _load_yaml_internal
|
||||
try:
|
||||
return _load_yaml_internal_with_type(
|
||||
ESPHomeLoader, file_name, file_handle, yaml_loader
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
[build-system]
|
||||
requires = ["setuptools==80.10.2", "wheel>=0.43,<0.47"]
|
||||
requires = ["setuptools==82.0.0", "wheel>=0.43,<0.47"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
|
||||
52
tests/components/mipi_rgb/common.yaml
Normal file
52
tests/components/mipi_rgb/common.yaml
Normal file
@@ -0,0 +1,52 @@
|
||||
display:
|
||||
- platform: mipi_rgb
|
||||
spi_id: spi_bus
|
||||
model: ZX2D10GE01R-V4848
|
||||
update_interval: 1s
|
||||
color_order: BGR
|
||||
draw_rounding: 2
|
||||
pixel_mode: 18bit
|
||||
invert_colors: false
|
||||
use_axis_flips: true
|
||||
pclk_frequency: 15000000.0
|
||||
pclk_inverted: true
|
||||
byte_order: big_endian
|
||||
hsync_pulse_width: 10
|
||||
hsync_back_porch: 10
|
||||
hsync_front_porch: 10
|
||||
vsync_pulse_width: 2
|
||||
vsync_back_porch: 12
|
||||
vsync_front_porch: 14
|
||||
data_pins:
|
||||
red:
|
||||
- number: 10
|
||||
- number: 16
|
||||
- number: 9
|
||||
- number: 15
|
||||
- number: 46
|
||||
green:
|
||||
- number: 8
|
||||
- number: 13
|
||||
- number: 18
|
||||
- number: 12
|
||||
- number: 11
|
||||
- number: 17
|
||||
blue:
|
||||
- number: 47
|
||||
- number: 1
|
||||
- number: 0
|
||||
- number: 42
|
||||
- number: 14
|
||||
de_pin:
|
||||
number: 39
|
||||
pclk_pin:
|
||||
number: 45
|
||||
hsync_pin:
|
||||
number: 38
|
||||
vsync_pin:
|
||||
number: 48
|
||||
data_rate: 1000000.0
|
||||
spi_mode: MODE0
|
||||
cs_pin:
|
||||
number: 21
|
||||
show_test_card: true
|
||||
6
tests/components/mipi_rgb/test.esp32-p4-idf.yaml
Normal file
6
tests/components/mipi_rgb/test.esp32-p4-idf.yaml
Normal file
@@ -0,0 +1,6 @@
|
||||
packages:
|
||||
spi: !include ../../test_build_components/common/spi/esp32-p4-idf.yaml
|
||||
|
||||
psram:
|
||||
|
||||
<<: !include common.yaml
|
||||
@@ -4,58 +4,4 @@ packages:
|
||||
psram:
|
||||
mode: octal
|
||||
|
||||
display:
|
||||
- platform: mipi_rgb
|
||||
spi_id: spi_bus
|
||||
model: ZX2D10GE01R-V4848
|
||||
update_interval: 1s
|
||||
color_order: BGR
|
||||
draw_rounding: 2
|
||||
pixel_mode: 18bit
|
||||
invert_colors: false
|
||||
use_axis_flips: true
|
||||
pclk_frequency: 15000000.0
|
||||
pclk_inverted: true
|
||||
byte_order: big_endian
|
||||
hsync_pulse_width: 10
|
||||
hsync_back_porch: 10
|
||||
hsync_front_porch: 10
|
||||
vsync_pulse_width: 2
|
||||
vsync_back_porch: 12
|
||||
vsync_front_porch: 14
|
||||
data_pins:
|
||||
red:
|
||||
- number: 10
|
||||
- number: 16
|
||||
- number: 9
|
||||
- number: 15
|
||||
- number: 46
|
||||
ignore_strapping_warning: true
|
||||
green:
|
||||
- number: 8
|
||||
- number: 13
|
||||
- number: 18
|
||||
- number: 12
|
||||
- number: 11
|
||||
- number: 17
|
||||
blue:
|
||||
- number: 47
|
||||
- number: 1
|
||||
- number: 0
|
||||
ignore_strapping_warning: true
|
||||
- number: 42
|
||||
- number: 14
|
||||
de_pin:
|
||||
number: 39
|
||||
pclk_pin:
|
||||
number: 45
|
||||
ignore_strapping_warning: true
|
||||
hsync_pin:
|
||||
number: 38
|
||||
vsync_pin:
|
||||
number: 48
|
||||
data_rate: 1000000.0
|
||||
spi_mode: MODE0
|
||||
cs_pin:
|
||||
number: 21
|
||||
show_test_card: true
|
||||
<<: !include common.yaml
|
||||
|
||||
@@ -29,7 +29,7 @@ class MockUARTComponent : public UARTComponent {
|
||||
|
||||
MOCK_METHOD(bool, read_array, (uint8_t * data, size_t len), (override));
|
||||
MOCK_METHOD(bool, peek_byte, (uint8_t * data), (override));
|
||||
MOCK_METHOD(int, available, (), (override));
|
||||
MOCK_METHOD(size_t, available, (), (override));
|
||||
MOCK_METHOD(void, flush, (), (override));
|
||||
MOCK_METHOD(void, check_logger_conflict, (), (override));
|
||||
};
|
||||
|
||||
@@ -68,3 +68,24 @@ voice_assistant:
|
||||
- logger.log:
|
||||
format: "Voice assistant error - code %s, message: %s"
|
||||
args: [code.c_str(), message.c_str()]
|
||||
on_timer_started:
|
||||
- logger.log:
|
||||
format: "Timer started: %s"
|
||||
args: [timer.id.c_str()]
|
||||
on_timer_updated:
|
||||
- logger.log:
|
||||
format: "Timer updated: %s"
|
||||
args: [timer.id.c_str()]
|
||||
on_timer_cancelled:
|
||||
- logger.log:
|
||||
format: "Timer cancelled: %s"
|
||||
args: [timer.id.c_str()]
|
||||
on_timer_finished:
|
||||
- logger.log:
|
||||
format: "Timer finished: %s"
|
||||
args: [timer.id.c_str()]
|
||||
on_timer_tick:
|
||||
- lambda: |-
|
||||
for (auto &timer : timers) {
|
||||
ESP_LOGD("timer", "Timer %s: %" PRIu32 "s left", timer.name.c_str(), timer.seconds_left);
|
||||
}
|
||||
|
||||
@@ -58,3 +58,24 @@ voice_assistant:
|
||||
- logger.log:
|
||||
format: "Voice assistant error - code %s, message: %s"
|
||||
args: [code.c_str(), message.c_str()]
|
||||
on_timer_started:
|
||||
- logger.log:
|
||||
format: "Timer started: %s"
|
||||
args: [timer.id.c_str()]
|
||||
on_timer_updated:
|
||||
- logger.log:
|
||||
format: "Timer updated: %s"
|
||||
args: [timer.id.c_str()]
|
||||
on_timer_cancelled:
|
||||
- logger.log:
|
||||
format: "Timer cancelled: %s"
|
||||
args: [timer.id.c_str()]
|
||||
on_timer_finished:
|
||||
- logger.log:
|
||||
format: "Timer finished: %s"
|
||||
args: [timer.id.c_str()]
|
||||
on_timer_tick:
|
||||
- lambda: |-
|
||||
for (auto &timer : timers) {
|
||||
ESP_LOGD("timer", "Timer %s: %" PRIu32 "s left", timer.name.c_str(), timer.seconds_left);
|
||||
}
|
||||
|
||||
12
tests/test_build_components/common/spi/esp32-p4-idf.yaml
Normal file
12
tests/test_build_components/common/spi/esp32-p4-idf.yaml
Normal file
@@ -0,0 +1,12 @@
|
||||
# Common SPI configuration for ESP32-P4 IDF tests
|
||||
|
||||
substitutions:
|
||||
clk_pin: GPIO36
|
||||
mosi_pin: GPIO32
|
||||
miso_pin: GPIO33
|
||||
|
||||
spi:
|
||||
- id: spi_bus
|
||||
clk_pin: ${clk_pin}
|
||||
mosi_pin: ${mosi_pin}
|
||||
miso_pin: ${miso_pin}
|
||||
@@ -1,18 +0,0 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIICzjCCAbagAwIBAgIUW3BzjtekVgMj12/oeXawSswGyXMwDQYJKoZIhvcNAQEL
|
||||
BQAwITEfMB0GA1UEAwwWRVNQSG9tZSBCdW5kbGUgVGVzdCBDQTAeFw0yNjAyMDYx
|
||||
MzMxMTZaFw0yNzAyMDYxMzMxMTZaMCExHzAdBgNVBAMMFkVTUEhvbWUgQnVuZGxl
|
||||
IFRlc3QgQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDG62vBFkGn
|
||||
hEu54gh2A7b1ZwesVadZ6u0iaVO7GSWiI0o4nb6xv7ULZbGrgsKNIO6qCV4VSR3p
|
||||
BfMhF5dFy8kkMzA8dKZMk16tygzocdNum2QQ8BHyIsATL7SGZ33si9Alp30gXv6h
|
||||
XSlEKYDKHFavkDhWPFNa5+oeHbMS/MxjpOUXIpq32VaFpJr427d9Y9wGjuK8B7Gp
|
||||
CI5Ub1g2dpC9xSHqQKD3JZokmtc70+mD74AcNWbyxWp0bkW9wOfNJJnAoiwhJxQ8
|
||||
yfE37UsUIVc8014NhdhU1K/S0iQuOKfGX1L/GAshv8syQIcDfzJuJdX+5E/leAYD
|
||||
UEKqRkcLT+D5AgMBAAEwDQYJKoZIhvcNAQELBQADggEBAF1HpJ6d+W5WrzOQrGej
|
||||
41pxCDeJ9tSiSj/KtvJfjEVIpg0hMRTY7nSL7OAg9KGESfx4u1jMwVnyOv34br5B
|
||||
DTlRl+wF2k7Ip8CNnyZfCC+1SVQZpUt1mVNz8BhIZZ9/a830wCILNQQrVKkSeNBk
|
||||
SEc1qTt4mIhQZ+M422qAswluv4fz/FW1f4oB9KhCpzUCANjmyERnqTnImjnJu8h0
|
||||
jbPNnNsN+G+Roju8UD/7atWYfAUmDjHx72Ci/5G9SzoM5fhgxxu43XYd5RW5wBzt
|
||||
j4KdKdYlDtOL62mRPKWd40uGnJcieUjisU7noRn0ErMgbUlhLdbXT9X7aNborZcu
|
||||
x6I=
|
||||
-----END CERTIFICATE-----
|
||||
@@ -1,18 +0,0 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIICzjCCAbagAwIBAgIUW3BzjtekVgMj12/oeXawSswGyXMwDQYJKoZIhvcNAQEL
|
||||
BQAwITEfMB0GA1UEAwwWRVNQSG9tZSBCdW5kbGUgVGVzdCBDQTAeFw0yNjAyMDYx
|
||||
MzMxMTZaFw0yNzAyMDYxMzMxMTZaMCExHzAdBgNVBAMMFkVTUEhvbWUgQnVuZGxl
|
||||
IFRlc3QgQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDG62vBFkGn
|
||||
hEu54gh2A7b1ZwesVadZ6u0iaVO7GSWiI0o4nb6xv7ULZbGrgsKNIO6qCV4VSR3p
|
||||
BfMhF5dFy8kkMzA8dKZMk16tygzocdNum2QQ8BHyIsATL7SGZ33si9Alp30gXv6h
|
||||
XSlEKYDKHFavkDhWPFNa5+oeHbMS/MxjpOUXIpq32VaFpJr427d9Y9wGjuK8B7Gp
|
||||
CI5Ub1g2dpC9xSHqQKD3JZokmtc70+mD74AcNWbyxWp0bkW9wOfNJJnAoiwhJxQ8
|
||||
yfE37UsUIVc8014NhdhU1K/S0iQuOKfGX1L/GAshv8syQIcDfzJuJdX+5E/leAYD
|
||||
UEKqRkcLT+D5AgMBAAEwDQYJKoZIhvcNAQELBQADggEBAF1HpJ6d+W5WrzOQrGej
|
||||
41pxCDeJ9tSiSj/KtvJfjEVIpg0hMRTY7nSL7OAg9KGESfx4u1jMwVnyOv34br5B
|
||||
DTlRl+wF2k7Ip8CNnyZfCC+1SVQZpUt1mVNz8BhIZZ9/a830wCILNQQrVKkSeNBk
|
||||
SEc1qTt4mIhQZ+M422qAswluv4fz/FW1f4oB9KhCpzUCANjmyERnqTnImjnJu8h0
|
||||
jbPNnNsN+G+Roju8UD/7atWYfAUmDjHx72Ci/5G9SzoM5fhgxxu43XYd5RW5wBzt
|
||||
j4KdKdYlDtOL62mRPKWd40uGnJcieUjisU7noRn0ErMgbUlhLdbXT9X7aNborZcu
|
||||
x6I=
|
||||
-----END CERTIFICATE-----
|
||||
@@ -1,27 +0,0 @@
|
||||
-----BEGIN RSA PRIVATE KEY-----
|
||||
MIIEowIBAAKCAQEAxutrwRZBp4RLueIIdgO29WcHrFWnWertImlTuxkloiNKOJ2+
|
||||
sb+1C2Wxq4LCjSDuqgleFUkd6QXzIReXRcvJJDMwPHSmTJNercoM6HHTbptkEPAR
|
||||
8iLAEy+0hmd97IvQJad9IF7+oV0pRCmAyhxWr5A4VjxTWufqHh2zEvzMY6TlFyKa
|
||||
t9lWhaSa+Nu3fWPcBo7ivAexqQiOVG9YNnaQvcUh6kCg9yWaJJrXO9Ppg++AHDVm
|
||||
8sVqdG5FvcDnzSSZwKIsIScUPMnxN+1LFCFXPNNeDYXYVNSv0tIkLjinxl9S/xgL
|
||||
Ib/LMkCHA38ybiXV/uRP5XgGA1BCqkZHC0/g+QIDAQABAoIBAEpsFwcJNCwf95MG
|
||||
qcK5lhCPaRQFgdTG68ylmoGUIXvddy3ies+W2X33oLb5958ElLaCRbRyBCJEKxgU
|
||||
8vBWk50bF69uty9MLa6YuyaWO5QUyCX8I8KzVKh4/zIP81F2Z7xGwy5CzEKED+Xk
|
||||
Hz6+xoHt094TuN34iaOV2gM/GJsok4Wp/lzsuT3X6i3Nad9YGrV2yL/wv5c542bw
|
||||
vrFDtYQ/+ADZZPW4+xK0ShiarSqV3iXB2cEjc4JX7yLX1hB4LY8VHRzl+Byjdl0/
|
||||
lheiIesl5htl82SFxquZDimDsbilTm7TLW2bbm3b3/oC7DchTx6COBjp90VJqk3R
|
||||
QrO5dicCgYEA80pyA7tCB0bGnJ7KWkteKddyOdakeYeM7Bpfv17qbCm9ciMw9nqt
|
||||
KJVZPtAuqZGTpfSJseOCIyz9zloB79hVJ3mdWpGJVvmNM5H+BJyCciXpwfqp64QG
|
||||
1gMqGlSy/MwsZHqNCsOIvrzH09GFN0LSPNKeXN7GNAtU1vI5s7Xf158CgYEA0U+Y
|
||||
Qe1qJY4m597spHNFfkGznoFXAjHOoWYHv95902cH6JD4GnYPfwFXxgFsrJhFaFMC
|
||||
jXlT0fRFAIe4NuUJhGD6TYSJqsFkH3xJkAepvKpfjM5qJ7+PQHRnED/E5OS2Nj0R
|
||||
+cxBhTEWTw9YiOFBRbj6hlphkj8izVGJZ2pL4GcCgYEApsjiYKx/F33tqnExR7Vj
|
||||
WEvagswi9S137mQmP4tSKdRzi0uUxWRUUP4RsH4HfzfNgHej7c+J55Nwa4ZIzaQA
|
||||
vI8i0HP1MyrhIflzqrWgt6BGIDU3R7268fw5YNOv4J4X0Moy5q4lkJzaYNvB96BX
|
||||
gFrjNceDGSqrfq+P3yNP0QECgYBNQfHTM8ygPA4EO/Zg5ONbrOidsuPovXWlgUGP
|
||||
ApKy+y6iGxBYxAcIO/in71KrijDkRu+ERKo5rs3hWjcWnAedQyZggnFGA8fvDzMf
|
||||
5JQ0PTazhGUOcthvVAfOqZsFWZ4f+v6tk0UD4pB3chSdwXcUQyjFeorVLlSsMFJl
|
||||
R4jmNQKBgG38YFR2bqIc7jJItr+34POXdJ4te8Dm1jJHbo8xXsnjVSaxjc5PGs3p
|
||||
OuJpwuMwzEuFEnE7XLkQxTJw54OBLMmDgK0XUOPDq6eLzrKkW5NlpejqaQV9Piyo
|
||||
q1kqbJan20jfJQUGTcX7FXHMUThzqJltHILR1GTW6I9z4k8xdsDY
|
||||
-----END RSA PRIVATE KEY-----
|
||||
Binary file not shown.
Binary file not shown.
|
Before Width: | Height: | Size: 9.5 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 685 B |
@@ -1,2 +0,0 @@
|
||||
/* Dummy CSS for bundle testing */
|
||||
body { color: red; }
|
||||
@@ -1,2 +0,0 @@
|
||||
// Dummy JS for bundle testing
|
||||
console.log("test");
|
||||
@@ -1,60 +0,0 @@
|
||||
esphome:
|
||||
name: bundle-test
|
||||
includes:
|
||||
- includes/custom_sensor.h
|
||||
|
||||
esp32:
|
||||
board: esp32dev
|
||||
framework:
|
||||
type: esp-idf
|
||||
|
||||
logger:
|
||||
<<: !include common/base.yaml
|
||||
|
||||
wifi:
|
||||
ssid: !secret wifi_ssid
|
||||
password: !secret wifi_password
|
||||
|
||||
api:
|
||||
|
||||
ota:
|
||||
- platform: esphome
|
||||
password: !secret ota_password
|
||||
|
||||
web_server:
|
||||
port: 80
|
||||
css_include: assets/web/custom.css
|
||||
js_include: assets/web/custom.js
|
||||
|
||||
i2c:
|
||||
sda: GPIO21
|
||||
scl: GPIO22
|
||||
|
||||
font:
|
||||
- id: test_font
|
||||
file: assets/fonts/test_font.ttf
|
||||
size: 16
|
||||
|
||||
image:
|
||||
- id: test_image
|
||||
file: assets/images/logo.png
|
||||
type: BINARY
|
||||
resize: 16x16
|
||||
|
||||
animation:
|
||||
- id: test_animation
|
||||
file: assets/images/animation.gif
|
||||
type: BINARY
|
||||
resize: 16x16
|
||||
|
||||
display:
|
||||
- platform: ssd1306_i2c
|
||||
model: SSD1306_128X64
|
||||
address: 0x3C
|
||||
lambda: |-
|
||||
it.image(0, 0, id(test_image));
|
||||
|
||||
external_components:
|
||||
- source:
|
||||
type: local
|
||||
path: local_components
|
||||
@@ -1 +0,0 @@
|
||||
level: DEBUG
|
||||
@@ -1,3 +0,0 @@
|
||||
// Dummy custom sensor header for bundle testing
|
||||
#pragma once
|
||||
#include "esphome/core/component.h"
|
||||
@@ -1 +0,0 @@
|
||||
# Dummy local external component for bundle testing
|
||||
@@ -1,2 +0,0 @@
|
||||
// Dummy component header for bundle testing
|
||||
#pragma once
|
||||
@@ -1,4 +0,0 @@
|
||||
wifi_ssid: "TestNetwork"
|
||||
wifi_password: "TestPassword123"
|
||||
api_key: "unused_secret_should_not_appear"
|
||||
ota_password: "ota_test_password"
|
||||
File diff suppressed because it is too large
Load Diff
@@ -20,7 +20,6 @@ from esphome.__main__ import (
|
||||
Purpose,
|
||||
choose_upload_log_host,
|
||||
command_analyze_memory,
|
||||
command_bundle,
|
||||
command_clean_all,
|
||||
command_rename,
|
||||
command_update_all,
|
||||
@@ -42,7 +41,6 @@ from esphome.__main__ import (
|
||||
upload_program,
|
||||
upload_using_esptool,
|
||||
)
|
||||
from esphome.bundle import BUNDLE_EXTENSION, BundleFile, BundleResult
|
||||
from esphome.components.esp32 import KEY_ESP32, KEY_VARIANT, VARIANT_ESP32
|
||||
from esphome.const import (
|
||||
CONF_API,
|
||||
@@ -867,8 +865,6 @@ class MockArgs:
|
||||
name: str | None = None
|
||||
dashboard: bool = False
|
||||
reset: bool = False
|
||||
list_only: bool = False
|
||||
output: str | None = None
|
||||
|
||||
|
||||
def test_upload_program_serial_esp32(
|
||||
@@ -3295,195 +3291,3 @@ esp32:
|
||||
clean_output.split("SUMMARY")[1] if "SUMMARY" in clean_output else ""
|
||||
)
|
||||
assert "secrets.yaml" not in summary_section
|
||||
|
||||
|
||||
# --- command_bundle tests ---
|
||||
|
||||
|
||||
def test_command_bundle_list_only(
|
||||
tmp_path: Path,
|
||||
capsys: CaptureFixture[str],
|
||||
) -> None:
|
||||
"""Test command_bundle with --list-only prints files and returns 0."""
|
||||
mock_files = [
|
||||
BundleFile(path="device.yaml", source=tmp_path / "device.yaml"),
|
||||
BundleFile(path="secrets.yaml", source=tmp_path / "secrets.yaml"),
|
||||
BundleFile(path="common/base.yaml", source=tmp_path / "common" / "base.yaml"),
|
||||
]
|
||||
|
||||
args = MockArgs(list_only=True)
|
||||
config: dict[str, Any] = {}
|
||||
|
||||
mock_creator = MagicMock()
|
||||
mock_creator.discover_files.return_value = mock_files
|
||||
|
||||
with patch("esphome.bundle.ConfigBundleCreator", return_value=mock_creator):
|
||||
result = command_bundle(args, config)
|
||||
|
||||
assert result == 0
|
||||
captured = capsys.readouterr()
|
||||
# Files should be printed in sorted order
|
||||
assert "common/base.yaml" in captured.out
|
||||
assert "device.yaml" in captured.out
|
||||
assert "secrets.yaml" in captured.out
|
||||
|
||||
|
||||
def test_command_bundle_list_only_empty(
|
||||
tmp_path: Path,
|
||||
capsys: CaptureFixture[str],
|
||||
) -> None:
|
||||
"""Test command_bundle --list-only with no files discovered."""
|
||||
args = MockArgs(list_only=True)
|
||||
config: dict[str, Any] = {}
|
||||
|
||||
mock_creator = MagicMock()
|
||||
mock_creator.discover_files.return_value = []
|
||||
|
||||
with patch("esphome.bundle.ConfigBundleCreator", return_value=mock_creator):
|
||||
result = command_bundle(args, config)
|
||||
|
||||
assert result == 0
|
||||
|
||||
|
||||
def test_command_bundle_creates_archive(tmp_path: Path) -> None:
|
||||
"""Test command_bundle creates archive at default output path."""
|
||||
CORE.config_path = tmp_path / "mydevice.yaml"
|
||||
|
||||
mock_result = BundleResult(
|
||||
data=b"fake-tar-gz-data",
|
||||
manifest={"manifest_version": 1},
|
||||
files=[BundleFile(path="mydevice.yaml", source=tmp_path / "mydevice.yaml")],
|
||||
)
|
||||
|
||||
args = MockArgs()
|
||||
config: dict[str, Any] = {}
|
||||
|
||||
mock_creator = MagicMock()
|
||||
mock_creator.create_bundle.return_value = mock_result
|
||||
|
||||
with patch("esphome.bundle.ConfigBundleCreator", return_value=mock_creator):
|
||||
result = command_bundle(args, config)
|
||||
|
||||
assert result == 0
|
||||
output_path = tmp_path / f"mydevice{BUNDLE_EXTENSION}"
|
||||
assert output_path.exists()
|
||||
assert output_path.read_bytes() == b"fake-tar-gz-data"
|
||||
|
||||
|
||||
def test_command_bundle_custom_output(tmp_path: Path) -> None:
|
||||
"""Test command_bundle with -o custom output path."""
|
||||
custom_output = tmp_path / "output" / "custom.esphomebundle.tar.gz"
|
||||
mock_result = BundleResult(
|
||||
data=b"custom-output-data",
|
||||
manifest={"manifest_version": 1},
|
||||
files=[BundleFile(path="mydevice.yaml", source=tmp_path / "mydevice.yaml")],
|
||||
)
|
||||
|
||||
args = MockArgs(output=str(custom_output))
|
||||
config: dict[str, Any] = {}
|
||||
|
||||
mock_creator = MagicMock()
|
||||
mock_creator.create_bundle.return_value = mock_result
|
||||
|
||||
with patch("esphome.bundle.ConfigBundleCreator", return_value=mock_creator):
|
||||
result = command_bundle(args, config)
|
||||
|
||||
assert result == 0
|
||||
assert custom_output.exists()
|
||||
assert custom_output.read_bytes() == b"custom-output-data"
|
||||
|
||||
|
||||
def test_command_bundle_creates_parent_dirs(tmp_path: Path) -> None:
|
||||
"""Test command_bundle creates parent directories for output path."""
|
||||
nested_output = tmp_path / "deep" / "nested" / "dir" / "out.tar.gz"
|
||||
mock_result = BundleResult(
|
||||
data=b"data",
|
||||
manifest={"manifest_version": 1},
|
||||
files=[BundleFile(path="mydevice.yaml", source=tmp_path / "mydevice.yaml")],
|
||||
)
|
||||
|
||||
args = MockArgs(output=str(nested_output))
|
||||
config: dict[str, Any] = {}
|
||||
|
||||
mock_creator = MagicMock()
|
||||
mock_creator.create_bundle.return_value = mock_result
|
||||
|
||||
with patch("esphome.bundle.ConfigBundleCreator", return_value=mock_creator):
|
||||
result = command_bundle(args, config)
|
||||
|
||||
assert result == 0
|
||||
assert nested_output.exists()
|
||||
|
||||
|
||||
def test_command_bundle_logs_info(
|
||||
tmp_path: Path,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
"""Test command_bundle logs bundle creation info."""
|
||||
CORE.config_path = tmp_path / "mydevice.yaml"
|
||||
|
||||
mock_result = BundleResult(
|
||||
data=b"x" * 2048,
|
||||
manifest={"manifest_version": 1},
|
||||
files=[
|
||||
BundleFile(path="mydevice.yaml", source=tmp_path / "mydevice.yaml"),
|
||||
BundleFile(path="secrets.yaml", source=tmp_path / "secrets.yaml"),
|
||||
],
|
||||
)
|
||||
|
||||
args = MockArgs()
|
||||
config: dict[str, Any] = {}
|
||||
|
||||
mock_creator = MagicMock()
|
||||
mock_creator.create_bundle.return_value = mock_result
|
||||
|
||||
with (
|
||||
patch("esphome.bundle.ConfigBundleCreator", return_value=mock_creator),
|
||||
caplog.at_level(logging.INFO),
|
||||
):
|
||||
result = command_bundle(args, config)
|
||||
|
||||
assert result == 0
|
||||
assert "Bundle created" in caplog.text
|
||||
assert "2 files" in caplog.text
|
||||
assert "2.0 KB" in caplog.text
|
||||
|
||||
|
||||
def test_run_esphome_bundle_detection(tmp_path: Path) -> None:
|
||||
"""Test run_esphome detects .esphomebundle.tar.gz and extracts it."""
|
||||
bundle_path = tmp_path / f"device{BUNDLE_EXTENSION}"
|
||||
bundle_path.write_bytes(b"fake-bundle")
|
||||
|
||||
extracted_yaml = tmp_path / "extracted" / "device.yaml"
|
||||
|
||||
with (
|
||||
patch("esphome.bundle.is_bundle_path", return_value=True) as mock_is_bundle,
|
||||
patch(
|
||||
"esphome.bundle.prepare_bundle_for_compile",
|
||||
return_value=extracted_yaml,
|
||||
) as mock_prepare,
|
||||
patch("esphome.__main__.read_config", return_value=None),
|
||||
):
|
||||
result = run_esphome(["esphome", "compile", str(bundle_path)])
|
||||
|
||||
mock_is_bundle.assert_called_once()
|
||||
mock_prepare.assert_called_once_with(bundle_path)
|
||||
# read_config returns None → exit code 2
|
||||
assert result == 2
|
||||
|
||||
|
||||
def test_run_esphome_non_bundle_skips_extraction(tmp_path: Path) -> None:
|
||||
"""Test run_esphome does not extract for regular .yaml files."""
|
||||
yaml_file = tmp_path / "device.yaml"
|
||||
yaml_file.write_text("esphome:\n name: test\n")
|
||||
|
||||
with (
|
||||
patch("esphome.bundle.is_bundle_path", return_value=False) as mock_is_bundle,
|
||||
patch("esphome.bundle.prepare_bundle_for_compile") as mock_prepare,
|
||||
patch("esphome.__main__.read_config", return_value=None),
|
||||
):
|
||||
result = run_esphome(["esphome", "compile", str(yaml_file)])
|
||||
|
||||
mock_is_bundle.assert_called_once()
|
||||
mock_prepare.assert_not_called()
|
||||
assert result == 2
|
||||
|
||||
@@ -306,57 +306,3 @@ def test_dump_sort_keys() -> None:
|
||||
# nested keys should also be sorted
|
||||
assert "a_key:" in sorted_dump
|
||||
assert sorted_dump.index("a_key:") < sorted_dump.index("z_key:")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# track_yaml_loads
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_track_yaml_loads_records_files(tmp_path: Path) -> None:
|
||||
"""track_yaml_loads records every file loaded inside the context."""
|
||||
yaml_file = tmp_path / "test.yaml"
|
||||
yaml_file.write_text("key: value\n")
|
||||
|
||||
with yaml_util.track_yaml_loads() as loaded:
|
||||
yaml_util.load_yaml(yaml_file)
|
||||
|
||||
assert len(loaded) == 1
|
||||
assert loaded[0] == yaml_file.resolve()
|
||||
|
||||
|
||||
def test_track_yaml_loads_records_includes(tmp_path: Path) -> None:
|
||||
"""track_yaml_loads records nested !include files."""
|
||||
inc = tmp_path / "included.yaml"
|
||||
inc.write_text("included_key: 42\n")
|
||||
main = tmp_path / "main.yaml"
|
||||
main.write_text("child: !include included.yaml\n")
|
||||
|
||||
with yaml_util.track_yaml_loads() as loaded:
|
||||
yaml_util.load_yaml(main)
|
||||
|
||||
resolved = [p.name for p in loaded]
|
||||
assert "main.yaml" in resolved
|
||||
assert "included.yaml" in resolved
|
||||
|
||||
|
||||
def test_track_yaml_loads_empty_outside_context(tmp_path: Path) -> None:
|
||||
"""Files loaded outside the context are not recorded."""
|
||||
yaml_file = tmp_path / "test.yaml"
|
||||
yaml_file.write_text("key: value\n")
|
||||
|
||||
with yaml_util.track_yaml_loads() as loaded:
|
||||
pass # load nothing inside
|
||||
|
||||
yaml_util.load_yaml(yaml_file)
|
||||
assert loaded == []
|
||||
|
||||
|
||||
def test_track_yaml_loads_cleanup_on_exception(tmp_path: Path) -> None:
|
||||
"""Listener is removed even if the body raises."""
|
||||
before = len(yaml_util._load_listeners)
|
||||
|
||||
with pytest.raises(RuntimeError), yaml_util.track_yaml_loads():
|
||||
raise RuntimeError("boom")
|
||||
|
||||
assert len(yaml_util._load_listeners) == before
|
||||
|
||||
Reference in New Issue
Block a user