mirror of
https://github.com/esphome/esphome.git
synced 2026-09-15 09:08:41 +00:00
Merge remote-tracking branch 'upstream/dev' into integration
This commit is contained in:
@@ -70,7 +70,7 @@ jobs:
|
||||
pip3 install build
|
||||
python3 -m build
|
||||
- name: Publish
|
||||
uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # v1.13.0
|
||||
uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # v1.14.0
|
||||
with:
|
||||
skip-existing: true
|
||||
|
||||
|
||||
@@ -148,6 +148,7 @@ esphome/components/ee895/* @Stock-M
|
||||
esphome/components/ektf2232/touchscreen/* @jesserockz
|
||||
esphome/components/emc2101/* @ellull
|
||||
esphome/components/emmeti/* @E440QF
|
||||
esphome/components/emontx/* @FredM67 @glynhudson @TrystanLea
|
||||
esphome/components/ens160/* @latonita
|
||||
esphome/components/ens160_base/* @latonita @vincentscode
|
||||
esphome/components/ens160_i2c/* @latonita
|
||||
|
||||
@@ -1242,6 +1242,38 @@ 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
|
||||
|
||||
@@ -1517,6 +1549,7 @@ POST_CONFIG_ACTIONS = {
|
||||
"rename": command_rename,
|
||||
"discover": command_discover,
|
||||
"analyze-memory": command_analyze_memory,
|
||||
"bundle": command_bundle,
|
||||
}
|
||||
|
||||
SIMPLE_CONFIG_ACTIONS = [
|
||||
@@ -1818,6 +1851,24 @@ 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>.
|
||||
#
|
||||
@@ -1896,6 +1947,16 @@ 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,3 +1,4 @@
|
||||
from dataclasses import dataclass, field
|
||||
import logging
|
||||
|
||||
import esphome.codegen as cg
|
||||
@@ -715,3 +716,35 @@ async def build_callback_automation(
|
||||
# MockObjs (not user input), and there's no Expression type for positional
|
||||
# aggregate initialization (StructInitializer uses named fields).
|
||||
cg.add(getattr(parent, callback_method)(cg.RawExpression(f"{forwarder}{{{obj}}}")))
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class CallbackAutomation:
|
||||
"""A single callback automation entry for build_callback_automations."""
|
||||
|
||||
conf_key: str
|
||||
callback_method: str
|
||||
args: TemplateArgsType = field(default_factory=list)
|
||||
forwarder: MockObj | MockObjClass | None = None
|
||||
|
||||
|
||||
async def build_callback_automations(
|
||||
parent: MockObj,
|
||||
config: ConfigType,
|
||||
entries: tuple[CallbackAutomation, ...],
|
||||
) -> None:
|
||||
"""Build multiple callback automations from a tuple of entries.
|
||||
|
||||
:param parent: The component object (e.g., button, sensor).
|
||||
:param config: The full component config dict.
|
||||
:param entries: Tuple of CallbackAutomation entries to process.
|
||||
"""
|
||||
for entry in entries:
|
||||
for conf in config.get(entry.conf_key, []):
|
||||
await build_callback_automation(
|
||||
parent,
|
||||
entry.callback_method,
|
||||
entry.args,
|
||||
conf,
|
||||
forwarder=entry.forwarder,
|
||||
)
|
||||
|
||||
@@ -0,0 +1,699 @@
|
||||
"""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
|
||||
@@ -111,42 +111,66 @@ ALARM_CONTROL_PANEL_CONDITION_SCHEMA = maybe_simple_id(
|
||||
)
|
||||
|
||||
|
||||
_CALLBACK_AUTOMATIONS = (
|
||||
automation.CallbackAutomation(
|
||||
CONF_ON_STATE, "add_on_state_callback", forwarder=StateAnyForwarder
|
||||
),
|
||||
automation.CallbackAutomation(
|
||||
CONF_ON_TRIGGERED,
|
||||
"add_on_state_callback",
|
||||
forwarder=StateEnterForwarder.template(
|
||||
AlarmControlPanelState.ACP_STATE_TRIGGERED
|
||||
),
|
||||
),
|
||||
automation.CallbackAutomation(
|
||||
CONF_ON_ARMING,
|
||||
"add_on_state_callback",
|
||||
forwarder=StateEnterForwarder.template(AlarmControlPanelState.ACP_STATE_ARMING),
|
||||
),
|
||||
automation.CallbackAutomation(
|
||||
CONF_ON_PENDING,
|
||||
"add_on_state_callback",
|
||||
forwarder=StateEnterForwarder.template(
|
||||
AlarmControlPanelState.ACP_STATE_PENDING
|
||||
),
|
||||
),
|
||||
automation.CallbackAutomation(
|
||||
CONF_ON_ARMED_HOME,
|
||||
"add_on_state_callback",
|
||||
forwarder=StateEnterForwarder.template(
|
||||
AlarmControlPanelState.ACP_STATE_ARMED_HOME
|
||||
),
|
||||
),
|
||||
automation.CallbackAutomation(
|
||||
CONF_ON_ARMED_NIGHT,
|
||||
"add_on_state_callback",
|
||||
forwarder=StateEnterForwarder.template(
|
||||
AlarmControlPanelState.ACP_STATE_ARMED_NIGHT
|
||||
),
|
||||
),
|
||||
automation.CallbackAutomation(
|
||||
CONF_ON_ARMED_AWAY,
|
||||
"add_on_state_callback",
|
||||
forwarder=StateEnterForwarder.template(
|
||||
AlarmControlPanelState.ACP_STATE_ARMED_AWAY
|
||||
),
|
||||
),
|
||||
automation.CallbackAutomation(
|
||||
CONF_ON_DISARMED,
|
||||
"add_on_state_callback",
|
||||
forwarder=StateEnterForwarder.template(
|
||||
AlarmControlPanelState.ACP_STATE_DISARMED
|
||||
),
|
||||
),
|
||||
automation.CallbackAutomation(CONF_ON_CLEARED, "add_on_cleared_callback"),
|
||||
automation.CallbackAutomation(CONF_ON_CHIME, "add_on_chime_callback"),
|
||||
automation.CallbackAutomation(CONF_ON_READY, "add_on_ready_callback"),
|
||||
)
|
||||
|
||||
|
||||
@setup_entity("alarm_control_panel")
|
||||
async def setup_alarm_control_panel_core_(var, config):
|
||||
for conf in config.get(CONF_ON_STATE, []):
|
||||
await automation.build_callback_automation(
|
||||
var, "add_on_state_callback", [], conf, forwarder=StateAnyForwarder
|
||||
)
|
||||
_STATE_ENTER_MAP = {
|
||||
CONF_ON_TRIGGERED: AlarmControlPanelState.ACP_STATE_TRIGGERED,
|
||||
CONF_ON_ARMING: AlarmControlPanelState.ACP_STATE_ARMING,
|
||||
CONF_ON_PENDING: AlarmControlPanelState.ACP_STATE_PENDING,
|
||||
CONF_ON_ARMED_HOME: AlarmControlPanelState.ACP_STATE_ARMED_HOME,
|
||||
CONF_ON_ARMED_NIGHT: AlarmControlPanelState.ACP_STATE_ARMED_NIGHT,
|
||||
CONF_ON_ARMED_AWAY: AlarmControlPanelState.ACP_STATE_ARMED_AWAY,
|
||||
CONF_ON_DISARMED: AlarmControlPanelState.ACP_STATE_DISARMED,
|
||||
}
|
||||
for conf_key, state_enum in _STATE_ENTER_MAP.items():
|
||||
for conf in config.get(conf_key, []):
|
||||
await automation.build_callback_automation(
|
||||
var,
|
||||
"add_on_state_callback",
|
||||
[],
|
||||
conf,
|
||||
forwarder=StateEnterForwarder.template(state_enum),
|
||||
)
|
||||
for conf in config.get(CONF_ON_CLEARED, []):
|
||||
await automation.build_callback_automation(
|
||||
var, "add_on_cleared_callback", [], conf
|
||||
)
|
||||
for conf in config.get(CONF_ON_CHIME, []):
|
||||
await automation.build_callback_automation(
|
||||
var, "add_on_chime_callback", [], conf
|
||||
)
|
||||
for conf in config.get(CONF_ON_READY, []):
|
||||
await automation.build_callback_automation(
|
||||
var, "add_on_ready_callback", [], conf
|
||||
)
|
||||
await automation.build_callback_automations(var, config, _CALLBACK_AUTOMATIONS)
|
||||
if web_server_config := config.get(CONF_WEB_SERVER):
|
||||
await web_server.add_entity_config(var, web_server_config)
|
||||
if mqtt_id := config.get(CONF_MQTT_ID):
|
||||
|
||||
@@ -12,7 +12,7 @@ CODEOWNERS = ["@B48D81EFCC"]
|
||||
|
||||
sensor_ns = cg.esphome_ns.namespace("bh1900nux")
|
||||
BH1900NUXSensor = sensor_ns.class_(
|
||||
"BH1900NUXSensor", cg.PollingComponent, i2c.I2CDevice
|
||||
"BH1900NUXSensor", sensor.Sensor, cg.PollingComponent, i2c.I2CDevice
|
||||
)
|
||||
|
||||
CONFIG_SCHEMA = (
|
||||
|
||||
@@ -531,16 +531,31 @@ def binary_sensor_schema(
|
||||
return _BINARY_SENSOR_SCHEMA.extend(schema)
|
||||
|
||||
|
||||
_CALLBACK_AUTOMATIONS = (
|
||||
automation.CallbackAutomation(
|
||||
CONF_ON_PRESS,
|
||||
"add_on_state_callback",
|
||||
forwarder=automation.TriggerOnTrueForwarder,
|
||||
),
|
||||
automation.CallbackAutomation(
|
||||
CONF_ON_RELEASE,
|
||||
"add_on_state_callback",
|
||||
forwarder=automation.TriggerOnFalseForwarder,
|
||||
),
|
||||
automation.CallbackAutomation(
|
||||
CONF_ON_STATE, "add_on_state_callback", [(bool, "x")]
|
||||
),
|
||||
automation.CallbackAutomation(
|
||||
CONF_ON_STATE_CHANGE,
|
||||
"add_full_state_callback",
|
||||
[(cg.optional.template(bool), "x_previous"), (cg.optional.template(bool), "x")],
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@coroutine_with_priority(CoroPriority.AUTOMATION)
|
||||
async def _build_binary_sensor_automations(var, config):
|
||||
for conf_key, forwarder in (
|
||||
(CONF_ON_PRESS, automation.TriggerOnTrueForwarder),
|
||||
(CONF_ON_RELEASE, automation.TriggerOnFalseForwarder),
|
||||
):
|
||||
for conf in config.get(conf_key, []):
|
||||
await automation.build_callback_automation(
|
||||
var, "add_on_state_callback", [], conf, forwarder=forwarder
|
||||
)
|
||||
await automation.build_callback_automations(var, config, _CALLBACK_AUTOMATIONS)
|
||||
|
||||
for conf in config.get(CONF_ON_CLICK, []):
|
||||
trigger = cg.new_Pvariable(
|
||||
@@ -572,22 +587,6 @@ async def _build_binary_sensor_automations(var, config):
|
||||
await cg.register_component(trigger, conf)
|
||||
await automation.build_automation(trigger, [], conf)
|
||||
|
||||
for conf in config.get(CONF_ON_STATE, []):
|
||||
await automation.build_callback_automation(
|
||||
var, "add_on_state_callback", [(bool, "x")], conf
|
||||
)
|
||||
|
||||
for conf in config.get(CONF_ON_STATE_CHANGE, []):
|
||||
await automation.build_callback_automation(
|
||||
var,
|
||||
"add_full_state_callback",
|
||||
[
|
||||
(cg.optional.template(bool), "x_previous"),
|
||||
(cg.optional.template(bool), "x"),
|
||||
],
|
||||
conf,
|
||||
)
|
||||
|
||||
|
||||
@setup_entity("binary_sensor")
|
||||
async def setup_binary_sensor_core_(var, config):
|
||||
|
||||
@@ -14,7 +14,7 @@ CONFIG_SCHEMA = output.FLOAT_OUTPUT_SCHEMA.extend(
|
||||
{
|
||||
cv.GenerateID(CONF_BP1658CJ_ID): cv.use_id(BP1658CJ),
|
||||
cv.Required(CONF_ID): cv.declare_id(Channel),
|
||||
cv.Required(CONF_CHANNEL): cv.int_range(min=0, max=65535),
|
||||
cv.Required(CONF_CHANNEL): cv.int_range(min=0, max=4),
|
||||
}
|
||||
).extend(cv.COMPONENT_SCHEMA)
|
||||
|
||||
|
||||
@@ -79,12 +79,14 @@ def button_schema(
|
||||
return _BUTTON_SCHEMA.extend(schema)
|
||||
|
||||
|
||||
_CALLBACK_AUTOMATIONS = (
|
||||
automation.CallbackAutomation(CONF_ON_PRESS, "add_on_press_callback"),
|
||||
)
|
||||
|
||||
|
||||
@setup_entity("button")
|
||||
async def setup_button_core_(var, config):
|
||||
for conf in config.get(CONF_ON_PRESS, []):
|
||||
await automation.build_callback_automation(
|
||||
var, "add_on_press_callback", [], conf
|
||||
)
|
||||
await automation.build_callback_automations(var, config, _CALLBACK_AUTOMATIONS)
|
||||
|
||||
setup_device_class(config)
|
||||
|
||||
|
||||
@@ -64,15 +64,19 @@ FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema(
|
||||
)
|
||||
|
||||
|
||||
_CALLBACK_AUTOMATIONS = (
|
||||
automation.CallbackAutomation(
|
||||
CONF_ON_FINISHED_PLAYBACK, "add_on_finished_playback_callback"
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
await uart.register_uart_device(var, config)
|
||||
|
||||
for conf in config.get(CONF_ON_FINISHED_PLAYBACK, []):
|
||||
await automation.build_callback_automation(
|
||||
var, "add_on_finished_playback_callback", [], conf
|
||||
)
|
||||
await automation.build_callback_automations(var, config, _CALLBACK_AUTOMATIONS)
|
||||
|
||||
|
||||
@automation.register_action(
|
||||
|
||||
@@ -37,7 +37,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
cv.Optional(CONF_GAS_MBUS_ID, default=1): cv.int_,
|
||||
cv.Optional(CONF_WATER_MBUS_ID, default=2): cv.int_,
|
||||
cv.Optional(CONF_THERMAL_MBUS_ID, default=3): cv.int_,
|
||||
cv.Optional(CONF_MAX_TELEGRAM_LENGTH, default=1500): cv.int_,
|
||||
cv.Optional(CONF_MAX_TELEGRAM_LENGTH, default=1500): cv.int_range(min=1),
|
||||
cv.Optional(CONF_REQUEST_PIN): pins.gpio_output_pin_schema,
|
||||
cv.Optional(
|
||||
CONF_REQUEST_INTERVAL, default="0ms"
|
||||
|
||||
@@ -0,0 +1,152 @@
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
from esphome import automation
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import uart
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_COMMAND,
|
||||
CONF_ID,
|
||||
CONF_ON_DATA,
|
||||
CONF_RX_BUFFER_SIZE,
|
||||
CONF_UART_ID,
|
||||
)
|
||||
from esphome.core import CORE
|
||||
import esphome.final_validate as fv
|
||||
from esphome.types import ConfigType
|
||||
|
||||
AUTO_LOAD = ["json"]
|
||||
CODEOWNERS = ["@FredM67", "@TrystanLea", "@glynhudson"]
|
||||
DEPENDENCIES = ["uart"]
|
||||
|
||||
emontx_ns = cg.esphome_ns.namespace("emontx")
|
||||
EmonTx = emontx_ns.class_("EmonTx", cg.Component, uart.UARTDevice)
|
||||
|
||||
# Action to send command to emonTx
|
||||
EmonTxSendCommandAction = emontx_ns.class_("EmonTxSendCommandAction", automation.Action)
|
||||
|
||||
CONF_EMONTX_ID = "emontx_id"
|
||||
CONF_TAG_NAME = "tag_name"
|
||||
CONF_ON_JSON = "on_json"
|
||||
|
||||
DOMAIN = "emontx"
|
||||
|
||||
MINIMUM_RX_BUFFER_SIZE = 2048
|
||||
|
||||
|
||||
@dataclass
|
||||
class EmonTxData:
|
||||
sensor_counts: dict[str, int] = field(default_factory=dict)
|
||||
|
||||
|
||||
def _get_data() -> EmonTxData:
|
||||
if DOMAIN not in CORE.data:
|
||||
CORE.data[DOMAIN] = EmonTxData()
|
||||
return CORE.data[DOMAIN]
|
||||
|
||||
|
||||
# Main configuration schema
|
||||
CONFIG_SCHEMA = (
|
||||
cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(EmonTx),
|
||||
cv.Optional(CONF_ON_JSON): automation.validate_automation({}),
|
||||
cv.Optional(CONF_ON_DATA): automation.validate_automation({}),
|
||||
}
|
||||
)
|
||||
.extend(cv.COMPONENT_SCHEMA)
|
||||
.extend(uart.UART_DEVICE_SCHEMA)
|
||||
)
|
||||
|
||||
|
||||
def final_validate(config: ConfigType) -> ConfigType:
|
||||
full_config = fv.full_config.get()
|
||||
|
||||
# Count sensors registered to this hub (IDs are resolved at final_validate stage)
|
||||
hub_id = str(config[CONF_ID])
|
||||
sensor_count = sum(
|
||||
1
|
||||
for s in full_config.get("sensor", [])
|
||||
if s.get("platform") == "emontx" and str(s.get(CONF_EMONTX_ID)) == hub_id
|
||||
)
|
||||
_get_data().sensor_counts[hub_id] = sensor_count
|
||||
|
||||
# Ensure UART RX buffer size is large enough to handle data bursts from firmware
|
||||
for uart_conf in full_config["uart"]:
|
||||
if uart_conf[CONF_ID] == config[CONF_UART_ID]:
|
||||
current_buffer_size = uart_conf[CONF_RX_BUFFER_SIZE]
|
||||
if current_buffer_size < MINIMUM_RX_BUFFER_SIZE:
|
||||
raise cv.Invalid(
|
||||
f"Component emontx requires UART '{config[CONF_UART_ID]}' to have "
|
||||
f"rx_buffer_size of at least {MINIMUM_RX_BUFFER_SIZE} bytes "
|
||||
f"(currently set to {current_buffer_size} bytes). "
|
||||
f"Please add 'rx_buffer_size: {MINIMUM_RX_BUFFER_SIZE}' to your uart configuration.",
|
||||
path=[CONF_UART_ID],
|
||||
)
|
||||
break
|
||||
|
||||
# Validate UART settings
|
||||
schema = uart.final_validate_device_schema(
|
||||
"emontx",
|
||||
baud_rate=115200,
|
||||
require_tx=False,
|
||||
require_rx=True,
|
||||
data_bits=8,
|
||||
parity="NONE",
|
||||
stop_bits=1,
|
||||
)
|
||||
return schema(config)
|
||||
|
||||
|
||||
FINAL_VALIDATE_SCHEMA = final_validate
|
||||
|
||||
|
||||
_CALLBACK_AUTOMATIONS = (
|
||||
automation.CallbackAutomation(
|
||||
CONF_ON_JSON,
|
||||
"add_on_json_callback",
|
||||
[(cg.JsonObject, "json"), (cg.std_string, "raw_json")],
|
||||
),
|
||||
automation.CallbackAutomation(
|
||||
CONF_ON_DATA, "add_on_data_callback", [(cg.std_string, "data")]
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
await uart.register_uart_device(var, config)
|
||||
|
||||
# Initialize sensor storage with count from final_validate
|
||||
sensor_count = _get_data().sensor_counts.get(str(config[CONF_ID]), 0)
|
||||
if sensor_count > 0:
|
||||
cg.add(var.init_sensors(sensor_count))
|
||||
|
||||
await automation.build_callback_automations(var, config, _CALLBACK_AUTOMATIONS)
|
||||
|
||||
|
||||
# Action: emontx.send_command
|
||||
|
||||
EMONTX_SEND_COMMAND_ACTION_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.use_id(EmonTx),
|
||||
cv.Required(CONF_COMMAND): cv.templatable(cv.string),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@automation.register_action(
|
||||
"emontx.send_command",
|
||||
EmonTxSendCommandAction,
|
||||
EMONTX_SEND_COMMAND_ACTION_SCHEMA,
|
||||
synchronous=True,
|
||||
)
|
||||
async def emontx_send_command_action_to_code(
|
||||
config: ConfigType, action_id, template_arg, args
|
||||
) -> None:
|
||||
var = cg.new_Pvariable(action_id, template_arg)
|
||||
await cg.register_parented(var, config[CONF_ID])
|
||||
template_ = await cg.templatable(config[CONF_COMMAND], args, cg.std_string)
|
||||
cg.add(var.set_command(template_))
|
||||
return var
|
||||
@@ -0,0 +1,116 @@
|
||||
#include "emontx.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/components/json/json_util.h"
|
||||
|
||||
namespace esphome::emontx {
|
||||
|
||||
static const char *const TAG = "emontx";
|
||||
|
||||
void EmonTx::setup() { this->buffer_pos_ = 0; }
|
||||
|
||||
/**
|
||||
* @brief Implements the main loop for parsing data from the serial port.
|
||||
*
|
||||
* @details Continuously processes incoming UART data line-by-line:
|
||||
* 1. Fire on_data callbacks for all received lines
|
||||
* 2. If line starts with '{', parse as JSON and update sensors/callbacks
|
||||
*/
|
||||
void EmonTx::loop() {
|
||||
// Read all available data to prevent UART buffer overflow
|
||||
while (this->available() > 0) {
|
||||
uint8_t received = this->read();
|
||||
|
||||
if (received == '\r') {
|
||||
continue; // Ignore CR
|
||||
} else if (received == '\n') {
|
||||
// End of line - process the buffer
|
||||
if (this->buffer_pos_ > 0) {
|
||||
// Null-terminate for safe logging and c_str() use
|
||||
size_t len = this->buffer_pos_;
|
||||
this->buffer_[len] = '\0';
|
||||
this->buffer_pos_ = 0;
|
||||
|
||||
StringRef line(this->buffer_.data(), len);
|
||||
ESP_LOGD(TAG, "Received line: %s", line.c_str());
|
||||
|
||||
// Fire data callbacks for all received lines
|
||||
this->data_callbacks_.call(line);
|
||||
|
||||
// Check if this line is JSON (starts with '{')
|
||||
if (this->buffer_[0] == '{') {
|
||||
ESP_LOGV(TAG, "Line is JSON, parsing...");
|
||||
this->parse_json_(this->buffer_.data(), len);
|
||||
}
|
||||
}
|
||||
} else if (this->buffer_pos_ >= MAX_LINE_LENGTH) {
|
||||
ESP_LOGW(TAG, "Buffer overflow (>%zu bytes), discarding buffer", MAX_LINE_LENGTH);
|
||||
this->buffer_pos_ = 0;
|
||||
} else {
|
||||
this->buffer_[this->buffer_pos_++] = static_cast<char>(received);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void EmonTx::parse_json_(const char *data, size_t len) {
|
||||
bool success = json::parse_json(reinterpret_cast<const uint8_t *>(data), len, [this, data, len](JsonObject root) {
|
||||
#ifdef USE_SENSOR
|
||||
for (auto &sensor_pair : this->sensors_) {
|
||||
auto val = root[sensor_pair.first];
|
||||
if (val.is<JsonVariant>()) {
|
||||
float value = val;
|
||||
ESP_LOGV(TAG, "Updating sensor '%s' with value: %.2f", sensor_pair.first, value);
|
||||
sensor_pair.second->publish_state(value);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
this->json_callbacks_.call(root, StringRef(data, len));
|
||||
return true;
|
||||
});
|
||||
|
||||
if (!success) {
|
||||
ESP_LOGW(TAG, "Failed to parse JSON");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Logs the EmonTx component configuration details.
|
||||
*/
|
||||
void EmonTx::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "EmonTx:");
|
||||
|
||||
#ifdef USE_SENSOR
|
||||
ESP_LOGCONFIG(TAG, " Registered sensors: %zu", this->sensors_.size());
|
||||
for (const auto &sensor_pair : this->sensors_) {
|
||||
ESP_LOGCONFIG(TAG, " Sensor: %s", sensor_pair.first);
|
||||
}
|
||||
#else
|
||||
ESP_LOGCONFIG(TAG, " Sensor support: DISABLED");
|
||||
#endif
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Sends a command string to the emonTx device via UART.
|
||||
*
|
||||
* @param command The command string to send (LF will be appended automatically).
|
||||
*/
|
||||
void EmonTx::send_command(const std::string &command) {
|
||||
ESP_LOGD(TAG, "Sending command to emonTx: %s", command.c_str());
|
||||
this->write_str(command.c_str());
|
||||
this->write_byte('\n');
|
||||
}
|
||||
|
||||
#ifdef USE_SENSOR
|
||||
/**
|
||||
* @brief Registers a sensor to receive updates for a specific JSON tag.
|
||||
*
|
||||
* @param tag_name The JSON key to monitor for this sensor (must be a string literal).
|
||||
* @param sensor Pointer to the sensor that will receive value updates.
|
||||
*/
|
||||
void EmonTx::register_sensor(const char *tag_name, sensor::Sensor *sensor) {
|
||||
ESP_LOGCONFIG(TAG, "Registering sensor for tag: %s", tag_name);
|
||||
this->sensors_.emplace_back(tag_name, sensor);
|
||||
}
|
||||
#endif
|
||||
|
||||
} // namespace esphome::emontx
|
||||
@@ -0,0 +1,69 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/defines.h"
|
||||
#include "esphome/core/automation.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/string_ref.h"
|
||||
#include "esphome/components/uart/uart.h"
|
||||
#include "esphome/components/json/json_util.h"
|
||||
|
||||
#include <array>
|
||||
|
||||
#ifdef USE_SENSOR
|
||||
#include "esphome/components/sensor/sensor.h"
|
||||
#endif
|
||||
|
||||
namespace esphome::emontx {
|
||||
|
||||
/// Maximum line length in bytes (plus one byte reserved for null terminator)
|
||||
static constexpr size_t MAX_LINE_LENGTH = 1024;
|
||||
|
||||
/**
|
||||
* @class EmonTx
|
||||
* @brief Main class for the EmonTx component.
|
||||
*
|
||||
* The EmonTx processes incoming data frames via UART,
|
||||
* extracts tags and values, and publishes them to registered sensors.
|
||||
*/
|
||||
class EmonTx : public Component, public uart::UARTDevice {
|
||||
public:
|
||||
EmonTx() = default;
|
||||
|
||||
void loop() override;
|
||||
void setup() override;
|
||||
void dump_config() override;
|
||||
|
||||
template<typename F> void add_on_json_callback(F &&callback) { this->json_callbacks_.add(std::forward<F>(callback)); }
|
||||
|
||||
template<typename F> void add_on_data_callback(F &&callback) { this->data_callbacks_.add(std::forward<F>(callback)); }
|
||||
|
||||
// Send command to emonTx via UART
|
||||
void send_command(const std::string &command);
|
||||
|
||||
#ifdef USE_SENSOR
|
||||
void init_sensors(size_t count) { this->sensors_.init(count); }
|
||||
void register_sensor(const char *tag_name, sensor::Sensor *sensor);
|
||||
#endif
|
||||
|
||||
protected:
|
||||
void parse_json_(const char *data, size_t len);
|
||||
|
||||
#ifdef USE_SENSOR
|
||||
FixedVector<std::pair<const char *, sensor::Sensor *>> sensors_{};
|
||||
#endif
|
||||
LazyCallbackManager<void(JsonObject, StringRef)> json_callbacks_;
|
||||
LazyCallbackManager<void(StringRef)> data_callbacks_;
|
||||
uint16_t buffer_pos_{0};
|
||||
std::array<char, MAX_LINE_LENGTH + 1> buffer_{};
|
||||
};
|
||||
|
||||
// Action to send command to emonTx
|
||||
template<typename... Ts> class EmonTxSendCommandAction : public Action<Ts...>, public Parented<EmonTx> {
|
||||
public:
|
||||
TEMPLATABLE_VALUE(std::string, command)
|
||||
|
||||
void play(const Ts &...x) override { this->parent_->send_command(this->command_.value(x...)); }
|
||||
};
|
||||
|
||||
} // namespace esphome::emontx
|
||||
@@ -0,0 +1,133 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import sensor
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_ACCURACY_DECIMALS,
|
||||
CONF_DEVICE_CLASS,
|
||||
CONF_ID,
|
||||
CONF_STATE_CLASS,
|
||||
CONF_UNIT_OF_MEASUREMENT,
|
||||
DEVICE_CLASS_CURRENT,
|
||||
DEVICE_CLASS_ENERGY,
|
||||
DEVICE_CLASS_POWER,
|
||||
DEVICE_CLASS_POWER_FACTOR,
|
||||
DEVICE_CLASS_TEMPERATURE,
|
||||
DEVICE_CLASS_VOLTAGE,
|
||||
STATE_CLASS_MEASUREMENT,
|
||||
STATE_CLASS_TOTAL_INCREASING,
|
||||
UNIT_AMPERE,
|
||||
UNIT_CELSIUS,
|
||||
UNIT_EMPTY,
|
||||
UNIT_PULSES,
|
||||
UNIT_VOLT,
|
||||
UNIT_WATT,
|
||||
UNIT_WATT_HOURS,
|
||||
)
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from .. import CONF_EMONTX_ID, CONF_TAG_NAME, EmonTx, emontx_ns
|
||||
|
||||
EmonTxSensor = emontx_ns.class_("EmonTxSensor", sensor.Sensor, cg.Component)
|
||||
|
||||
# Define sensor type configurations by prefix
|
||||
SENSOR_CONFIGS = {
|
||||
"P": {
|
||||
CONF_UNIT_OF_MEASUREMENT: UNIT_WATT,
|
||||
CONF_DEVICE_CLASS: DEVICE_CLASS_POWER,
|
||||
CONF_STATE_CLASS: STATE_CLASS_MEASUREMENT,
|
||||
CONF_ACCURACY_DECIMALS: 0,
|
||||
},
|
||||
"E": {
|
||||
CONF_UNIT_OF_MEASUREMENT: UNIT_WATT_HOURS,
|
||||
CONF_DEVICE_CLASS: DEVICE_CLASS_ENERGY,
|
||||
CONF_STATE_CLASS: STATE_CLASS_TOTAL_INCREASING,
|
||||
CONF_ACCURACY_DECIMALS: 0,
|
||||
},
|
||||
"V": {
|
||||
CONF_UNIT_OF_MEASUREMENT: UNIT_VOLT,
|
||||
CONF_DEVICE_CLASS: DEVICE_CLASS_VOLTAGE,
|
||||
CONF_STATE_CLASS: STATE_CLASS_MEASUREMENT,
|
||||
CONF_ACCURACY_DECIMALS: 2,
|
||||
},
|
||||
"I": {
|
||||
CONF_UNIT_OF_MEASUREMENT: UNIT_AMPERE,
|
||||
CONF_DEVICE_CLASS: DEVICE_CLASS_CURRENT,
|
||||
CONF_STATE_CLASS: STATE_CLASS_MEASUREMENT,
|
||||
CONF_ACCURACY_DECIMALS: 2,
|
||||
},
|
||||
"T": {
|
||||
CONF_UNIT_OF_MEASUREMENT: UNIT_CELSIUS,
|
||||
CONF_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE,
|
||||
CONF_STATE_CLASS: STATE_CLASS_MEASUREMENT,
|
||||
CONF_ACCURACY_DECIMALS: 2,
|
||||
},
|
||||
}
|
||||
|
||||
# Pattern-based configurations
|
||||
PATTERN_CONFIGS = {
|
||||
"PULSE": {
|
||||
CONF_UNIT_OF_MEASUREMENT: UNIT_PULSES,
|
||||
CONF_DEVICE_CLASS: DEVICE_CLASS_ENERGY,
|
||||
CONF_ACCURACY_DECIMALS: 0,
|
||||
},
|
||||
"PF": {
|
||||
CONF_UNIT_OF_MEASUREMENT: UNIT_EMPTY,
|
||||
CONF_DEVICE_CLASS: DEVICE_CLASS_POWER_FACTOR,
|
||||
CONF_STATE_CLASS: STATE_CLASS_MEASUREMENT,
|
||||
CONF_ACCURACY_DECIMALS: 2,
|
||||
},
|
||||
}
|
||||
|
||||
# Create a base schema that's flexible for any tag
|
||||
BASE_SCHEMA = sensor.sensor_schema(
|
||||
EmonTxSensor,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
accuracy_decimals=0,
|
||||
).extend(
|
||||
{
|
||||
cv.GenerateID(CONF_EMONTX_ID): cv.use_id(EmonTx),
|
||||
cv.Required(CONF_TAG_NAME): cv.string,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def apply_tag_defaults(config: ConfigType) -> ConfigType:
|
||||
"""Apply defaults based on tag prefix if applicable, but don't restrict any tags."""
|
||||
tag = config[CONF_TAG_NAME]
|
||||
|
||||
# Skip if tag is too short
|
||||
if len(tag) < 2:
|
||||
return config
|
||||
|
||||
# Check if this tag starts with a known prefix
|
||||
tag_upper = tag.upper()
|
||||
|
||||
for pattern, pattern_config in PATTERN_CONFIGS.items():
|
||||
if tag_upper.startswith(pattern):
|
||||
# Apply pattern defaults if not overridden by user
|
||||
for key, value in pattern_config.items():
|
||||
if key not in config:
|
||||
config[key] = value
|
||||
return config
|
||||
|
||||
# Only apply defaults for known prefixes with numeric indices
|
||||
prefix = tag_upper[0]
|
||||
if prefix in SENSOR_CONFIGS and len(tag) > 1 and tag[1:].isdigit():
|
||||
# Apply defaults for known tag types, but only if not overridden by user
|
||||
defaults = SENSOR_CONFIGS[prefix]
|
||||
for key, value in defaults.items():
|
||||
if key not in config:
|
||||
config[key] = value
|
||||
|
||||
return config
|
||||
|
||||
|
||||
CONFIG_SCHEMA = cv.All(BASE_SCHEMA, apply_tag_defaults)
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
await sensor.register_sensor(var, config)
|
||||
hub = await cg.get_variable(config[CONF_EMONTX_ID])
|
||||
cg.add(hub.register_sensor(config[CONF_TAG_NAME], var))
|
||||
@@ -0,0 +1,10 @@
|
||||
#include "emontx_sensor.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome::emontx {
|
||||
|
||||
static const char *const TAG = "emontx_sensor";
|
||||
|
||||
void EmonTxSensor::dump_config() { LOG_SENSOR(" ", "EmonTx Sensor", this); }
|
||||
|
||||
} // namespace esphome::emontx
|
||||
@@ -0,0 +1,13 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/components/sensor/sensor.h"
|
||||
#include "esphome/core/component.h"
|
||||
|
||||
namespace esphome::emontx {
|
||||
|
||||
class EmonTxSensor : public sensor::Sensor, public Component {
|
||||
public:
|
||||
void dump_config() override;
|
||||
};
|
||||
|
||||
} // namespace esphome::emontx
|
||||
@@ -82,12 +82,16 @@ def event_schema(
|
||||
return _EVENT_SCHEMA.extend(schema)
|
||||
|
||||
|
||||
_CALLBACK_AUTOMATIONS = (
|
||||
automation.CallbackAutomation(
|
||||
CONF_ON_EVENT, "add_on_event_callback", [(cg.StringRef, "event_type")]
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@setup_entity("event")
|
||||
async def setup_event_core_(var, config, *, event_types: list[str]):
|
||||
for conf in config.get(CONF_ON_EVENT, []):
|
||||
await automation.build_callback_automation(
|
||||
var, "add_on_event_callback", [(cg.StringRef, "event_type")], conf
|
||||
)
|
||||
await automation.build_callback_automations(var, config, _CALLBACK_AUTOMATIONS)
|
||||
|
||||
cg.add(var.set_event_types(event_types))
|
||||
|
||||
|
||||
@@ -38,33 +38,30 @@ CONFIG_SCHEMA = (
|
||||
)
|
||||
|
||||
|
||||
_CALLBACK_AUTOMATIONS = (
|
||||
automation.CallbackAutomation(
|
||||
CONF_ON_CUSTOM, "add_custom_callback", [(cg.std_string, "x")]
|
||||
),
|
||||
automation.CallbackAutomation(CONF_ON_LED, "add_led_state_callback", [(bool, "x")]),
|
||||
automation.CallbackAutomation(
|
||||
CONF_ON_DEVICE_INFORMATION,
|
||||
"add_device_infomation_callback",
|
||||
[(cg.std_string, "x")],
|
||||
),
|
||||
automation.CallbackAutomation(
|
||||
CONF_ON_SLOPE, "add_slope_callback", [(cg.std_string, "x")]
|
||||
),
|
||||
automation.CallbackAutomation(
|
||||
CONF_ON_CALIBRATION, "add_calibration_callback", [(cg.std_string, "x")]
|
||||
),
|
||||
automation.CallbackAutomation(CONF_ON_T, "add_t_callback", [(cg.std_string, "x")]),
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
await sensor.register_sensor(var, config)
|
||||
await i2c.register_i2c_device(var, config)
|
||||
|
||||
for conf in config.get(CONF_ON_CUSTOM, []):
|
||||
await automation.build_callback_automation(
|
||||
var, "add_custom_callback", [(cg.std_string, "x")], conf
|
||||
)
|
||||
for conf in config.get(CONF_ON_LED, []):
|
||||
await automation.build_callback_automation(
|
||||
var, "add_led_state_callback", [(bool, "x")], conf
|
||||
)
|
||||
for conf in config.get(CONF_ON_DEVICE_INFORMATION, []):
|
||||
await automation.build_callback_automation(
|
||||
var, "add_device_infomation_callback", [(cg.std_string, "x")], conf
|
||||
)
|
||||
for conf in config.get(CONF_ON_SLOPE, []):
|
||||
await automation.build_callback_automation(
|
||||
var, "add_slope_callback", [(cg.std_string, "x")], conf
|
||||
)
|
||||
for conf in config.get(CONF_ON_CALIBRATION, []):
|
||||
await automation.build_callback_automation(
|
||||
var, "add_calibration_callback", [(cg.std_string, "x")], conf
|
||||
)
|
||||
for conf in config.get(CONF_ON_T, []):
|
||||
await automation.build_callback_automation(
|
||||
var, "add_t_callback", [(cg.std_string, "x")], conf
|
||||
)
|
||||
await automation.build_callback_automations(var, config, _CALLBACK_AUTOMATIONS)
|
||||
|
||||
@@ -73,6 +73,15 @@ def _final_validate(config):
|
||||
FINAL_VALIDATE_SCHEMA = _final_validate
|
||||
|
||||
|
||||
_CALLBACK_AUTOMATIONS = (
|
||||
automation.CallbackAutomation(
|
||||
CONF_ON_INCREMENT,
|
||||
"add_increment_callback",
|
||||
[(cg.uint8, "x"), (cg.uint8, "target")],
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
if reset_count := config.get(CONF_RESETS_REQUIRED):
|
||||
var = cg.new_Pvariable(
|
||||
@@ -81,10 +90,4 @@ async def to_code(config):
|
||||
config[CONF_MAX_DELAY].total_seconds,
|
||||
)
|
||||
await cg.register_component(var, config)
|
||||
for conf in config.get(CONF_ON_INCREMENT, []):
|
||||
await automation.build_callback_automation(
|
||||
var,
|
||||
"add_increment_callback",
|
||||
[(cg.uint8, "x"), (cg.uint8, "target")],
|
||||
conf,
|
||||
)
|
||||
await automation.build_callback_automations(var, config, _CALLBACK_AUTOMATIONS)
|
||||
|
||||
@@ -116,6 +116,44 @@ CONFIG_SCHEMA = cv.All(
|
||||
)
|
||||
|
||||
|
||||
_CALLBACK_AUTOMATIONS = (
|
||||
automation.CallbackAutomation(
|
||||
CONF_ON_FINGER_SCAN_START, "add_on_finger_scan_start_callback"
|
||||
),
|
||||
automation.CallbackAutomation(
|
||||
CONF_ON_FINGER_SCAN_MATCHED,
|
||||
"add_on_finger_scan_matched_callback",
|
||||
[(cg.uint16, "finger_id"), (cg.uint16, "confidence")],
|
||||
),
|
||||
automation.CallbackAutomation(
|
||||
CONF_ON_FINGER_SCAN_UNMATCHED,
|
||||
"add_on_finger_scan_unmatched_callback",
|
||||
),
|
||||
automation.CallbackAutomation(
|
||||
CONF_ON_FINGER_SCAN_MISPLACED,
|
||||
"add_on_finger_scan_misplaced_callback",
|
||||
),
|
||||
automation.CallbackAutomation(
|
||||
CONF_ON_FINGER_SCAN_INVALID, "add_on_finger_scan_invalid_callback"
|
||||
),
|
||||
automation.CallbackAutomation(
|
||||
CONF_ON_ENROLLMENT_SCAN,
|
||||
"add_on_enrollment_scan_callback",
|
||||
[(cg.uint8, "scan_num"), (cg.uint16, "finger_id")],
|
||||
),
|
||||
automation.CallbackAutomation(
|
||||
CONF_ON_ENROLLMENT_DONE,
|
||||
"add_on_enrollment_done_callback",
|
||||
[(cg.uint16, "finger_id")],
|
||||
),
|
||||
automation.CallbackAutomation(
|
||||
CONF_ON_ENROLLMENT_FAILED,
|
||||
"add_on_enrollment_failed_callback",
|
||||
[(cg.uint16, "finger_id")],
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
@@ -140,44 +178,7 @@ async def to_code(config):
|
||||
idle_period_to_sleep_ms = config[CONF_IDLE_PERIOD_TO_SLEEP]
|
||||
cg.add(var.set_idle_period_to_sleep_ms(idle_period_to_sleep_ms))
|
||||
|
||||
for conf in config.get(CONF_ON_FINGER_SCAN_START, []):
|
||||
await automation.build_callback_automation(
|
||||
var, "add_on_finger_scan_start_callback", [], conf
|
||||
)
|
||||
for conf in config.get(CONF_ON_FINGER_SCAN_MATCHED, []):
|
||||
await automation.build_callback_automation(
|
||||
var,
|
||||
"add_on_finger_scan_matched_callback",
|
||||
[(cg.uint16, "finger_id"), (cg.uint16, "confidence")],
|
||||
conf,
|
||||
)
|
||||
for conf in config.get(CONF_ON_FINGER_SCAN_UNMATCHED, []):
|
||||
await automation.build_callback_automation(
|
||||
var, "add_on_finger_scan_unmatched_callback", [], conf
|
||||
)
|
||||
for conf in config.get(CONF_ON_FINGER_SCAN_MISPLACED, []):
|
||||
await automation.build_callback_automation(
|
||||
var, "add_on_finger_scan_misplaced_callback", [], conf
|
||||
)
|
||||
for conf in config.get(CONF_ON_FINGER_SCAN_INVALID, []):
|
||||
await automation.build_callback_automation(
|
||||
var, "add_on_finger_scan_invalid_callback", [], conf
|
||||
)
|
||||
for conf in config.get(CONF_ON_ENROLLMENT_SCAN, []):
|
||||
await automation.build_callback_automation(
|
||||
var,
|
||||
"add_on_enrollment_scan_callback",
|
||||
[(cg.uint8, "scan_num"), (cg.uint16, "finger_id")],
|
||||
conf,
|
||||
)
|
||||
for conf in config.get(CONF_ON_ENROLLMENT_DONE, []):
|
||||
await automation.build_callback_automation(
|
||||
var, "add_on_enrollment_done_callback", [(cg.uint16, "finger_id")], conf
|
||||
)
|
||||
for conf in config.get(CONF_ON_ENROLLMENT_FAILED, []):
|
||||
await automation.build_callback_automation(
|
||||
var, "add_on_enrollment_failed_callback", [(cg.uint16, "finger_id")], conf
|
||||
)
|
||||
await automation.build_callback_automations(var, config, _CALLBACK_AUTOMATIONS)
|
||||
|
||||
|
||||
@automation.register_action(
|
||||
|
||||
@@ -13,7 +13,7 @@ DEPENDENCIES = ["i2c"]
|
||||
|
||||
gl_r01_i2c_ns = cg.esphome_ns.namespace("gl_r01_i2c")
|
||||
GLR01I2CComponent = gl_r01_i2c_ns.class_(
|
||||
"GLR01I2CComponent", i2c.I2CDevice, cg.PollingComponent
|
||||
"GLR01I2CComponent", sensor.Sensor, i2c.I2CDevice, cg.PollingComponent
|
||||
)
|
||||
|
||||
CONFIG_SCHEMA = (
|
||||
|
||||
@@ -110,7 +110,7 @@ GRAPH_SCHEMA = cv.Schema(
|
||||
cv.Optional(CONF_MIN_RANGE): cv.float_range(min=0, min_included=False),
|
||||
cv.Optional(CONF_MAX_RANGE): cv.float_range(min=0, min_included=False),
|
||||
cv.Optional(CONF_TRACES): cv.ensure_list(GRAPH_TRACE_SCHEMA),
|
||||
cv.Optional(CONF_LEGEND): cv.ensure_list(GRAPH_LEGEND_SCHEMA),
|
||||
cv.Optional(CONF_LEGEND): GRAPH_LEGEND_SCHEMA,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -192,7 +192,7 @@ async def to_code(config):
|
||||
cg.add(var.add_trace(tr))
|
||||
# Add legend
|
||||
if CONF_LEGEND in config:
|
||||
lgd = config[CONF_LEGEND][0]
|
||||
lgd = config[CONF_LEGEND]
|
||||
legend = cg.new_Pvariable(lgd[CONF_ID], GraphLegend())
|
||||
if CONF_NAME_FONT in lgd:
|
||||
font = await cg.get_variable(lgd[CONF_NAME_FONT])
|
||||
|
||||
@@ -80,11 +80,9 @@ async def grove_tb6612fng_run_to_code(config, action_id, template_arg, args):
|
||||
|
||||
template_channel = await cg.templatable(config[CONF_CHANNEL], args, int)
|
||||
template_speed = await cg.templatable(config[CONF_SPEED], args, cg.uint16)
|
||||
template_speed = (
|
||||
template_speed if config[CONF_DIRECTION] == "FORWARD" else -template_speed
|
||||
)
|
||||
cg.add(var.set_channel(template_channel))
|
||||
cg.add(var.set_speed(template_speed))
|
||||
cg.add(var.set_direction(config[CONF_DIRECTION] == "FORWARD"))
|
||||
return var
|
||||
|
||||
|
||||
|
||||
@@ -168,11 +168,19 @@ class GROVETB6612FNGMotorRunAction : public Action<Ts...>, public Parented<Grove
|
||||
TEMPLATABLE_VALUE(uint8_t, channel)
|
||||
TEMPLATABLE_VALUE(uint16_t, speed)
|
||||
|
||||
void set_direction(bool forward) { this->forward_ = forward; }
|
||||
|
||||
void play(const Ts &...x) override {
|
||||
auto channel = this->channel_.value(x...);
|
||||
auto speed = this->speed_.value(x...);
|
||||
int16_t speed = this->speed_.value(x...);
|
||||
if (!this->forward_) {
|
||||
speed = -speed;
|
||||
}
|
||||
this->parent_->dc_motor_run(channel, speed);
|
||||
}
|
||||
|
||||
protected:
|
||||
bool forward_{true};
|
||||
};
|
||||
|
||||
template<typename... Ts>
|
||||
|
||||
@@ -215,9 +215,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
{
|
||||
cv.Optional(
|
||||
CONF_CONTROL_METHOD, default="SET_GROUP_PARAMETERS"
|
||||
): cv.ensure_list(
|
||||
cv.enum(SUPPORTED_HON_CONTROL_METHODS, upper=True)
|
||||
),
|
||||
): cv.enum(SUPPORTED_HON_CONTROL_METHODS, upper=True),
|
||||
cv.Optional(CONF_BEEPER): cv.invalid(
|
||||
f"The {CONF_BEEPER} option is deprecated, use beeper_on/beeper_off actions or beeper switch for a haier platform instead"
|
||||
),
|
||||
@@ -456,6 +454,25 @@ def _final_validate(config):
|
||||
FINAL_VALIDATE_SCHEMA = _final_validate
|
||||
|
||||
|
||||
_CALLBACK_AUTOMATIONS = (
|
||||
automation.CallbackAutomation(
|
||||
CONF_ON_ALARM_START,
|
||||
"add_alarm_start_callback",
|
||||
[(cg.uint8, "code"), (cg.const_char_ptr, "message")],
|
||||
),
|
||||
automation.CallbackAutomation(
|
||||
CONF_ON_ALARM_END,
|
||||
"add_alarm_end_callback",
|
||||
[(cg.uint8, "code"), (cg.const_char_ptr, "message")],
|
||||
),
|
||||
automation.CallbackAutomation(
|
||||
CONF_ON_STATUS_MESSAGE,
|
||||
"add_status_message_callback",
|
||||
[(cg.const_char_ptr, "data"), (cg.size_t, "data_size")],
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
cg.add(haier_ns.init_haier_protocol_logging())
|
||||
var = await climate.new_climate(config)
|
||||
@@ -497,26 +514,6 @@ async def to_code(config):
|
||||
cg.add(
|
||||
var.set_status_message_header_size(config[CONF_STATUS_MESSAGE_HEADER_SIZE])
|
||||
)
|
||||
for conf in config.get(CONF_ON_ALARM_START, []):
|
||||
await automation.build_callback_automation(
|
||||
var,
|
||||
"add_alarm_start_callback",
|
||||
[(cg.uint8, "code"), (cg.const_char_ptr, "message")],
|
||||
conf,
|
||||
)
|
||||
for conf in config.get(CONF_ON_ALARM_END, []):
|
||||
await automation.build_callback_automation(
|
||||
var,
|
||||
"add_alarm_end_callback",
|
||||
[(cg.uint8, "code"), (cg.const_char_ptr, "message")],
|
||||
conf,
|
||||
)
|
||||
for conf in config.get(CONF_ON_STATUS_MESSAGE, []):
|
||||
await automation.build_callback_automation(
|
||||
var,
|
||||
"add_status_message_callback",
|
||||
[(cg.const_char_ptr, "data"), (cg.size_t, "data_size")],
|
||||
conf,
|
||||
)
|
||||
await automation.build_callback_automations(var, config, _CALLBACK_AUTOMATIONS)
|
||||
# https://github.com/paveldn/HaierProtocol
|
||||
cg.add_library("pavlodn/HaierProtocol", "0.9.31")
|
||||
|
||||
@@ -52,58 +52,53 @@ CONFIG_SCHEMA = cv.All(
|
||||
)
|
||||
|
||||
|
||||
_CALLBACK_AUTOMATIONS = (
|
||||
automation.CallbackAutomation(
|
||||
CONF_ON_FACE_SCAN_MATCHED,
|
||||
"add_on_face_scan_matched_callback",
|
||||
[(cg.int16, "face_id"), (cg.std_string, "name")],
|
||||
),
|
||||
automation.CallbackAutomation(
|
||||
CONF_ON_FACE_SCAN_UNMATCHED, "add_on_face_scan_unmatched_callback"
|
||||
),
|
||||
automation.CallbackAutomation(
|
||||
CONF_ON_FACE_SCAN_INVALID,
|
||||
"add_on_face_scan_invalid_callback",
|
||||
[(cg.uint8, "error")],
|
||||
),
|
||||
automation.CallbackAutomation(
|
||||
CONF_ON_FACE_INFO,
|
||||
"add_on_face_info_callback",
|
||||
[
|
||||
(cg.int16, "status"),
|
||||
(cg.int16, "left"),
|
||||
(cg.int16, "top"),
|
||||
(cg.int16, "right"),
|
||||
(cg.int16, "bottom"),
|
||||
(cg.int16, "yaw"),
|
||||
(cg.int16, "pitch"),
|
||||
(cg.int16, "roll"),
|
||||
],
|
||||
),
|
||||
automation.CallbackAutomation(
|
||||
CONF_ON_ENROLLMENT_DONE,
|
||||
"add_on_enrollment_done_callback",
|
||||
[(cg.int16, "face_id"), (cg.uint8, "direction")],
|
||||
),
|
||||
automation.CallbackAutomation(
|
||||
CONF_ON_ENROLLMENT_FAILED,
|
||||
"add_on_enrollment_failed_callback",
|
||||
[(cg.uint8, "error")],
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
await uart.register_uart_device(var, config)
|
||||
|
||||
for conf in config.get(CONF_ON_FACE_SCAN_MATCHED, []):
|
||||
await automation.build_callback_automation(
|
||||
var,
|
||||
"add_on_face_scan_matched_callback",
|
||||
[(cg.int16, "face_id"), (cg.std_string, "name")],
|
||||
conf,
|
||||
)
|
||||
|
||||
for conf in config.get(CONF_ON_FACE_SCAN_UNMATCHED, []):
|
||||
await automation.build_callback_automation(
|
||||
var, "add_on_face_scan_unmatched_callback", [], conf
|
||||
)
|
||||
|
||||
for conf in config.get(CONF_ON_FACE_SCAN_INVALID, []):
|
||||
await automation.build_callback_automation(
|
||||
var, "add_on_face_scan_invalid_callback", [(cg.uint8, "error")], conf
|
||||
)
|
||||
|
||||
for conf in config.get(CONF_ON_FACE_INFO, []):
|
||||
await automation.build_callback_automation(
|
||||
var,
|
||||
"add_on_face_info_callback",
|
||||
[
|
||||
(cg.int16, "status"),
|
||||
(cg.int16, "left"),
|
||||
(cg.int16, "top"),
|
||||
(cg.int16, "right"),
|
||||
(cg.int16, "bottom"),
|
||||
(cg.int16, "yaw"),
|
||||
(cg.int16, "pitch"),
|
||||
(cg.int16, "roll"),
|
||||
],
|
||||
conf,
|
||||
)
|
||||
|
||||
for conf in config.get(CONF_ON_ENROLLMENT_DONE, []):
|
||||
await automation.build_callback_automation(
|
||||
var,
|
||||
"add_on_enrollment_done_callback",
|
||||
[(cg.int16, "face_id"), (cg.uint8, "direction")],
|
||||
conf,
|
||||
)
|
||||
|
||||
for conf in config.get(CONF_ON_ENROLLMENT_FAILED, []):
|
||||
await automation.build_callback_automation(
|
||||
var, "add_on_enrollment_failed_callback", [(cg.uint8, "error")], conf
|
||||
)
|
||||
await automation.build_callback_automations(var, config, _CALLBACK_AUTOMATIONS)
|
||||
|
||||
|
||||
@automation.register_action(
|
||||
@@ -136,7 +131,7 @@ async def hlk_fm22x_enroll_to_code(config, action_id, template_arg, args):
|
||||
cv.maybe_simple_value(
|
||||
{
|
||||
cv.GenerateID(): cv.use_id(HlkFm22xComponent),
|
||||
cv.Required(CONF_FACE_ID): cv.templatable(cv.uint16_t),
|
||||
cv.Required(CONF_FACE_ID): cv.templatable(cv.int_range(min=0, max=32767)),
|
||||
},
|
||||
key=CONF_FACE_ID,
|
||||
),
|
||||
|
||||
@@ -12,7 +12,7 @@ CONF_SELECTS = [
|
||||
"Simple",
|
||||
]
|
||||
|
||||
LD2420Select = ld2420_ns.class_("LD2420Select", cg.Component)
|
||||
LD2420Select = ld2420_ns.class_("LD2420Select", select.Select, cg.Component)
|
||||
|
||||
CONFIG_SCHEMA = {
|
||||
cv.GenerateID(CONF_LD2420_ID): cv.use_id(LD2420Component),
|
||||
@@ -28,7 +28,7 @@ async def to_code(config):
|
||||
if operating_mode_config := config.get(CONF_OPERATING_MODE):
|
||||
sel = await select.new_select(
|
||||
operating_mode_config,
|
||||
options=[CONF_SELECTS],
|
||||
options=CONF_SELECTS,
|
||||
)
|
||||
await cg.register_parented(sel, config[CONF_LD2420_ID])
|
||||
cg.add(LD2420_component.set_operating_mode_select(sel))
|
||||
|
||||
@@ -44,11 +44,13 @@ FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema(
|
||||
)
|
||||
|
||||
|
||||
_CALLBACK_AUTOMATIONS = (
|
||||
automation.CallbackAutomation(CONF_ON_DATA, "add_on_data_callback"),
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
await uart.register_uart_device(var, config)
|
||||
for conf in config.get(CONF_ON_DATA, []):
|
||||
await automation.build_callback_automation(
|
||||
var, "add_on_data_callback", [], conf
|
||||
)
|
||||
await automation.build_callback_automations(var, config, _CALLBACK_AUTOMATIONS)
|
||||
|
||||
@@ -41,7 +41,7 @@ def _lookup_board_pins(board):
|
||||
board_pins = component.board_pins.get(board, {})
|
||||
# Resolve aliased board pins (shorthand when two boards have the same pin configuration)
|
||||
while isinstance(board_pins, str):
|
||||
board_pins = board_pins[board_pins]
|
||||
board_pins = component.board_pins[board_pins]
|
||||
return board_pins
|
||||
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ CONFIG_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(LIGHTWAVERFComponent),
|
||||
cv.Optional(CONF_READ_PIN, default=13): pins.internal_gpio_input_pin_schema,
|
||||
cv.Optional(CONF_WRITE_PIN, default=14): pins.internal_gpio_input_pin_schema,
|
||||
cv.Optional(CONF_WRITE_PIN, default=14): pins.internal_gpio_output_pin_schema,
|
||||
}
|
||||
).extend(cv.polling_component_schema("1s"))
|
||||
|
||||
|
||||
@@ -81,20 +81,23 @@ def lock_schema(
|
||||
return _LOCK_SCHEMA.extend(schema)
|
||||
|
||||
|
||||
_CALLBACK_AUTOMATIONS = (
|
||||
automation.CallbackAutomation(
|
||||
CONF_ON_LOCK,
|
||||
"add_on_state_callback",
|
||||
forwarder=LockStateForwarder.template(LockState.LOCK_STATE_LOCKED),
|
||||
),
|
||||
automation.CallbackAutomation(
|
||||
CONF_ON_UNLOCK,
|
||||
"add_on_state_callback",
|
||||
forwarder=LockStateForwarder.template(LockState.LOCK_STATE_UNLOCKED),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@setup_entity("lock")
|
||||
async def _setup_lock_core(var, config):
|
||||
for conf_key, state_enum in (
|
||||
(CONF_ON_LOCK, LockState.LOCK_STATE_LOCKED),
|
||||
(CONF_ON_UNLOCK, LockState.LOCK_STATE_UNLOCKED),
|
||||
):
|
||||
for conf in config.get(conf_key, []):
|
||||
await automation.build_callback_automation(
|
||||
var,
|
||||
"add_on_state_callback",
|
||||
[],
|
||||
conf,
|
||||
forwarder=LockStateForwarder.template(state_enum),
|
||||
)
|
||||
await automation.build_callback_automations(var, config, _CALLBACK_AUTOMATIONS)
|
||||
|
||||
if mqtt_id := config.get(CONF_MQTT_ID):
|
||||
mqtt_ = cg.new_Pvariable(mqtt_id, var)
|
||||
|
||||
@@ -211,6 +211,16 @@ CONFIG_SCHEMA = cv.All(
|
||||
)
|
||||
|
||||
|
||||
_CALLBACK_AUTOMATIONS = (
|
||||
automation.CallbackAutomation(
|
||||
CONF_ON_PS_HIGH_THRESHOLD, "add_on_ps_high_trigger_callback"
|
||||
),
|
||||
automation.CallbackAutomation(
|
||||
CONF_ON_PS_LOW_THRESHOLD, "add_on_ps_low_trigger_callback"
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
@@ -240,14 +250,7 @@ async def to_code(config):
|
||||
sens = await sensor.new_sensor(prox_cnt_config)
|
||||
cg.add(var.set_proximity_counts_sensor(sens))
|
||||
|
||||
for conf in config.get(CONF_ON_PS_HIGH_THRESHOLD, []):
|
||||
await automation.build_callback_automation(
|
||||
var, "add_on_ps_high_trigger_callback", [], conf
|
||||
)
|
||||
for conf in config.get(CONF_ON_PS_LOW_THRESHOLD, []):
|
||||
await automation.build_callback_automation(
|
||||
var, "add_on_ps_low_trigger_callback", [], conf
|
||||
)
|
||||
await automation.build_callback_automations(var, config, _CALLBACK_AUTOMATIONS)
|
||||
|
||||
cg.add(var.set_ltr_type(config[CONF_TYPE]))
|
||||
|
||||
|
||||
@@ -201,6 +201,16 @@ CONFIG_SCHEMA = cv.All(
|
||||
)
|
||||
|
||||
|
||||
_CALLBACK_AUTOMATIONS = (
|
||||
automation.CallbackAutomation(
|
||||
CONF_ON_PS_HIGH_THRESHOLD, "add_on_ps_high_trigger_callback"
|
||||
),
|
||||
automation.CallbackAutomation(
|
||||
CONF_ON_PS_LOW_THRESHOLD, "add_on_ps_low_trigger_callback"
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
@@ -230,14 +240,7 @@ async def to_code(config):
|
||||
sens = await sensor.new_sensor(prox_cnt_config)
|
||||
cg.add(var.set_proximity_counts_sensor(sens))
|
||||
|
||||
for conf in config.get(CONF_ON_PS_HIGH_THRESHOLD, []):
|
||||
await automation.build_callback_automation(
|
||||
var, "add_on_ps_high_trigger_callback", [], conf
|
||||
)
|
||||
for conf in config.get(CONF_ON_PS_LOW_THRESHOLD, []):
|
||||
await automation.build_callback_automation(
|
||||
var, "add_on_ps_low_trigger_callback", [], conf
|
||||
)
|
||||
await automation.build_callback_automations(var, config, _CALLBACK_AUTOMATIONS)
|
||||
|
||||
cg.add(var.set_ltr_type(config[CONF_TYPE]))
|
||||
|
||||
|
||||
@@ -35,7 +35,7 @@ CONFIG_SCHEMA = (
|
||||
.extend(
|
||||
{
|
||||
cv.GenerateID(CONF_MCP3008_ID): cv.use_id(MCP3008),
|
||||
cv.Required(CONF_NUMBER): cv.int_,
|
||||
cv.Required(CONF_NUMBER): cv.int_range(min=0, max=7),
|
||||
cv.Optional(CONF_REFERENCE_VOLTAGE, default="3.3V"): cv.voltage,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -48,11 +48,11 @@ async def to_code(config):
|
||||
config[CONF_CHANNEL],
|
||||
)
|
||||
if not config[CONF_TERMINAL_A]:
|
||||
cg.add(parent.initialize_terminal_disabled(config[CONF_CHANNEL], "a"))
|
||||
cg.add(parent.initialize_terminal_disabled(config[CONF_CHANNEL], ord("a")))
|
||||
if not config[CONF_TERMINAL_B]:
|
||||
cg.add(parent.initialize_terminal_disabled(config[CONF_CHANNEL], "b"))
|
||||
cg.add(parent.initialize_terminal_disabled(config[CONF_CHANNEL], ord("b")))
|
||||
if not config[CONF_TERMINAL_W]:
|
||||
cg.add(parent.initialize_terminal_disabled(config[CONF_CHANNEL], "w"))
|
||||
cg.add(parent.initialize_terminal_disabled(config[CONF_CHANNEL], ord("w")))
|
||||
if CONF_INITIAL_VALUE in config:
|
||||
cg.add(
|
||||
parent.set_initial_value(config[CONF_CHANNEL], config[CONF_INITIAL_VALUE])
|
||||
|
||||
@@ -69,7 +69,7 @@ StateEnterForwarder = media_player_ns.class_("StateEnterForwarder")
|
||||
MediaPlayerState = media_player_ns.enum("MediaPlayerState")
|
||||
|
||||
# State triggers: (config_key, state enum or None for any-state)
|
||||
_STATE_TRIGGERS = [
|
||||
_STATE_TRIGGERS = (
|
||||
(CONF_ON_STATE, None),
|
||||
(CONF_ON_IDLE, MediaPlayerState.MEDIA_PLAYER_STATE_IDLE),
|
||||
(CONF_ON_PLAY, MediaPlayerState.MEDIA_PLAYER_STATE_PLAYING),
|
||||
@@ -77,7 +77,7 @@ _STATE_TRIGGERS = [
|
||||
(CONF_ON_ANNOUNCEMENT, MediaPlayerState.MEDIA_PLAYER_STATE_ANNOUNCING),
|
||||
(CONF_ON_TURN_ON, MediaPlayerState.MEDIA_PLAYER_STATE_ON),
|
||||
(CONF_ON_TURN_OFF, MediaPlayerState.MEDIA_PLAYER_STATE_OFF),
|
||||
]
|
||||
)
|
||||
|
||||
# State conditions that all share the same schema and codegen handler
|
||||
_STATE_CONDITIONS = [
|
||||
@@ -102,17 +102,54 @@ VolumeSetAction = media_player_ns.class_(
|
||||
)
|
||||
|
||||
|
||||
_CALLBACK_AUTOMATIONS = (
|
||||
automation.CallbackAutomation(
|
||||
CONF_ON_STATE, "add_on_state_callback", forwarder=StateAnyForwarder
|
||||
),
|
||||
automation.CallbackAutomation(
|
||||
CONF_ON_IDLE,
|
||||
"add_on_state_callback",
|
||||
forwarder=StateEnterForwarder.template(
|
||||
MediaPlayerState.MEDIA_PLAYER_STATE_IDLE
|
||||
),
|
||||
),
|
||||
automation.CallbackAutomation(
|
||||
CONF_ON_PLAY,
|
||||
"add_on_state_callback",
|
||||
forwarder=StateEnterForwarder.template(
|
||||
MediaPlayerState.MEDIA_PLAYER_STATE_PLAYING
|
||||
),
|
||||
),
|
||||
automation.CallbackAutomation(
|
||||
CONF_ON_PAUSE,
|
||||
"add_on_state_callback",
|
||||
forwarder=StateEnterForwarder.template(
|
||||
MediaPlayerState.MEDIA_PLAYER_STATE_PAUSED
|
||||
),
|
||||
),
|
||||
automation.CallbackAutomation(
|
||||
CONF_ON_ANNOUNCEMENT,
|
||||
"add_on_state_callback",
|
||||
forwarder=StateEnterForwarder.template(
|
||||
MediaPlayerState.MEDIA_PLAYER_STATE_ANNOUNCING
|
||||
),
|
||||
),
|
||||
automation.CallbackAutomation(
|
||||
CONF_ON_TURN_ON,
|
||||
"add_on_state_callback",
|
||||
forwarder=StateEnterForwarder.template(MediaPlayerState.MEDIA_PLAYER_STATE_ON),
|
||||
),
|
||||
automation.CallbackAutomation(
|
||||
CONF_ON_TURN_OFF,
|
||||
"add_on_state_callback",
|
||||
forwarder=StateEnterForwarder.template(MediaPlayerState.MEDIA_PLAYER_STATE_OFF),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@setup_entity("media_player")
|
||||
async def setup_media_player_core_(var, config):
|
||||
for conf_key, state_enum in _STATE_TRIGGERS:
|
||||
for conf in config.get(conf_key, []):
|
||||
if state_enum is None:
|
||||
forwarder = StateAnyForwarder
|
||||
else:
|
||||
forwarder = StateEnterForwarder.template(state_enum)
|
||||
await automation.build_callback_automation(
|
||||
var, "add_on_state_callback", [], conf, forwarder=forwarder
|
||||
)
|
||||
await automation.build_callback_automations(var, config, _CALLBACK_AUTOMATIONS)
|
||||
|
||||
|
||||
async def register_media_player(var, config):
|
||||
|
||||
@@ -28,7 +28,7 @@ CONFIG_SCHEMA = cv.Schema(
|
||||
is_polling_component=False,
|
||||
)
|
||||
)
|
||||
.extend({cv.Required(CONF_MEMORY_DATA): cv.hex_int_range()}),
|
||||
.extend({cv.Required(CONF_MEMORY_DATA): cv.hex_int_range(min=0x00, max=0xFF)}),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -37,8 +37,12 @@ CONFIG_SCHEMA = cv.Schema(
|
||||
)
|
||||
.extend(
|
||||
{
|
||||
cv.Optional(CONF_MEMORY_DATA_OFF, default=0x06): cv.hex_int_range(),
|
||||
cv.Optional(CONF_MEMORY_DATA_ON, default=0x01): cv.hex_int_range(),
|
||||
cv.Optional(CONF_MEMORY_DATA_OFF, default=0x06): cv.hex_int_range(
|
||||
min=0x00, max=0xFF
|
||||
),
|
||||
cv.Optional(CONF_MEMORY_DATA_ON, default=0x01): cv.hex_int_range(
|
||||
min=0x00, max=0xFF
|
||||
),
|
||||
}
|
||||
),
|
||||
}
|
||||
|
||||
@@ -205,6 +205,25 @@ async def add_modbus_base_properties(
|
||||
cg.add(var.set_template(template_))
|
||||
|
||||
|
||||
_CALLBACK_AUTOMATIONS = (
|
||||
automation.CallbackAutomation(
|
||||
CONF_ON_COMMAND_SENT,
|
||||
"add_on_command_sent_callback",
|
||||
[(cg.int_, "function_code"), (cg.int_, "address")],
|
||||
),
|
||||
automation.CallbackAutomation(
|
||||
CONF_ON_ONLINE,
|
||||
"add_on_online_callback",
|
||||
[(cg.int_, "function_code"), (cg.int_, "address")],
|
||||
),
|
||||
automation.CallbackAutomation(
|
||||
CONF_ON_OFFLINE,
|
||||
"add_on_offline_callback",
|
||||
[(cg.int_, "function_code"), (cg.int_, "address")],
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
cg.add(var.set_allow_duplicate_commands(config[CONF_ALLOW_DUPLICATE_COMMANDS]))
|
||||
@@ -257,27 +276,7 @@ async def to_code(config):
|
||||
)
|
||||
cg.add(var.add_server_register(server_register_var))
|
||||
await register_modbus_device(var, config)
|
||||
for conf in config.get(CONF_ON_COMMAND_SENT, []):
|
||||
await automation.build_callback_automation(
|
||||
var,
|
||||
"add_on_command_sent_callback",
|
||||
[(cg.int_, "function_code"), (cg.int_, "address")],
|
||||
conf,
|
||||
)
|
||||
for conf in config.get(CONF_ON_ONLINE, []):
|
||||
await automation.build_callback_automation(
|
||||
var,
|
||||
"add_on_online_callback",
|
||||
[(cg.int_, "function_code"), (cg.int_, "address")],
|
||||
conf,
|
||||
)
|
||||
for conf in config.get(CONF_ON_OFFLINE, []):
|
||||
await automation.build_callback_automation(
|
||||
var,
|
||||
"add_on_offline_callback",
|
||||
[(cg.int_, "function_code"), (cg.int_, "address")],
|
||||
conf,
|
||||
)
|
||||
await automation.build_callback_automations(var, config, _CALLBACK_AUTOMATIONS)
|
||||
|
||||
|
||||
async def register_modbus_device(var, config):
|
||||
|
||||
@@ -81,9 +81,9 @@ void MY9231OutputComponent::loop() {
|
||||
}
|
||||
this->update_ = false;
|
||||
}
|
||||
void MY9231OutputComponent::set_channel_value_(uint8_t channel, uint16_t value) {
|
||||
void MY9231OutputComponent::set_channel_value_(uint16_t channel, uint16_t value) {
|
||||
ESP_LOGV(TAG, "set channels %u to %u", channel, value);
|
||||
uint8_t index = this->num_channels_ - channel - 1;
|
||||
uint16_t index = this->num_channels_ - channel - 1;
|
||||
if (this->pwm_amounts_[index] != value) {
|
||||
this->update_ = true;
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ class MY9231OutputComponent : public Component {
|
||||
class Channel : public output::FloatOutput {
|
||||
public:
|
||||
void set_parent(MY9231OutputComponent *parent) { parent_ = parent; }
|
||||
void set_channel(uint8_t channel) { channel_ = channel; }
|
||||
void set_channel(uint16_t channel) { channel_ = channel; }
|
||||
|
||||
protected:
|
||||
void write_state(float state) override {
|
||||
@@ -39,13 +39,13 @@ class MY9231OutputComponent : public Component {
|
||||
}
|
||||
|
||||
MY9231OutputComponent *parent_;
|
||||
uint8_t channel_;
|
||||
uint16_t channel_;
|
||||
};
|
||||
|
||||
protected:
|
||||
uint16_t get_max_amount_() const { return (uint32_t(1) << this->bit_depth_) - 1; }
|
||||
|
||||
void set_channel_value_(uint8_t channel, uint16_t value);
|
||||
void set_channel_value_(uint16_t channel, uint16_t value);
|
||||
void init_chips_(uint8_t command);
|
||||
void write_word_(uint16_t value, uint8_t bits);
|
||||
void send_di_pulses_(uint8_t count);
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/automation.h"
|
||||
#include "esphome/core/string_ref.h"
|
||||
|
||||
#include "nextion.h"
|
||||
|
||||
namespace esphome::nextion {
|
||||
|
||||
@@ -19,6 +19,10 @@ CONF_MAX_COMMANDS_PER_LOOP = "max_commands_per_loop"
|
||||
CONF_MAX_QUEUE_AGE = "max_queue_age"
|
||||
CONF_MAX_QUEUE_SIZE = "max_queue_size"
|
||||
CONF_ON_BUFFER_OVERFLOW = "on_buffer_overflow"
|
||||
CONF_ON_CUSTOM_BINARY_SENSOR = "on_custom_binary_sensor"
|
||||
CONF_ON_CUSTOM_SENSOR = "on_custom_sensor"
|
||||
CONF_ON_CUSTOM_SWITCH = "on_custom_switch"
|
||||
CONF_ON_CUSTOM_TEXT_SENSOR = "on_custom_text_sensor"
|
||||
CONF_ON_PAGE = "on_page"
|
||||
CONF_ON_SETUP = "on_setup"
|
||||
CONF_ON_SLEEP = "on_sleep"
|
||||
|
||||
@@ -20,6 +20,10 @@ from .base_component import (
|
||||
CONF_MAX_QUEUE_AGE,
|
||||
CONF_MAX_QUEUE_SIZE,
|
||||
CONF_ON_BUFFER_OVERFLOW,
|
||||
CONF_ON_CUSTOM_BINARY_SENSOR,
|
||||
CONF_ON_CUSTOM_SENSOR,
|
||||
CONF_ON_CUSTOM_SWITCH,
|
||||
CONF_ON_CUSTOM_TEXT_SENSOR,
|
||||
CONF_ON_PAGE,
|
||||
CONF_ON_SETUP,
|
||||
CONF_ON_SLEEP,
|
||||
@@ -88,6 +92,12 @@ CONFIG_SCHEMA = cv.All(
|
||||
cv.Optional(CONF_MAX_COMMANDS_PER_LOOP): cv.uint16_t,
|
||||
cv.Optional(CONF_MAX_QUEUE_SIZE): cv.positive_int,
|
||||
cv.Optional(CONF_ON_BUFFER_OVERFLOW): automation.validate_automation({}),
|
||||
cv.Optional(CONF_ON_CUSTOM_BINARY_SENSOR): automation.validate_automation(
|
||||
{}
|
||||
),
|
||||
cv.Optional(CONF_ON_CUSTOM_SENSOR): automation.validate_automation({}),
|
||||
cv.Optional(CONF_ON_CUSTOM_SWITCH): automation.validate_automation({}),
|
||||
cv.Optional(CONF_ON_CUSTOM_TEXT_SENSOR): automation.validate_automation({}),
|
||||
cv.Optional(CONF_ON_PAGE): automation.validate_automation({}),
|
||||
cv.Optional(CONF_ON_SETUP): automation.validate_automation({}),
|
||||
cv.Optional(CONF_ON_SLEEP): automation.validate_automation({}),
|
||||
@@ -144,6 +154,56 @@ async def nextion_set_brightness_to_code(config, action_id, template_arg, args):
|
||||
return var
|
||||
|
||||
|
||||
_CALLBACK_AUTOMATIONS = (
|
||||
automation.CallbackAutomation(CONF_ON_SETUP, "add_setup_state_callback"),
|
||||
automation.CallbackAutomation(CONF_ON_SLEEP, "add_sleep_state_callback"),
|
||||
automation.CallbackAutomation(CONF_ON_WAKE, "add_wake_state_callback"),
|
||||
automation.CallbackAutomation(
|
||||
CONF_ON_PAGE, "add_new_page_callback", [(cg.uint8, "x")]
|
||||
),
|
||||
automation.CallbackAutomation(
|
||||
CONF_ON_TOUCH,
|
||||
"add_touch_event_callback",
|
||||
[
|
||||
(cg.uint8, "page_id"),
|
||||
(cg.uint8, "component_id"),
|
||||
(cg.bool_, "touch_event"),
|
||||
],
|
||||
),
|
||||
automation.CallbackAutomation(
|
||||
CONF_ON_BUFFER_OVERFLOW, "add_buffer_overflow_event_callback"
|
||||
),
|
||||
automation.CallbackAutomation(
|
||||
CONF_ON_CUSTOM_BINARY_SENSOR,
|
||||
"add_custom_binary_sensor_callback",
|
||||
[(cg.StringRef, "key"), (cg.bool_, "value")],
|
||||
),
|
||||
automation.CallbackAutomation(
|
||||
CONF_ON_CUSTOM_SENSOR,
|
||||
"add_custom_sensor_callback",
|
||||
[(cg.StringRef, "key"), (cg.int32, "value")],
|
||||
),
|
||||
automation.CallbackAutomation(
|
||||
CONF_ON_CUSTOM_SWITCH,
|
||||
"add_custom_switch_callback",
|
||||
[(cg.StringRef, "key"), (cg.bool_, "value")],
|
||||
),
|
||||
automation.CallbackAutomation(
|
||||
CONF_ON_CUSTOM_TEXT_SENSOR,
|
||||
"add_custom_text_sensor_callback",
|
||||
[(cg.StringRef, "key"), (cg.StringRef, "value")],
|
||||
),
|
||||
)
|
||||
|
||||
# Map custom trigger config keys to their conditional defines
|
||||
_CUSTOM_TRIGGER_DEFINES = {
|
||||
CONF_ON_CUSTOM_BINARY_SENSOR: "USE_NEXTION_TRIGGER_CUSTOM_BINARY_SENSOR",
|
||||
CONF_ON_CUSTOM_SENSOR: "USE_NEXTION_TRIGGER_CUSTOM_SENSOR",
|
||||
CONF_ON_CUSTOM_SWITCH: "USE_NEXTION_TRIGGER_CUSTOM_SWITCH",
|
||||
CONF_ON_CUSTOM_TEXT_SENSOR: "USE_NEXTION_TRIGGER_CUSTOM_TEXT_SENSOR",
|
||||
}
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await uart.register_uart_device(var, config)
|
||||
@@ -231,35 +291,8 @@ async def to_code(config):
|
||||
cg.add(var.set_max_commands_per_loop(max_commands_per_loop))
|
||||
|
||||
await display.register_display(var, config)
|
||||
for conf_key, define_name in _CUSTOM_TRIGGER_DEFINES.items():
|
||||
if config.get(conf_key):
|
||||
cg.add_define(define_name)
|
||||
|
||||
for conf in config.get(CONF_ON_SETUP, []):
|
||||
await automation.build_callback_automation(
|
||||
var, "add_setup_state_callback", [], conf
|
||||
)
|
||||
for conf in config.get(CONF_ON_SLEEP, []):
|
||||
await automation.build_callback_automation(
|
||||
var, "add_sleep_state_callback", [], conf
|
||||
)
|
||||
for conf in config.get(CONF_ON_WAKE, []):
|
||||
await automation.build_callback_automation(
|
||||
var, "add_wake_state_callback", [], conf
|
||||
)
|
||||
for conf in config.get(CONF_ON_PAGE, []):
|
||||
await automation.build_callback_automation(
|
||||
var, "add_new_page_callback", [(cg.uint8, "x")], conf
|
||||
)
|
||||
for conf in config.get(CONF_ON_TOUCH, []):
|
||||
await automation.build_callback_automation(
|
||||
var,
|
||||
"add_touch_event_callback",
|
||||
[
|
||||
(cg.uint8, "page_id"),
|
||||
(cg.uint8, "component_id"),
|
||||
(cg.bool_, "touch_event"),
|
||||
],
|
||||
conf,
|
||||
)
|
||||
for conf in config.get(CONF_ON_BUFFER_OVERFLOW, []):
|
||||
await automation.build_callback_automation(
|
||||
var, "add_buffer_overflow_event_callback", [], conf
|
||||
)
|
||||
await automation.build_callback_automations(var, config, _CALLBACK_AUTOMATIONS)
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
#include "nextion.h"
|
||||
|
||||
#include <cinttypes>
|
||||
|
||||
#include "esphome/core/application.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/string_ref.h"
|
||||
#include "esphome/core/util.h"
|
||||
|
||||
namespace esphome::nextion {
|
||||
@@ -706,7 +709,7 @@ void Nextion::process_nextion_commands_() {
|
||||
auto index = to_process.find('\0');
|
||||
if (index == std::string::npos || (to_process_length - index - 1) < 1) {
|
||||
ESP_LOGE(TAG, "Bad switch data (0x90)");
|
||||
ESP_LOGN(TAG, "proc: %s %zu %d", to_process.c_str(), to_process_length, index);
|
||||
ESP_LOGN(TAG, "proc: %s %zu %zu", to_process.c_str(), to_process_length, index);
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -715,6 +718,10 @@ void Nextion::process_nextion_commands_() {
|
||||
|
||||
ESP_LOGN(TAG, "Switch %s: %s", ONOFF(to_process[index] != 0), variable_name.c_str());
|
||||
|
||||
#ifdef USE_NEXTION_TRIGGER_CUSTOM_SWITCH
|
||||
this->custom_switch_callback_.call(StringRef(variable_name), to_process[index] != 0);
|
||||
#endif // USE_NEXTION_TRIGGER_CUSTOM_SWITCH
|
||||
|
||||
for (auto *switchtype : this->switchtype_) {
|
||||
switchtype->process_bool(variable_name, to_process[index] != 0);
|
||||
}
|
||||
@@ -732,7 +739,7 @@ void Nextion::process_nextion_commands_() {
|
||||
auto index = to_process.find('\0');
|
||||
if (index == std::string::npos || (to_process_length - index - 1) != 4) {
|
||||
ESP_LOGE(TAG, "Bad sensor data (0x91)");
|
||||
ESP_LOGN(TAG, "proc: %s %zu %d", to_process.c_str(), to_process_length, index);
|
||||
ESP_LOGN(TAG, "proc: %s %zu %zu", to_process.c_str(), to_process_length, index);
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -744,6 +751,10 @@ void Nextion::process_nextion_commands_() {
|
||||
|
||||
ESP_LOGN(TAG, "Sensor: %s=%d", variable_name.c_str(), value);
|
||||
|
||||
#ifdef USE_NEXTION_TRIGGER_CUSTOM_SENSOR
|
||||
this->custom_sensor_callback_.call(StringRef(variable_name), value);
|
||||
#endif // USE_NEXTION_TRIGGER_CUSTOM_SENSOR
|
||||
|
||||
for (auto *sensor : this->sensortype_) {
|
||||
sensor->process_sensor(variable_name, value);
|
||||
}
|
||||
@@ -765,7 +776,7 @@ void Nextion::process_nextion_commands_() {
|
||||
auto index = to_process.find('\0');
|
||||
if (index == std::string::npos || (to_process_length - index - 1) < 1) {
|
||||
ESP_LOGE(TAG, "Bad text data (0x92)");
|
||||
ESP_LOGN(TAG, "proc: %s %zu %d", to_process.c_str(), to_process_length, index);
|
||||
ESP_LOGN(TAG, "proc: %s %zu %zu", to_process.c_str(), to_process_length, index);
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -781,6 +792,11 @@ void Nextion::process_nextion_commands_() {
|
||||
// nq->variable_name = variable_name;
|
||||
// nq->state = text_value;
|
||||
// this->textsensorq_.push_back(nq);
|
||||
|
||||
#ifdef USE_NEXTION_TRIGGER_CUSTOM_TEXT_SENSOR
|
||||
this->custom_text_sensor_callback_.call(StringRef(variable_name), StringRef(text_value));
|
||||
#endif // USE_NEXTION_TRIGGER_CUSTOM_TEXT_SENSOR
|
||||
|
||||
for (auto *textsensortype : this->textsensortype_) {
|
||||
textsensortype->process_text(variable_name, text_value);
|
||||
}
|
||||
@@ -798,8 +814,8 @@ void Nextion::process_nextion_commands_() {
|
||||
// Get variable name
|
||||
auto index = to_process.find('\0');
|
||||
if (index == std::string::npos || (to_process_length - index - 1) < 1) {
|
||||
ESP_LOGE(TAG, "Bad binary data (0x92)");
|
||||
ESP_LOGN(TAG, "proc: %s %zu %d", to_process.c_str(), to_process_length, index);
|
||||
ESP_LOGE(TAG, "Bad binary data (0x93)");
|
||||
ESP_LOGN(TAG, "proc: %s %zu %zu", to_process.c_str(), to_process_length, index);
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -808,6 +824,10 @@ void Nextion::process_nextion_commands_() {
|
||||
|
||||
ESP_LOGN(TAG, "Binary sensor: %s=%s", variable_name.c_str(), ONOFF(to_process[index] != 0));
|
||||
|
||||
#ifdef USE_NEXTION_TRIGGER_CUSTOM_BINARY_SENSOR
|
||||
this->custom_binary_sensor_callback_.call(StringRef(variable_name), to_process[index] != 0);
|
||||
#endif // USE_NEXTION_TRIGGER_CUSTOM_BINARY_SENSOR
|
||||
|
||||
for (auto *binarysensortype : this->binarysensortype_) {
|
||||
binarysensortype->process_bool(&variable_name[0], to_process[index] != 0);
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
#include "esphome/components/display/display_color_utils.h"
|
||||
#include "esphome/components/uart/uart.h"
|
||||
#include "esphome/core/defines.h"
|
||||
#include "esphome/core/string_ref.h"
|
||||
#include "esphome/core/time.h"
|
||||
|
||||
#ifdef USE_NEXTION_WAVEFORM
|
||||
@@ -1183,6 +1184,59 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe
|
||||
this->buffer_overflow_callback_.add(std::forward<F>(callback));
|
||||
}
|
||||
|
||||
// Callbacks for Nextion "custom protocol" frames (0x90..0x93)
|
||||
#ifdef USE_NEXTION_TRIGGER_CUSTOM_BINARY_SENSOR
|
||||
/** Add a callback to be notified when Nextion sends a custom binary sensor protocol frame (0x93).
|
||||
*
|
||||
* This callback is invoked when a Nextion custom binary sensor frame is received,
|
||||
* providing the component name as the key and the decoded boolean value.
|
||||
*
|
||||
* @param callback The void(const StringRef &key, bool value) callback.
|
||||
*/
|
||||
template<typename F> void add_custom_binary_sensor_callback(F &&callback) {
|
||||
this->custom_binary_sensor_callback_.add(std::forward<F>(callback));
|
||||
}
|
||||
#endif // USE_NEXTION_TRIGGER_CUSTOM_BINARY_SENSOR
|
||||
|
||||
#ifdef USE_NEXTION_TRIGGER_CUSTOM_SENSOR
|
||||
/** Add a callback to be notified when Nextion sends a custom sensor protocol frame (0x91).
|
||||
*
|
||||
* This callback is invoked when a Nextion custom sensor frame is received,
|
||||
* providing the component name as the key and the decoded integer value.
|
||||
*
|
||||
* @param callback The void(StringRef key, int32_t value) callback.
|
||||
*/
|
||||
template<typename F> void add_custom_sensor_callback(F &&callback) {
|
||||
this->custom_sensor_callback_.add(std::forward<F>(callback));
|
||||
}
|
||||
#endif // USE_NEXTION_TRIGGER_CUSTOM_SENSOR
|
||||
|
||||
#ifdef USE_NEXTION_TRIGGER_CUSTOM_SWITCH
|
||||
/** Add a callback to be notified when Nextion sends a custom switch protocol frame (0x90).
|
||||
*
|
||||
* This callback is invoked when a Nextion custom switch frame is received,
|
||||
* providing the component name as the key and the decoded boolean value.
|
||||
*
|
||||
* @param callback The void(const StringRef &key, bool value) callback.
|
||||
*/
|
||||
template<typename F> void add_custom_switch_callback(F &&callback) {
|
||||
this->custom_switch_callback_.add(std::forward<F>(callback));
|
||||
}
|
||||
#endif // USE_NEXTION_TRIGGER_CUSTOM_SWITCH
|
||||
|
||||
#ifdef USE_NEXTION_TRIGGER_CUSTOM_TEXT_SENSOR
|
||||
/** Add a callback to be notified when Nextion sends a custom text sensor protocol frame (0x92).
|
||||
*
|
||||
* This callback is invoked when a Nextion custom text sensor frame is received,
|
||||
* providing the component name as the key and the decoded text value.
|
||||
*
|
||||
* @param callback The void(const StringRef &key, const StringRef &value) callback.
|
||||
*/
|
||||
template<typename F> void add_custom_text_sensor_callback(F &&callback) {
|
||||
this->custom_text_sensor_callback_.add(std::forward<F>(callback));
|
||||
}
|
||||
#endif // USE_NEXTION_TRIGGER_CUSTOM_TEXT_SENSOR
|
||||
|
||||
void update_all_components();
|
||||
|
||||
/**
|
||||
@@ -1535,6 +1589,18 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe
|
||||
CallbackManager<void(uint8_t)> page_callback_{};
|
||||
CallbackManager<void(uint8_t, uint8_t, bool)> touch_callback_{};
|
||||
CallbackManager<void()> buffer_overflow_callback_{};
|
||||
#ifdef USE_NEXTION_TRIGGER_CUSTOM_BINARY_SENSOR
|
||||
CallbackManager<void(StringRef, bool)> custom_binary_sensor_callback_{};
|
||||
#endif // USE_NEXTION_TRIGGER_CUSTOM_BINARY_SENSOR
|
||||
#ifdef USE_NEXTION_TRIGGER_CUSTOM_SENSOR
|
||||
CallbackManager<void(StringRef, int32_t)> custom_sensor_callback_{};
|
||||
#endif // USE_NEXTION_TRIGGER_CUSTOM_SENSOR
|
||||
#ifdef USE_NEXTION_TRIGGER_CUSTOM_SWITCH
|
||||
CallbackManager<void(StringRef, bool)> custom_switch_callback_{};
|
||||
#endif // USE_NEXTION_TRIGGER_CUSTOM_SWITCH
|
||||
#ifdef USE_NEXTION_TRIGGER_CUSTOM_TEXT_SENSOR
|
||||
CallbackManager<void(StringRef, StringRef)> custom_text_sensor_callback_{};
|
||||
#endif // USE_NEXTION_TRIGGER_CUSTOM_TEXT_SENSOR
|
||||
|
||||
nextion_writer_t writer_;
|
||||
optional<float> brightness_;
|
||||
|
||||
@@ -243,12 +243,16 @@ def number_schema(
|
||||
return _NUMBER_SCHEMA.extend(schema)
|
||||
|
||||
|
||||
_CALLBACK_AUTOMATIONS = (
|
||||
automation.CallbackAutomation(
|
||||
CONF_ON_VALUE, "add_on_state_callback", [(float, "x")]
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@coroutine_with_priority(CoroPriority.AUTOMATION)
|
||||
async def _build_number_automations(var, config):
|
||||
for conf in config.get(CONF_ON_VALUE, []):
|
||||
await automation.build_callback_automation(
|
||||
var, "add_on_state_callback", [(float, "x")], conf
|
||||
)
|
||||
await automation.build_callback_automations(var, config, _CALLBACK_AUTOMATIONS)
|
||||
for conf in config.get(CONF_ON_VALUE_RANGE, []):
|
||||
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
|
||||
await cg.register_component(trigger, conf)
|
||||
|
||||
@@ -105,6 +105,14 @@ async def online_image_action_to_code(config, action_id, template_arg, args):
|
||||
return var
|
||||
|
||||
|
||||
_CALLBACK_AUTOMATIONS = (
|
||||
automation.CallbackAutomation(
|
||||
CONF_ON_DOWNLOAD_FINISHED, "add_on_finished_callback", [(bool, "cached")]
|
||||
),
|
||||
automation.CallbackAutomation(CONF_ON_ERROR, "add_on_error_callback"),
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
# Use the enhanced helper function to get all runtime image parameters
|
||||
settings = await runtime_image.process_runtime_image_config(config)
|
||||
@@ -139,12 +147,4 @@ async def to_code(config):
|
||||
else:
|
||||
cg.add(var.add_request_header(key, value))
|
||||
|
||||
for conf in config.get(CONF_ON_DOWNLOAD_FINISHED, []):
|
||||
await automation.build_callback_automation(
|
||||
var, "add_on_finished_callback", [(bool, "cached")], conf
|
||||
)
|
||||
|
||||
for conf in config.get(CONF_ON_ERROR, []):
|
||||
await automation.build_callback_automation(
|
||||
var, "add_on_error_callback", [], conf
|
||||
)
|
||||
await automation.build_callback_automations(var, config, _CALLBACK_AUTOMATIONS)
|
||||
|
||||
@@ -51,7 +51,7 @@ PCA6416A_PIN_SCHEMA = cv.All(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(PCA6416AGPIOPin),
|
||||
cv.Required(CONF_PCA6416A): cv.use_id(PCA6416AComponent),
|
||||
cv.Required(CONF_NUMBER): cv.int_range(min=0, max=16),
|
||||
cv.Required(CONF_NUMBER): cv.int_range(min=0, max=15),
|
||||
cv.Optional(CONF_MODE, default={}): cv.All(
|
||||
{
|
||||
cv.Optional(CONF_INPUT, default=False): cv.boolean,
|
||||
|
||||
@@ -21,7 +21,7 @@ CONFIG_SCHEMA = time.TIME_SCHEMA.extend(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(pcf8563Component),
|
||||
}
|
||||
).extend(i2c.i2c_device_schema(0xA3))
|
||||
).extend(i2c.i2c_device_schema(0x51))
|
||||
|
||||
|
||||
@automation.register_action(
|
||||
|
||||
@@ -55,7 +55,7 @@ def validate_mode(value):
|
||||
|
||||
PCF8574_PIN_SCHEMA = pins.gpio_base_schema(
|
||||
PCF8574GPIOPin,
|
||||
cv.int_range(min=0, max=17),
|
||||
cv.int_range(min=0, max=15),
|
||||
modes=[CONF_INPUT, CONF_OUTPUT],
|
||||
mode_validator=validate_mode,
|
||||
invertible=True,
|
||||
|
||||
@@ -49,6 +49,13 @@ def CONFIG_SCHEMA(conf):
|
||||
)
|
||||
|
||||
|
||||
_CALLBACK_AUTOMATIONS = (
|
||||
automation.CallbackAutomation(
|
||||
CONF_ON_FINISHED_WRITE, "add_on_finished_write_callback"
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
async def setup_pn532(var, config):
|
||||
await cg.register_component(var, config)
|
||||
|
||||
@@ -66,10 +73,7 @@ async def setup_pn532(var, config):
|
||||
trigger, [(cg.std_string, "x"), (nfc.NfcTag, "tag")], conf
|
||||
)
|
||||
|
||||
for conf in config.get(CONF_ON_FINISHED_WRITE, []):
|
||||
await automation.build_callback_automation(
|
||||
var, "add_on_finished_write_callback", [], conf
|
||||
)
|
||||
await automation.build_callback_automations(var, config, _CALLBACK_AUTOMATIONS)
|
||||
|
||||
|
||||
@automation.register_condition(
|
||||
|
||||
@@ -164,6 +164,16 @@ async def pn7150_simple_action_to_code(config, action_id, template_arg, args):
|
||||
return var
|
||||
|
||||
|
||||
_CALLBACK_AUTOMATIONS = (
|
||||
automation.CallbackAutomation(
|
||||
CONF_ON_EMULATED_TAG_SCAN, "add_on_emulated_tag_scan_callback"
|
||||
),
|
||||
automation.CallbackAutomation(
|
||||
CONF_ON_FINISHED_WRITE, "add_on_finished_write_callback"
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
async def setup_pn7150(var, config):
|
||||
await cg.register_component(var, config)
|
||||
|
||||
@@ -194,15 +204,7 @@ async def setup_pn7150(var, config):
|
||||
trigger, [(cg.std_string, "x"), (nfc.NfcTag, "tag")], conf
|
||||
)
|
||||
|
||||
for conf in config.get(CONF_ON_EMULATED_TAG_SCAN, []):
|
||||
await automation.build_callback_automation(
|
||||
var, "add_on_emulated_tag_scan_callback", [], conf
|
||||
)
|
||||
|
||||
for conf in config.get(CONF_ON_FINISHED_WRITE, []):
|
||||
await automation.build_callback_automation(
|
||||
var, "add_on_finished_write_callback", [], conf
|
||||
)
|
||||
await automation.build_callback_automations(var, config, _CALLBACK_AUTOMATIONS)
|
||||
|
||||
|
||||
@automation.register_condition(
|
||||
|
||||
@@ -168,6 +168,16 @@ async def pn7160_simple_action_to_code(config, action_id, template_arg, args):
|
||||
return var
|
||||
|
||||
|
||||
_CALLBACK_AUTOMATIONS = (
|
||||
automation.CallbackAutomation(
|
||||
CONF_ON_EMULATED_TAG_SCAN, "add_on_emulated_tag_scan_callback"
|
||||
),
|
||||
automation.CallbackAutomation(
|
||||
CONF_ON_FINISHED_WRITE, "add_on_finished_write_callback"
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
async def setup_pn7160(var, config):
|
||||
await cg.register_component(var, config)
|
||||
|
||||
@@ -206,15 +216,7 @@ async def setup_pn7160(var, config):
|
||||
trigger, [(cg.std_string, "x"), (nfc.NfcTag, "tag")], conf
|
||||
)
|
||||
|
||||
for conf in config.get(CONF_ON_EMULATED_TAG_SCAN, []):
|
||||
await automation.build_callback_automation(
|
||||
var, "add_on_emulated_tag_scan_callback", [], conf
|
||||
)
|
||||
|
||||
for conf in config.get(CONF_ON_FINISHED_WRITE, []):
|
||||
await automation.build_callback_automation(
|
||||
var, "add_on_finished_write_callback", [], conf
|
||||
)
|
||||
await automation.build_callback_automations(var, config, _CALLBACK_AUTOMATIONS)
|
||||
|
||||
|
||||
@automation.register_condition(
|
||||
|
||||
@@ -67,22 +67,26 @@ CONFIG_SCHEMA = cv.All(
|
||||
)
|
||||
|
||||
|
||||
_CALLBACK_AUTOMATIONS = (
|
||||
automation.CallbackAutomation(
|
||||
CONF_ON_CODE_RECEIVED,
|
||||
"add_on_code_received_callback",
|
||||
[(RFBridgeData, "data")],
|
||||
),
|
||||
automation.CallbackAutomation(
|
||||
CONF_ON_ADVANCED_CODE_RECEIVED,
|
||||
"add_on_advanced_code_received_callback",
|
||||
[(RFBridgeAdvancedData, "data")],
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
await uart.register_uart_device(var, config)
|
||||
|
||||
for conf in config.get(CONF_ON_CODE_RECEIVED, []):
|
||||
await automation.build_callback_automation(
|
||||
var, "add_on_code_received_callback", [(RFBridgeData, "data")], conf
|
||||
)
|
||||
for conf in config.get(CONF_ON_ADVANCED_CODE_RECEIVED, []):
|
||||
await automation.build_callback_automation(
|
||||
var,
|
||||
"add_on_advanced_code_received_callback",
|
||||
[(RFBridgeAdvancedData, "data")],
|
||||
conf,
|
||||
)
|
||||
await automation.build_callback_automations(var, config, _CALLBACK_AUTOMATIONS)
|
||||
|
||||
|
||||
RFBRIDGE_SEND_CODE_SCHEMA = cv.Schema(
|
||||
|
||||
@@ -84,6 +84,14 @@ CONFIG_SCHEMA = cv.All(
|
||||
)
|
||||
|
||||
|
||||
_CALLBACK_AUTOMATIONS = (
|
||||
automation.CallbackAutomation(CONF_ON_CLOCKWISE, "add_on_clockwise_callback"),
|
||||
automation.CallbackAutomation(
|
||||
CONF_ON_ANTICLOCKWISE, "add_on_anticlockwise_callback"
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
var = await sensor.new_sensor(config)
|
||||
await cg.register_component(var, config)
|
||||
@@ -104,14 +112,7 @@ async def to_code(config):
|
||||
if CONF_MAX_VALUE in config:
|
||||
cg.add(var.set_max_value(config[CONF_MAX_VALUE]))
|
||||
|
||||
for conf in config.get(CONF_ON_CLOCKWISE, []):
|
||||
await automation.build_callback_automation(
|
||||
var, "add_on_clockwise_callback", [], conf
|
||||
)
|
||||
for conf in config.get(CONF_ON_ANTICLOCKWISE, []):
|
||||
await automation.build_callback_automation(
|
||||
var, "add_on_anticlockwise_callback", [], conf
|
||||
)
|
||||
await automation.build_callback_automations(var, config, _CALLBACK_AUTOMATIONS)
|
||||
|
||||
|
||||
@automation.register_action(
|
||||
@@ -119,7 +120,7 @@ async def to_code(config):
|
||||
RotaryEncoderSetValueAction,
|
||||
cv.Schema(
|
||||
{
|
||||
cv.Required(CONF_ID): cv.use_id(sensor.Sensor),
|
||||
cv.Required(CONF_ID): cv.use_id(RotaryEncoderSensor),
|
||||
cv.Required(CONF_VALUE): cv.templatable(cv.int_),
|
||||
}
|
||||
),
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import platform
|
||||
|
||||
import esphome.codegen as cg
|
||||
import esphome.config_validation as cv
|
||||
|
||||
DEPENDENCIES = ["rp2040"]
|
||||
|
||||
@@ -31,7 +32,13 @@ async def to_code(config):
|
||||
# "earlephilhower/tool-pioasm-rp2040-earlephilhower",
|
||||
# ],
|
||||
# )
|
||||
file = PIOASM_DOWNLOADS[platform.system().lower()][platform.machine().lower()]
|
||||
os_name = platform.system().lower()
|
||||
arch = platform.machine().lower()
|
||||
if os_name not in PIOASM_DOWNLOADS or arch not in PIOASM_DOWNLOADS[os_name]:
|
||||
raise cv.Invalid(
|
||||
f"pioasm is not available for {platform.system()} {platform.machine()}"
|
||||
)
|
||||
file = PIOASM_DOWNLOADS[os_name][arch]
|
||||
cg.add_platformio_option(
|
||||
"platform_packages",
|
||||
[f"earlephilhower/tool-pioasm-rp2040-earlephilhower@{PIOASM_REPO_BASE}/{file}"],
|
||||
|
||||
@@ -148,7 +148,6 @@ CHIPSETS = {
|
||||
"WS2812B": Chipset.CHIPSET_WS2812B,
|
||||
"SK6812": Chipset.CHIPSET_SK6812,
|
||||
"SM16703": Chipset.CHIPSET_SM16703,
|
||||
"CUSTOM": Chipset.CHIPSET_CUSTOM,
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -71,6 +71,13 @@ FINAL_VALIDATE_SCHEMA = cv.Schema(
|
||||
)
|
||||
|
||||
|
||||
_CALLBACK_AUTOMATIONS = (
|
||||
automation.CallbackAutomation(
|
||||
CONF_ON_FINISHED_PLAYBACK, "add_on_finished_playback_callback"
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
@@ -86,10 +93,7 @@ async def to_code(config):
|
||||
|
||||
cg.add(var.set_gain(config[CONF_GAIN]))
|
||||
|
||||
for conf in config.get(CONF_ON_FINISHED_PLAYBACK, []):
|
||||
await automation.build_callback_automation(
|
||||
var, "add_on_finished_playback_callback", [], conf
|
||||
)
|
||||
await automation.build_callback_automations(var, config, _CALLBACK_AUTOMATIONS)
|
||||
|
||||
|
||||
@automation.register_action(
|
||||
|
||||
@@ -65,18 +65,22 @@ async def safe_mode_mark_successful_to_code(config, action_id, template_arg, arg
|
||||
return var
|
||||
|
||||
|
||||
_CALLBACK_AUTOMATIONS = (
|
||||
automation.CallbackAutomation(CONF_ON_SAFE_MODE, "add_on_safe_mode_callback"),
|
||||
)
|
||||
|
||||
|
||||
@coroutine_with_priority(CoroPriority.APPLICATION)
|
||||
async def to_code(config):
|
||||
if not config[CONF_DISABLED]:
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
|
||||
if on_safe_mode_config := config.get(CONF_ON_SAFE_MODE):
|
||||
if config.get(CONF_ON_SAFE_MODE):
|
||||
cg.add_define("USE_SAFE_MODE_CALLBACK")
|
||||
for conf in on_safe_mode_config:
|
||||
await automation.build_callback_automation(
|
||||
var, "add_on_safe_mode_callback", [], conf
|
||||
)
|
||||
await automation.build_callback_automations(
|
||||
var, config, _CALLBACK_AUTOMATIONS
|
||||
)
|
||||
|
||||
condition = var.should_enter_safe_mode(
|
||||
config[CONF_NUM_ATTEMPTS],
|
||||
|
||||
@@ -14,7 +14,10 @@ CODEOWNERS = ["@Azimath"]
|
||||
|
||||
sdp3x_ns = cg.esphome_ns.namespace("sdp3x")
|
||||
SDP3XComponent = sdp3x_ns.class_(
|
||||
"SDP3XComponent", cg.PollingComponent, sensirion_common.SensirionI2CDevice
|
||||
"SDP3XComponent",
|
||||
sensor.Sensor,
|
||||
cg.PollingComponent,
|
||||
sensirion_common.SensirionI2CDevice,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -33,6 +33,7 @@ CONFIG_SCHEMA = (
|
||||
# This authentication mode requires that the device must have transmit and receive functionality, a parity mode of "NONE", and a stop bit of one.
|
||||
FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema(
|
||||
"seeed_mr24hpc1",
|
||||
baud_rate=115200,
|
||||
require_tx=True,
|
||||
require_rx=True,
|
||||
parity="NONE",
|
||||
|
||||
@@ -62,8 +62,6 @@ void MR24HPC1Component::dump_config() {
|
||||
|
||||
// Initialisation functions
|
||||
void MR24HPC1Component::setup() {
|
||||
this->check_uart_settings(115200);
|
||||
|
||||
#ifdef USE_NUMBER
|
||||
if (this->custom_mode_number_ != nullptr) {
|
||||
this->custom_mode_number_->publish_state(0); // Zero out the custom mode
|
||||
|
||||
@@ -12,7 +12,7 @@ DEPENDENCIES = ["i2c"]
|
||||
|
||||
sen0321_sensor_ns = cg.esphome_ns.namespace("sen0321_sensor")
|
||||
Sen0321Sensor = sen0321_sensor_ns.class_(
|
||||
"Sen0321Sensor", cg.PollingComponent, i2c.I2CDevice
|
||||
"Sen0321Sensor", sensor.Sensor, cg.PollingComponent, i2c.I2CDevice
|
||||
)
|
||||
|
||||
CONFIG_SCHEMA = (
|
||||
|
||||
@@ -8,7 +8,7 @@ DEPENDENCIES = ["i2c"]
|
||||
|
||||
sen21231_sensor_ns = cg.esphome_ns.namespace("sen21231_sensor")
|
||||
Sen21231Sensor = sen21231_sensor_ns.class_(
|
||||
"Sen21231Sensor", cg.PollingComponent, i2c.I2CDevice
|
||||
"Sen21231Sensor", sensor.Sensor, cg.PollingComponent, i2c.I2CDevice
|
||||
)
|
||||
|
||||
CONFIG_SCHEMA = (
|
||||
|
||||
@@ -892,16 +892,19 @@ async def build_filters(config):
|
||||
return await cg.build_registry_list(FILTER_REGISTRY, config)
|
||||
|
||||
|
||||
_CALLBACK_AUTOMATIONS = (
|
||||
automation.CallbackAutomation(
|
||||
CONF_ON_VALUE, "add_on_state_callback", [(float, "x")]
|
||||
),
|
||||
automation.CallbackAutomation(
|
||||
CONF_ON_RAW_VALUE, "add_on_raw_state_callback", [(float, "x")]
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@coroutine_with_priority(CoroPriority.AUTOMATION)
|
||||
async def _build_sensor_automations(var, config):
|
||||
for conf_key, callback in (
|
||||
(CONF_ON_VALUE, "add_on_state_callback"),
|
||||
(CONF_ON_RAW_VALUE, "add_on_raw_state_callback"),
|
||||
):
|
||||
for conf in config.get(conf_key, []):
|
||||
await automation.build_callback_automation(
|
||||
var, callback, [(float, "x")], conf
|
||||
)
|
||||
await automation.build_callback_automations(var, config, _CALLBACK_AUTOMATIONS)
|
||||
for conf in config.get(CONF_ON_VALUE_RANGE, []):
|
||||
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
|
||||
await cg.register_component(trigger, conf)
|
||||
|
||||
@@ -48,34 +48,37 @@ FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema(
|
||||
)
|
||||
|
||||
|
||||
_CALLBACK_AUTOMATIONS = (
|
||||
automation.CallbackAutomation(
|
||||
CONF_ON_SMS_RECEIVED,
|
||||
"add_on_sms_received_callback",
|
||||
[(cg.std_string, "message"), (cg.std_string, "sender")],
|
||||
),
|
||||
automation.CallbackAutomation(
|
||||
CONF_ON_INCOMING_CALL,
|
||||
"add_on_incoming_call_callback",
|
||||
[(cg.std_string, "caller_id")],
|
||||
),
|
||||
automation.CallbackAutomation(
|
||||
CONF_ON_CALL_CONNECTED, "add_on_call_connected_callback"
|
||||
),
|
||||
automation.CallbackAutomation(
|
||||
CONF_ON_CALL_DISCONNECTED, "add_on_call_disconnected_callback"
|
||||
),
|
||||
automation.CallbackAutomation(
|
||||
CONF_ON_USSD_RECEIVED,
|
||||
"add_on_ussd_received_callback",
|
||||
[(cg.std_string, "ussd")],
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
await uart.register_uart_device(var, config)
|
||||
|
||||
for conf in config.get(CONF_ON_SMS_RECEIVED, []):
|
||||
await automation.build_callback_automation(
|
||||
var,
|
||||
"add_on_sms_received_callback",
|
||||
[(cg.std_string, "message"), (cg.std_string, "sender")],
|
||||
conf,
|
||||
)
|
||||
for conf in config.get(CONF_ON_INCOMING_CALL, []):
|
||||
await automation.build_callback_automation(
|
||||
var, "add_on_incoming_call_callback", [(cg.std_string, "caller_id")], conf
|
||||
)
|
||||
for conf in config.get(CONF_ON_CALL_CONNECTED, []):
|
||||
await automation.build_callback_automation(
|
||||
var, "add_on_call_connected_callback", [], conf
|
||||
)
|
||||
for conf in config.get(CONF_ON_CALL_DISCONNECTED, []):
|
||||
await automation.build_callback_automation(
|
||||
var, "add_on_call_disconnected_callback", [], conf
|
||||
)
|
||||
for conf in config.get(CONF_ON_USSD_RECEIVED, []):
|
||||
await automation.build_callback_automation(
|
||||
var, "add_on_ussd_received_callback", [(cg.std_string, "ussd")], conf
|
||||
)
|
||||
await automation.build_callback_automations(var, config, _CALLBACK_AUTOMATIONS)
|
||||
|
||||
|
||||
SIM800L_SEND_SMS_SCHEMA = cv.Schema(
|
||||
|
||||
@@ -14,7 +14,7 @@ CONFIG_SCHEMA = output.FLOAT_OUTPUT_SCHEMA.extend(
|
||||
{
|
||||
cv.GenerateID(CONF_SM16716_ID): cv.use_id(SM16716),
|
||||
cv.Required(CONF_ID): cv.declare_id(Channel),
|
||||
cv.Required(CONF_CHANNEL): cv.int_range(min=0, max=65535),
|
||||
cv.Required(CONF_CHANNEL): cv.int_range(min=0, max=254),
|
||||
}
|
||||
).extend(cv.COMPONENT_SCHEMA)
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ CONFIG_SCHEMA = output.FLOAT_OUTPUT_SCHEMA.extend(
|
||||
{
|
||||
cv.GenerateID(CONF_SM2135_ID): cv.use_id(SM2135),
|
||||
cv.Required(CONF_ID): cv.declare_id(Channel),
|
||||
cv.Required(CONF_CHANNEL): cv.int_range(min=0, max=65535),
|
||||
cv.Required(CONF_CHANNEL): cv.int_range(min=0, max=4),
|
||||
}
|
||||
).extend(cv.COMPONENT_SCHEMA)
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ CONFIG_SCHEMA = output.FLOAT_OUTPUT_SCHEMA.extend(
|
||||
{
|
||||
cv.GenerateID(CONF_SM2235_ID): cv.use_id(SM2235),
|
||||
cv.Required(CONF_ID): cv.declare_id(Channel),
|
||||
cv.Required(CONF_CHANNEL): cv.int_range(min=0, max=65535),
|
||||
cv.Required(CONF_CHANNEL): cv.int_range(min=0, max=4),
|
||||
}
|
||||
).extend(cv.COMPONENT_SCHEMA)
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ CONFIG_SCHEMA = output.FLOAT_OUTPUT_SCHEMA.extend(
|
||||
{
|
||||
cv.GenerateID(CONF_SM2335_ID): cv.use_id(SM2335),
|
||||
cv.Required(CONF_ID): cv.declare_id(Channel),
|
||||
cv.Required(CONF_CHANNEL): cv.int_range(min=0, max=65535),
|
||||
cv.Required(CONF_CHANNEL): cv.int_range(min=0, max=4),
|
||||
}
|
||||
).extend(cv.COMPONENT_SCHEMA)
|
||||
|
||||
|
||||
@@ -31,23 +31,26 @@ CONFIG_SCHEMA = (
|
||||
)
|
||||
|
||||
|
||||
_CALLBACK_AUTOMATIONS = (
|
||||
automation.CallbackAutomation(
|
||||
CONF_ON_DATA,
|
||||
"add_on_data_callback",
|
||||
[
|
||||
(
|
||||
cg.std_vector.template(cg.uint8).operator("ref").operator("const"),
|
||||
"bytes",
|
||||
),
|
||||
(cg.bool_, "valid"),
|
||||
],
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
await uart.register_uart_device(var, config)
|
||||
for conf in config.get(CONF_ON_DATA, []):
|
||||
await automation.build_callback_automation(
|
||||
var,
|
||||
"add_on_data_callback",
|
||||
[
|
||||
(
|
||||
cg.std_vector.template(cg.uint8).operator("ref").operator("const"),
|
||||
"bytes",
|
||||
),
|
||||
(cg.bool_, "valid"),
|
||||
],
|
||||
conf,
|
||||
)
|
||||
await automation.build_callback_automations(var, config, _CALLBACK_AUTOMATIONS)
|
||||
|
||||
|
||||
def obis_code(value):
|
||||
|
||||
@@ -121,17 +121,26 @@ def switch_schema(
|
||||
return _SWITCH_SCHEMA.extend(schema)
|
||||
|
||||
|
||||
_CALLBACK_AUTOMATIONS = (
|
||||
automation.CallbackAutomation(
|
||||
CONF_ON_STATE, "add_on_state_callback", [(bool, "x")]
|
||||
),
|
||||
automation.CallbackAutomation(
|
||||
CONF_ON_TURN_ON,
|
||||
"add_on_state_callback",
|
||||
forwarder=automation.TriggerOnTrueForwarder,
|
||||
),
|
||||
automation.CallbackAutomation(
|
||||
CONF_ON_TURN_OFF,
|
||||
"add_on_state_callback",
|
||||
forwarder=automation.TriggerOnFalseForwarder,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@coroutine_with_priority(CoroPriority.AUTOMATION)
|
||||
async def _build_switch_automations(var, config):
|
||||
for conf_key, args, forwarder in (
|
||||
(CONF_ON_STATE, [(bool, "x")], None),
|
||||
(CONF_ON_TURN_ON, [], automation.TriggerOnTrueForwarder),
|
||||
(CONF_ON_TURN_OFF, [], automation.TriggerOnFalseForwarder),
|
||||
):
|
||||
for conf in config.get(conf_key, []):
|
||||
await automation.build_callback_automation(
|
||||
var, "add_on_state_callback", args, conf, forwarder=forwarder
|
||||
)
|
||||
await automation.build_callback_automations(var, config, _CALLBACK_AUTOMATIONS)
|
||||
|
||||
|
||||
@setup_entity("switch")
|
||||
|
||||
@@ -11,7 +11,9 @@ CODEOWNERS = ["@sethgirvan"]
|
||||
DEPENDENCIES = ["i2c"]
|
||||
|
||||
tc74_ns = cg.esphome_ns.namespace("tc74")
|
||||
TC74Component = tc74_ns.class_("TC74Component", cg.PollingComponent, i2c.I2CDevice)
|
||||
TC74Component = tc74_ns.class_(
|
||||
"TC74Component", sensor.Sensor, cg.PollingComponent, i2c.I2CDevice
|
||||
)
|
||||
|
||||
CONFIG_SCHEMA = (
|
||||
sensor.sensor_schema(
|
||||
|
||||
@@ -184,16 +184,19 @@ async def build_filters(config):
|
||||
return await cg.build_registry_list(FILTER_REGISTRY, config)
|
||||
|
||||
|
||||
_CALLBACK_AUTOMATIONS = (
|
||||
automation.CallbackAutomation(
|
||||
CONF_ON_VALUE, "add_on_state_callback", [(cg.std_string, "x")]
|
||||
),
|
||||
automation.CallbackAutomation(
|
||||
CONF_ON_RAW_VALUE, "add_on_raw_state_callback", [(cg.std_string, "x")]
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@coroutine_with_priority(CoroPriority.AUTOMATION)
|
||||
async def _build_text_sensor_automations(var, config):
|
||||
for conf_key, callback in (
|
||||
(CONF_ON_VALUE, "add_on_state_callback"),
|
||||
(CONF_ON_RAW_VALUE, "add_on_raw_state_callback"),
|
||||
):
|
||||
for conf in config.get(conf_key, []):
|
||||
await automation.build_callback_automation(
|
||||
var, callback, [(cg.std_string, "x")], conf
|
||||
)
|
||||
await automation.build_callback_automations(var, config, _CALLBACK_AUTOMATIONS)
|
||||
|
||||
|
||||
@setup_entity("text_sensor")
|
||||
|
||||
@@ -16,7 +16,7 @@ CONFIG_SCHEMA = output.FLOAT_OUTPUT_SCHEMA.extend(
|
||||
{
|
||||
cv.GenerateID(CONF_TLC5947_ID): cv.use_id(TLC5947),
|
||||
cv.Required(CONF_ID): cv.declare_id(TLC5947Channel),
|
||||
cv.Required(CONF_CHANNEL): cv.int_range(min=0, max=65535),
|
||||
cv.Required(CONF_CHANNEL): cv.uint16_t,
|
||||
}
|
||||
).extend(cv.COMPONENT_SCHEMA)
|
||||
|
||||
|
||||
@@ -11,11 +11,11 @@ namespace tlc5947 {
|
||||
|
||||
class TLC5947Channel : public output::FloatOutput, public Parented<TLC5947> {
|
||||
public:
|
||||
void set_channel(uint8_t channel) { this->channel_ = channel; }
|
||||
void set_channel(uint16_t channel) { this->channel_ = channel; }
|
||||
|
||||
protected:
|
||||
void write_state(float state) override;
|
||||
uint8_t channel_;
|
||||
uint16_t channel_;
|
||||
};
|
||||
|
||||
} // namespace tlc5947
|
||||
|
||||
@@ -16,7 +16,7 @@ CONFIG_SCHEMA = output.FLOAT_OUTPUT_SCHEMA.extend(
|
||||
{
|
||||
cv.GenerateID(CONF_TLC5971_ID): cv.use_id(TLC5971),
|
||||
cv.Required(CONF_ID): cv.declare_id(TLC5971Channel),
|
||||
cv.Required(CONF_CHANNEL): cv.int_range(min=0, max=65535),
|
||||
cv.Required(CONF_CHANNEL): cv.uint16_t,
|
||||
}
|
||||
).extend(cv.COMPONENT_SCHEMA)
|
||||
|
||||
|
||||
@@ -11,11 +11,11 @@ namespace tlc5971 {
|
||||
|
||||
class TLC5971Channel : public output::FloatOutput, public Parented<TLC5971> {
|
||||
public:
|
||||
void set_channel(uint8_t channel) { this->channel_ = channel; }
|
||||
void set_channel(uint16_t channel) { this->channel_ = channel; }
|
||||
|
||||
protected:
|
||||
void write_state(float state) override;
|
||||
uint8_t channel_;
|
||||
uint16_t channel_;
|
||||
};
|
||||
|
||||
} // namespace tlc5971
|
||||
|
||||
@@ -130,12 +130,9 @@ async def to_code(config):
|
||||
if (listen_address := str(config[CONF_LISTEN_ADDRESS])) != "255.255.255.255":
|
||||
cg.add(var.set_listen_address(listen_address))
|
||||
cg.add(var.set_addresses([str(addr) for addr in config[CONF_ADDRESSES]]))
|
||||
if on_receive := config.get(CONF_ON_RECEIVE):
|
||||
on_receive = on_receive[0]
|
||||
trigger_id = cg.new_Pvariable(on_receive[CONF_TRIGGER_ID])
|
||||
trigger = await automation.build_automation(
|
||||
trigger_id, trigger_argtype, on_receive
|
||||
)
|
||||
for conf in config.get(CONF_ON_RECEIVE, []):
|
||||
trigger_id = cg.new_Pvariable(conf[CONF_TRIGGER_ID])
|
||||
trigger = await automation.build_automation(trigger_id, trigger_argtype, conf)
|
||||
trigger_lambda = await cg.process_lambda(
|
||||
trigger.trigger(
|
||||
cg.std_vector.template(cg.uint8)(
|
||||
@@ -146,6 +143,7 @@ async def to_code(config):
|
||||
listener_argtype,
|
||||
)
|
||||
cg.add(var.add_listener(trigger_lambda))
|
||||
if config.get(CONF_ON_RECEIVE):
|
||||
cg.add(var.set_should_listen())
|
||||
|
||||
|
||||
|
||||
@@ -71,9 +71,11 @@ 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)
|
||||
# Validation result should be the path, not the loaded certificate
|
||||
return value
|
||||
return str(cv.file_(value))
|
||||
|
||||
|
||||
def _validate_load_private_key(key, cert_pw):
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
from esphome import core
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import binary_sensor, esp32_ble_tracker
|
||||
import esphome.config_validation as cv
|
||||
@@ -21,9 +22,10 @@ CONFIG_SCHEMA = cv.All(
|
||||
.extend(
|
||||
{
|
||||
cv.Required(CONF_MAC_ADDRESS): cv.mac_address,
|
||||
cv.Optional(
|
||||
CONF_TIMEOUT, default="5s"
|
||||
): cv.positive_time_period_milliseconds,
|
||||
cv.Optional(CONF_TIMEOUT, default="5s"): cv.All(
|
||||
cv.positive_time_period_milliseconds,
|
||||
cv.Range(max=core.TimePeriod(milliseconds=65535)),
|
||||
),
|
||||
}
|
||||
)
|
||||
.extend(esp32_ble_tracker.ESP_BLE_DEVICE_SCHEMA)
|
||||
|
||||
@@ -1468,17 +1468,53 @@ hex_uint64_t = hex_int_range(min=0, max=18446744073709551615)
|
||||
i2c_address = hex_uint8_t
|
||||
|
||||
|
||||
def percentage(value):
|
||||
def percentage(value: object) -> float:
|
||||
"""Validate that the value is a percentage.
|
||||
|
||||
The resulting value is an integer in the range 0.0 to 1.0.
|
||||
The resulting value is a float in the range 0.0 to 1.0.
|
||||
"""
|
||||
value = possibly_negative_percentage(value)
|
||||
value = _parse_percentage(value)
|
||||
return zero_to_one_float(value)
|
||||
|
||||
|
||||
def possibly_negative_percentage(value):
|
||||
has_percent_sign = False
|
||||
def possibly_negative_percentage(value: object) -> float:
|
||||
"""Validate that the value is a possibly negative percentage.
|
||||
|
||||
The resulting value is a float in the range -1.0 to 1.0.
|
||||
"""
|
||||
value = _parse_percentage(value)
|
||||
return negative_one_to_one_float(value)
|
||||
|
||||
|
||||
def unbounded_percentage(value: object) -> float:
|
||||
"""Validate that the value is a percentage, allowing values above 100%.
|
||||
|
||||
The resulting value is a non-negative float with no upper bound.
|
||||
For example, "150%" returns 1.5 and "50%" returns 0.5.
|
||||
"""
|
||||
value = _parse_percentage(value)
|
||||
if value < 0:
|
||||
raise Invalid("Percentage must not be negative")
|
||||
return value
|
||||
|
||||
|
||||
def unbounded_possibly_negative_percentage(value: object) -> float:
|
||||
"""Validate that the value is a possibly negative percentage without bounds.
|
||||
|
||||
The resulting value is an unbounded float.
|
||||
For example, "200%" returns 2.0 and "-150%" returns -1.5.
|
||||
"""
|
||||
return _parse_percentage(value)
|
||||
|
||||
|
||||
def _parse_percentage(value: object) -> float:
|
||||
"""Parse a percentage string or number into a float.
|
||||
|
||||
Handles both "50%" style strings and raw float values.
|
||||
Values without a percent sign above 1.0 or below -1.0 are rejected
|
||||
to prevent user mistakes (e.g. writing 50 instead of 50%).
|
||||
"""
|
||||
has_percent_sign: bool = False
|
||||
if isinstance(value, str):
|
||||
try:
|
||||
if value.endswith("%"):
|
||||
@@ -1490,21 +1526,16 @@ def possibly_negative_percentage(value):
|
||||
# pylint: disable=raise-missing-from
|
||||
raise Invalid("invalid number")
|
||||
try:
|
||||
if value > 1:
|
||||
msg = "Percentage must not be higher than 100%."
|
||||
if not has_percent_sign:
|
||||
msg += " Please put a percent sign after the number!"
|
||||
raise Invalid(msg)
|
||||
if value < -1:
|
||||
msg = "Percentage must not be smaller than -100%."
|
||||
if not has_percent_sign:
|
||||
msg += " Please put a percent sign after the number!"
|
||||
raise Invalid(msg)
|
||||
if not has_percent_sign and (value > 1 or value < -1):
|
||||
raise Invalid(
|
||||
"Percentage value must use a percent sign for values "
|
||||
"outside -1.0 to 1.0. Please put a percent sign after the number!"
|
||||
)
|
||||
except TypeError:
|
||||
raise Invalid( # pylint: disable=raise-missing-from
|
||||
"Expected percentage or float between -1.0 and 1.0"
|
||||
"Expected percentage or float"
|
||||
)
|
||||
return negative_one_to_one_float(value)
|
||||
return float(value)
|
||||
|
||||
|
||||
def percentage_int(value):
|
||||
|
||||
@@ -123,6 +123,10 @@
|
||||
#define USE_NEXTION_MAX_COMMANDS_PER_LOOP
|
||||
#define USE_NEXTION_MAX_QUEUE_SIZE
|
||||
#define USE_NEXTION_TFT_UPLOAD
|
||||
#define USE_NEXTION_TRIGGER_CUSTOM_BINARY_SENSOR
|
||||
#define USE_NEXTION_TRIGGER_CUSTOM_SENSOR
|
||||
#define USE_NEXTION_TRIGGER_CUSTOM_SWITCH
|
||||
#define USE_NEXTION_TRIGGER_CUSTOM_TEXT_SENSOR
|
||||
#define USE_NEXTION_WAVEFORM
|
||||
#define USE_NUMBER
|
||||
#define USE_OUTPUT
|
||||
|
||||
+28
-5
@@ -1,7 +1,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from contextlib import suppress
|
||||
from collections.abc import Callable, Generator
|
||||
from contextlib import contextmanager, suppress
|
||||
import functools
|
||||
import inspect
|
||||
from io import BytesIO, TextIOBase, TextIOWrapper
|
||||
@@ -44,6 +44,27 @@ _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:
|
||||
@@ -466,6 +487,8 @@ 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)
|
||||
@@ -473,10 +496,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=_load_yaml_internal
|
||||
) -> Any:
|
||||
def parse_yaml(file_name: Path, file_handle: TextIOWrapper, yaml_loader=None) -> 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
|
||||
|
||||
@@ -5,7 +5,7 @@ pyupgrade==3.21.2 # also change in .pre-commit-config.yaml when updating
|
||||
pre-commit
|
||||
|
||||
# Unit tests
|
||||
pytest==9.0.2
|
||||
pytest==9.0.3
|
||||
pytest-cov==7.1.0
|
||||
pytest-mock==3.15.1
|
||||
pytest-asyncio==1.3.0
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
button:
|
||||
- platform: template
|
||||
name: Send command test
|
||||
on_press:
|
||||
- emontx.send_command:
|
||||
id: test_emontx
|
||||
command: "v"
|
||||
|
||||
emontx:
|
||||
id: test_emontx
|
||||
on_json:
|
||||
- then:
|
||||
- logger.log: "Got JSON"
|
||||
on_data:
|
||||
- then:
|
||||
- logger.log:
|
||||
format: "Got data: %s"
|
||||
args: [data.c_str()]
|
||||
|
||||
sensor:
|
||||
- platform: emontx
|
||||
name: Power
|
||||
tag_name: P1
|
||||
emontx_id: test_emontx
|
||||
unit_of_measurement: W
|
||||
@@ -0,0 +1,4 @@
|
||||
packages:
|
||||
uart: !include ../../test_build_components/common/uart_115200/esp32-idf.yaml
|
||||
|
||||
<<: !include common.yaml
|
||||
@@ -0,0 +1,4 @@
|
||||
packages:
|
||||
uart: !include ../../test_build_components/common/uart_115200/esp8266-ard.yaml
|
||||
|
||||
<<: !include common.yaml
|
||||
@@ -0,0 +1,4 @@
|
||||
packages:
|
||||
uart: !include ../../test_build_components/common/uart_115200/rp2040-ard.yaml
|
||||
|
||||
<<: !include common.yaml
|
||||
@@ -6,6 +6,11 @@ esphome:
|
||||
speed: 255
|
||||
direction: BACKWARD
|
||||
id: test_motor
|
||||
- grove_tb6612fng.run:
|
||||
channel: 0
|
||||
speed: !lambda "return 200;"
|
||||
direction: BACKWARD
|
||||
id: test_motor
|
||||
- grove_tb6612fng.stop:
|
||||
channel: 1
|
||||
id: test_motor
|
||||
|
||||
@@ -22,3 +22,11 @@ output:
|
||||
id: digipot_wiper_4
|
||||
mcp4461_id: mcp4461_digipot_01
|
||||
channel: D
|
||||
|
||||
- platform: mcp4461
|
||||
id: digipot_wiper_5
|
||||
mcp4461_id: mcp4461_digipot_01
|
||||
channel: A
|
||||
terminal_a: false
|
||||
terminal_b: false
|
||||
terminal_w: false
|
||||
|
||||
@@ -286,6 +286,31 @@ display:
|
||||
on_buffer_overflow:
|
||||
then:
|
||||
logger.log: "Nextion reported a buffer overflow!"
|
||||
on_custom_text_sensor:
|
||||
then:
|
||||
- lambda: |-
|
||||
// key: StringRef, value: StringRef
|
||||
if (key == "csv") {
|
||||
// parse value here, or forward to your own component
|
||||
ESP_LOGD("nextion.csv", "Got CSV: %s", value.c_str());
|
||||
}
|
||||
on_custom_sensor:
|
||||
then:
|
||||
- lambda: |-
|
||||
// key: StringRef, value: int32_t
|
||||
if (key == "temperature_raw") {
|
||||
ESP_LOGD("nextion.custom", "%s=%d", key.c_str(), value);
|
||||
}
|
||||
on_custom_binary_sensor:
|
||||
then:
|
||||
- lambda: |-
|
||||
if (key == "btn1") {
|
||||
ESP_LOGD("nextion.btn", "btn1=%s", ONOFF(value));
|
||||
}
|
||||
on_custom_switch:
|
||||
then:
|
||||
- lambda: |-
|
||||
ESP_LOGD("nextion.sw", "%s=%s", key.c_str(), ONOFF(value));
|
||||
on_page:
|
||||
then:
|
||||
lambda: 'ESP_LOGD("display","Display shows new page %u", x);'
|
||||
@@ -304,8 +329,8 @@ display:
|
||||
on_wake:
|
||||
then:
|
||||
lambda: 'ESP_LOGD("display","Display woke up");'
|
||||
update_interval: 5s
|
||||
start_up_page: 1
|
||||
startup_override_ms: 10000ms # Wait 10s for display ready
|
||||
touch_sleep_timeout: 3
|
||||
update_interval: 5s
|
||||
wake_up_page: 2
|
||||
|
||||
@@ -9,3 +9,4 @@ uart:
|
||||
tx_pin: ${tx_pin}
|
||||
rx_pin: ${rx_pin}
|
||||
baud_rate: 115200
|
||||
rx_buffer_size: 2048
|
||||
|
||||
@@ -9,3 +9,4 @@ uart:
|
||||
tx_pin: ${tx_pin}
|
||||
rx_pin: ${rx_pin}
|
||||
baud_rate: 115200
|
||||
rx_buffer_size: 2048
|
||||
|
||||
@@ -10,3 +10,4 @@ uart:
|
||||
tx_pin: ${tx_pin}
|
||||
rx_pin: ${rx_pin}
|
||||
baud_rate: 115200
|
||||
rx_buffer_size: 2048
|
||||
|
||||
@@ -10,3 +10,4 @@ uart:
|
||||
tx_pin: ${tx_pin}
|
||||
rx_pin: ${rx_pin}
|
||||
baud_rate: 115200
|
||||
rx_buffer_size: 2048
|
||||
|
||||
@@ -9,3 +9,4 @@ uart:
|
||||
tx_pin: ${tx_pin}
|
||||
rx_pin: ${rx_pin}
|
||||
baud_rate: 115200
|
||||
rx_buffer_size: 2048
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user