mirror of
https://github.com/esphome/esphome.git
synced 2026-02-10 09:42:01 +00:00
Compare commits
4 Commits
esphome_bu
...
http_reque
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
154256e4ee | ||
|
|
038a939f96 | ||
|
|
3f9d6e39a9 | ||
|
|
ba6e050c91 |
@@ -1 +1 @@
|
||||
37ec8d5a343c8d0a485fd2118cbdabcbccd7b9bca197e4a392be75087974dced
|
||||
069fa9526c52f7c580a9ec17c7678d12f142221387e9b561c18f95394d4629a3
|
||||
|
||||
@@ -965,38 +965,6 @@ def command_clean(args: ArgsProtocol, config: ConfigType) -> int | None:
|
||||
return 0
|
||||
|
||||
|
||||
def command_bundle(args: ArgsProtocol, config: ConfigType) -> int | None:
|
||||
from esphome.bundle import BUNDLE_EXTENSION, ConfigBundleCreator
|
||||
|
||||
creator = ConfigBundleCreator(config)
|
||||
|
||||
if args.list_only:
|
||||
files = creator.discover_files()
|
||||
for bf in sorted(files, key=lambda f: f.path):
|
||||
safe_print(f" {bf.path}")
|
||||
_LOGGER.info("Found %d files", len(files))
|
||||
return 0
|
||||
|
||||
result = creator.create_bundle()
|
||||
|
||||
if args.output:
|
||||
output_path = Path(args.output)
|
||||
else:
|
||||
stem = CORE.config_path.stem
|
||||
output_path = CORE.config_dir / f"{stem}{BUNDLE_EXTENSION}"
|
||||
|
||||
output_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
output_path.write_bytes(result.data)
|
||||
|
||||
_LOGGER.info(
|
||||
"Bundle created: %s (%d files, %.1f KB)",
|
||||
output_path,
|
||||
len(result.files),
|
||||
len(result.data) / 1024,
|
||||
)
|
||||
return 0
|
||||
|
||||
|
||||
def command_dashboard(args: ArgsProtocol) -> int | None:
|
||||
from esphome.dashboard import dashboard
|
||||
|
||||
@@ -1274,7 +1242,6 @@ POST_CONFIG_ACTIONS = {
|
||||
"rename": command_rename,
|
||||
"discover": command_discover,
|
||||
"analyze-memory": command_analyze_memory,
|
||||
"bundle": command_bundle,
|
||||
}
|
||||
|
||||
SIMPLE_CONFIG_ACTIONS = [
|
||||
@@ -1578,24 +1545,6 @@ def parse_args(argv):
|
||||
"configuration", help="Your YAML configuration file(s).", nargs="+"
|
||||
)
|
||||
|
||||
parser_bundle = subparsers.add_parser(
|
||||
"bundle",
|
||||
help="Create a self-contained config bundle for remote compilation.",
|
||||
)
|
||||
parser_bundle.add_argument(
|
||||
"configuration", help="Your YAML configuration file(s).", nargs="+"
|
||||
)
|
||||
parser_bundle.add_argument(
|
||||
"-o",
|
||||
"--output",
|
||||
help="Output path for the bundle archive.",
|
||||
)
|
||||
parser_bundle.add_argument(
|
||||
"--list-only",
|
||||
help="List discovered files without creating the archive.",
|
||||
action="store_true",
|
||||
)
|
||||
|
||||
# Keep backward compatibility with the old command line format of
|
||||
# esphome <config> <command>.
|
||||
#
|
||||
@@ -1674,16 +1623,6 @@ def run_esphome(argv):
|
||||
_LOGGER.warning("Skipping secrets file %s", conf_path)
|
||||
return 0
|
||||
|
||||
# Bundle support: if the configuration is a .esphomebundle, extract it
|
||||
# and rewrite conf_path to the extracted YAML config.
|
||||
from esphome.bundle import is_bundle_path, prepare_bundle_for_compile
|
||||
|
||||
if is_bundle_path(conf_path):
|
||||
_LOGGER.info("Extracting config bundle %s...", conf_path)
|
||||
conf_path = prepare_bundle_for_compile(conf_path)
|
||||
# Update the argument so downstream code sees the extracted path
|
||||
args.configuration[0] = str(conf_path)
|
||||
|
||||
CORE.config_path = conf_path
|
||||
CORE.dashboard = args.dashboard
|
||||
|
||||
|
||||
@@ -43,7 +43,6 @@ _READELF_SECTION_PATTERN = re.compile(
|
||||
# Component category prefixes
|
||||
_COMPONENT_PREFIX_ESPHOME = "[esphome]"
|
||||
_COMPONENT_PREFIX_EXTERNAL = "[external]"
|
||||
_COMPONENT_PREFIX_LIB = "[lib]"
|
||||
_COMPONENT_CORE = f"{_COMPONENT_PREFIX_ESPHOME}core"
|
||||
_COMPONENT_API = f"{_COMPONENT_PREFIX_ESPHOME}api"
|
||||
|
||||
@@ -57,16 +56,6 @@ SymbolInfoType = tuple[str, int, str]
|
||||
# RAM sections - symbols in these sections consume RAM
|
||||
RAM_SECTIONS = frozenset([".data", ".bss"])
|
||||
|
||||
# nm symbol types for global/weak defined symbols (used for library symbol mapping)
|
||||
# Only global (uppercase) and weak symbols are safe to use - local symbols (lowercase)
|
||||
# can have name collisions across compilation units
|
||||
_NM_DEFINED_GLOBAL_TYPES = frozenset({"T", "D", "B", "R", "W", "V"})
|
||||
|
||||
# Pattern matching compiler-generated local names that can collide across compilation
|
||||
# units (e.g., packet$19, buf$20, flag$5261). These are unsafe for name-based lookup.
|
||||
# Does NOT match mangled C++ names with optimization suffixes (e.g., func$isra$0).
|
||||
_COMPILER_LOCAL_PATTERN = re.compile(r"^[a-zA-Z_]\w*\$\d+$")
|
||||
|
||||
|
||||
@dataclass
|
||||
class MemorySection:
|
||||
@@ -190,19 +179,11 @@ class MemoryAnalyzer:
|
||||
self._sdk_symbols: list[SDKSymbol] = []
|
||||
# CSWTCH symbols: list of (name, size, source_file, component)
|
||||
self._cswtch_symbols: list[tuple[str, int, str, str]] = []
|
||||
# Library symbol mapping: symbol_name -> library_name
|
||||
self._lib_symbol_map: dict[str, str] = {}
|
||||
# Library dir to name mapping: "lib641" -> "espsoftwareserial",
|
||||
# "espressif__mdns" -> "mdns"
|
||||
self._lib_hash_to_name: dict[str, str] = {}
|
||||
# Heuristic category to library redirect: "mdns_lib" -> "[lib]mdns"
|
||||
self._heuristic_to_lib: dict[str, str] = {}
|
||||
|
||||
def analyze(self) -> dict[str, ComponentMemory]:
|
||||
"""Analyze the ELF file and return component memory usage."""
|
||||
self._parse_sections()
|
||||
self._parse_symbols()
|
||||
self._scan_libraries()
|
||||
self._categorize_symbols()
|
||||
self._analyze_cswtch_symbols()
|
||||
self._analyze_sdk_libraries()
|
||||
@@ -347,19 +328,15 @@ class MemoryAnalyzer:
|
||||
# If no component match found, it's core
|
||||
return _COMPONENT_CORE
|
||||
|
||||
# Check library symbol map (more accurate than heuristic patterns)
|
||||
if lib_name := self._lib_symbol_map.get(symbol_name):
|
||||
return f"{_COMPONENT_PREFIX_LIB}{lib_name}"
|
||||
|
||||
# Check against symbol patterns
|
||||
for component, patterns in SYMBOL_PATTERNS.items():
|
||||
if any(pattern in symbol_name for pattern in patterns):
|
||||
return self._heuristic_to_lib.get(component, component)
|
||||
return component
|
||||
|
||||
# Check against demangled patterns
|
||||
for component, patterns in DEMANGLED_PATTERNS.items():
|
||||
if any(pattern in demangled for pattern in patterns):
|
||||
return self._heuristic_to_lib.get(component, component)
|
||||
return component
|
||||
|
||||
# Special cases that need more complex logic
|
||||
|
||||
@@ -407,327 +384,6 @@ class MemoryAnalyzer:
|
||||
|
||||
return "Other Core"
|
||||
|
||||
def _discover_pio_libraries(
|
||||
self,
|
||||
libraries: dict[str, list[Path]],
|
||||
hash_to_name: dict[str, str],
|
||||
) -> None:
|
||||
"""Discover PlatformIO third-party libraries from the build directory.
|
||||
|
||||
Scans ``lib<hex>/`` directories under ``.pioenvs/<env>/`` to find
|
||||
library names and their ``.a`` archive or ``.o`` file paths.
|
||||
|
||||
Args:
|
||||
libraries: Dict to populate with library name -> file path list mappings.
|
||||
Prefers ``.a`` archives when available, falls back to ``.o`` files
|
||||
(e.g., pioarduino ESP32 Arduino builds only produce ``.o`` files).
|
||||
hash_to_name: Dict to populate with dir name -> library name mappings
|
||||
for CSWTCH attribution (e.g., ``lib641`` -> ``espsoftwareserial``).
|
||||
"""
|
||||
build_dir = self.elf_path.parent
|
||||
|
||||
for entry in build_dir.iterdir():
|
||||
if not entry.is_dir() or not entry.name.startswith("lib"):
|
||||
continue
|
||||
# Validate that the suffix after "lib" is a hex hash
|
||||
hex_part = entry.name[3:]
|
||||
if not hex_part:
|
||||
continue
|
||||
try:
|
||||
int(hex_part, 16)
|
||||
except ValueError:
|
||||
continue
|
||||
|
||||
# Each lib<hex>/ directory contains a subdirectory named after the library
|
||||
for lib_subdir in entry.iterdir():
|
||||
if not lib_subdir.is_dir():
|
||||
continue
|
||||
lib_name = lib_subdir.name.lower()
|
||||
|
||||
# Prefer .a archive (lib<LibraryName>.a), fall back to .o files
|
||||
# e.g., lib72a/ESPAsyncTCP/... has lib72a/libESPAsyncTCP.a
|
||||
archive = entry / f"lib{lib_subdir.name}.a"
|
||||
if archive.exists():
|
||||
file_paths = [archive]
|
||||
elif archives := list(entry.glob("*.a")):
|
||||
# Case-insensitive fallback
|
||||
file_paths = [archives[0]]
|
||||
else:
|
||||
# No .a archive (e.g., pioarduino CMake builds) - use .o files
|
||||
file_paths = sorted(lib_subdir.rglob("*.o"))
|
||||
|
||||
if file_paths:
|
||||
libraries[lib_name] = file_paths
|
||||
hash_to_name[entry.name] = lib_name
|
||||
_LOGGER.debug(
|
||||
"Discovered PlatformIO library: %s -> %s",
|
||||
lib_subdir.name,
|
||||
file_paths[0],
|
||||
)
|
||||
|
||||
def _discover_idf_managed_components(
|
||||
self,
|
||||
libraries: dict[str, list[Path]],
|
||||
hash_to_name: dict[str, str],
|
||||
) -> None:
|
||||
"""Discover ESP-IDF managed component libraries from the build directory.
|
||||
|
||||
ESP-IDF managed components (from the IDF component registry) use a
|
||||
``<vendor>__<name>`` naming convention. Source files live under
|
||||
``managed_components/<vendor>__<name>/`` and the compiled archives are at
|
||||
``esp-idf/<vendor>__<name>/lib<vendor>__<name>.a``.
|
||||
|
||||
Args:
|
||||
libraries: Dict to populate with library name -> file path list mappings.
|
||||
hash_to_name: Dict to populate with dir name -> library name mappings
|
||||
for CSWTCH attribution (e.g., ``espressif__mdns`` -> ``mdns``).
|
||||
"""
|
||||
build_dir = self.elf_path.parent
|
||||
|
||||
managed_dir = build_dir / "managed_components"
|
||||
if not managed_dir.is_dir():
|
||||
return
|
||||
|
||||
espidf_dir = build_dir / "esp-idf"
|
||||
|
||||
for entry in managed_dir.iterdir():
|
||||
if not entry.is_dir() or "__" not in entry.name:
|
||||
continue
|
||||
|
||||
# Extract the short name: espressif__mdns -> mdns
|
||||
full_name = entry.name # e.g., espressif__mdns
|
||||
short_name = full_name.split("__", 1)[1].lower()
|
||||
|
||||
# Find the .a archive under esp-idf/<vendor>__<name>/
|
||||
archive = espidf_dir / full_name / f"lib{full_name}.a"
|
||||
if archive.exists():
|
||||
libraries[short_name] = [archive]
|
||||
hash_to_name[full_name] = short_name
|
||||
_LOGGER.debug(
|
||||
"Discovered IDF managed component: %s -> %s",
|
||||
short_name,
|
||||
archive,
|
||||
)
|
||||
|
||||
def _build_library_symbol_map(
|
||||
self, libraries: dict[str, list[Path]]
|
||||
) -> dict[str, str]:
|
||||
"""Build a symbol-to-library mapping from library archives or object files.
|
||||
|
||||
Runs ``nm --defined-only`` on each ``.a`` or ``.o`` file to collect
|
||||
global and weak defined symbols.
|
||||
|
||||
Args:
|
||||
libraries: Dictionary mapping library name to list of file paths
|
||||
(``.a`` archives or ``.o`` object files).
|
||||
|
||||
Returns:
|
||||
Dictionary mapping symbol name to library name.
|
||||
"""
|
||||
symbol_map: dict[str, str] = {}
|
||||
|
||||
if not self.nm_path:
|
||||
return symbol_map
|
||||
|
||||
for lib_name, file_paths in libraries.items():
|
||||
result = run_tool(
|
||||
[self.nm_path, "--defined-only", *(str(p) for p in file_paths)],
|
||||
timeout=10,
|
||||
)
|
||||
if result is None or result.returncode != 0:
|
||||
continue
|
||||
|
||||
for line in result.stdout.splitlines():
|
||||
parts = line.split()
|
||||
if len(parts) < 3:
|
||||
continue
|
||||
|
||||
sym_type = parts[-2]
|
||||
sym_name = parts[-1]
|
||||
|
||||
# Include global defined symbols (uppercase) and weak symbols (W/V)
|
||||
if sym_type in _NM_DEFINED_GLOBAL_TYPES:
|
||||
symbol_map[sym_name] = lib_name
|
||||
|
||||
return symbol_map
|
||||
|
||||
@staticmethod
|
||||
def _build_heuristic_to_lib_mapping(
|
||||
library_names: set[str],
|
||||
) -> dict[str, str]:
|
||||
"""Build mapping from heuristic pattern categories to discovered libraries.
|
||||
|
||||
Heuristic categories like ``mdns_lib``, ``web_server_lib``, ``async_tcp``
|
||||
exist as approximations for library attribution. When we discover the
|
||||
actual library, symbols matching those heuristics should be redirected
|
||||
to the ``[lib]`` category instead.
|
||||
|
||||
The mapping is built by checking if the normalized category name
|
||||
(stripped of ``_lib`` suffix and underscores) appears as a substring
|
||||
of any discovered library name.
|
||||
|
||||
Examples::
|
||||
|
||||
mdns_lib -> mdns -> in "mdns" or "esp8266mdns" -> [lib]mdns
|
||||
web_server_lib -> webserver -> in "espasyncwebserver" -> [lib]espasyncwebserver
|
||||
async_tcp -> asynctcp -> in "espasynctcp" -> [lib]espasynctcp
|
||||
|
||||
Args:
|
||||
library_names: Set of discovered library names (lowercase).
|
||||
|
||||
Returns:
|
||||
Dictionary mapping heuristic category to ``[lib]<name>`` string.
|
||||
"""
|
||||
mapping: dict[str, str] = {}
|
||||
all_categories = set(SYMBOL_PATTERNS) | set(DEMANGLED_PATTERNS)
|
||||
|
||||
for category in all_categories:
|
||||
base = category.removesuffix("_lib").replace("_", "")
|
||||
# Collect all libraries whose name contains the base string
|
||||
candidates = [lib_name for lib_name in library_names if base in lib_name]
|
||||
if not candidates:
|
||||
continue
|
||||
|
||||
# Choose a deterministic "best" match:
|
||||
# 1. Prefer exact name matches over substring matches.
|
||||
# 2. Among non-exact matches, prefer the shortest library name.
|
||||
# 3. Break remaining ties lexicographically.
|
||||
best_lib = min(
|
||||
candidates,
|
||||
key=lambda lib_name, _base=base: (
|
||||
lib_name != _base,
|
||||
len(lib_name),
|
||||
lib_name,
|
||||
),
|
||||
)
|
||||
mapping[category] = f"{_COMPONENT_PREFIX_LIB}{best_lib}"
|
||||
|
||||
if mapping:
|
||||
_LOGGER.debug(
|
||||
"Heuristic-to-library redirects: %s",
|
||||
", ".join(f"{k} -> {v}" for k, v in sorted(mapping.items())),
|
||||
)
|
||||
|
||||
return mapping
|
||||
|
||||
def _parse_map_file(self) -> dict[str, str] | None:
|
||||
"""Parse linker map file to build authoritative symbol-to-library mapping.
|
||||
|
||||
The linker map file contains the definitive source attribution for every
|
||||
symbol, including local/static ones that ``nm`` cannot safely export.
|
||||
|
||||
Map file format (GNU ld)::
|
||||
|
||||
.text._mdns_service_task
|
||||
0x400e9fdc 0x65c .pioenvs/env/esp-idf/espressif__mdns/libespressif__mdns.a(mdns.c.o)
|
||||
|
||||
Each section entry has a ``.section.symbol_name`` line followed by an
|
||||
indented line with address, size, and source path.
|
||||
|
||||
Returns:
|
||||
Symbol-to-library dict, or ``None`` if no usable map file exists.
|
||||
"""
|
||||
map_path = self.elf_path.with_suffix(".map")
|
||||
if not map_path.exists() or map_path.stat().st_size < 10000:
|
||||
return None
|
||||
|
||||
_LOGGER.info("Parsing linker map file: %s", map_path.name)
|
||||
|
||||
try:
|
||||
map_text = map_path.read_text(encoding="utf-8", errors="replace")
|
||||
except OSError as err:
|
||||
_LOGGER.warning("Failed to read map file: %s", err)
|
||||
return None
|
||||
|
||||
symbol_map: dict[str, str] = {}
|
||||
current_symbol: str | None = None
|
||||
section_prefixes = (".text.", ".rodata.", ".data.", ".bss.", ".literal.")
|
||||
|
||||
for line in map_text.splitlines():
|
||||
# Match section.symbol line: " .text.symbol_name"
|
||||
# Single space indent, starts with dot
|
||||
if len(line) > 2 and line[0] == " " and line[1] == ".":
|
||||
stripped = line.strip()
|
||||
for prefix in section_prefixes:
|
||||
if stripped.startswith(prefix):
|
||||
current_symbol = stripped[len(prefix) :]
|
||||
break
|
||||
else:
|
||||
current_symbol = None
|
||||
continue
|
||||
|
||||
# Match source attribution line: " 0xADDR 0xSIZE source_path"
|
||||
if current_symbol is None:
|
||||
continue
|
||||
|
||||
fields = line.split()
|
||||
# Skip compiler-generated local names (e.g., packet$19, buf$20)
|
||||
# that can collide across compilation units
|
||||
if (
|
||||
len(fields) >= 3
|
||||
and fields[0].startswith("0x")
|
||||
and fields[1].startswith("0x")
|
||||
and not _COMPILER_LOCAL_PATTERN.match(current_symbol)
|
||||
):
|
||||
source_path = fields[2]
|
||||
# Check if source path contains a known library directory
|
||||
for dir_key, lib_name in self._lib_hash_to_name.items():
|
||||
if dir_key in source_path:
|
||||
symbol_map[current_symbol] = lib_name
|
||||
break
|
||||
|
||||
current_symbol = None
|
||||
|
||||
return symbol_map or None
|
||||
|
||||
def _scan_libraries(self) -> None:
|
||||
"""Discover third-party libraries and build symbol mapping.
|
||||
|
||||
Scans both PlatformIO ``lib<hex>/`` directories (Arduino builds) and
|
||||
ESP-IDF ``managed_components/`` (IDF builds) to find library archives.
|
||||
|
||||
Uses the linker map file for authoritative symbol attribution when
|
||||
available, falling back to ``nm`` scanning with heuristic redirects.
|
||||
"""
|
||||
libraries: dict[str, list[Path]] = {}
|
||||
self._discover_pio_libraries(libraries, self._lib_hash_to_name)
|
||||
self._discover_idf_managed_components(libraries, self._lib_hash_to_name)
|
||||
|
||||
if not libraries:
|
||||
_LOGGER.debug("No third-party libraries found")
|
||||
return
|
||||
|
||||
_LOGGER.info(
|
||||
"Scanning %d libraries: %s",
|
||||
len(libraries),
|
||||
", ".join(sorted(libraries)),
|
||||
)
|
||||
|
||||
# Heuristic redirect catches local symbols (e.g., mdns_task_buffer$14)
|
||||
# that can't be safely added to the symbol map due to name collisions
|
||||
self._heuristic_to_lib = self._build_heuristic_to_lib_mapping(
|
||||
set(libraries.keys())
|
||||
)
|
||||
|
||||
# Try linker map file first (authoritative, includes local symbols)
|
||||
map_symbols = self._parse_map_file()
|
||||
if map_symbols is not None:
|
||||
self._lib_symbol_map = map_symbols
|
||||
_LOGGER.info(
|
||||
"Built library symbol map from linker map: %d symbols",
|
||||
len(self._lib_symbol_map),
|
||||
)
|
||||
return
|
||||
|
||||
# Fall back to nm scanning (global symbols only)
|
||||
self._lib_symbol_map = self._build_library_symbol_map(libraries)
|
||||
|
||||
_LOGGER.info(
|
||||
"Built library symbol map from nm: %d symbols from %d libraries",
|
||||
len(self._lib_symbol_map),
|
||||
len(libraries),
|
||||
)
|
||||
|
||||
def _find_object_files_dir(self) -> Path | None:
|
||||
"""Find the directory containing object files for this build.
|
||||
|
||||
@@ -741,38 +397,47 @@ class MemoryAnalyzer:
|
||||
return pioenvs_dir
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def _parse_nm_cswtch_output(
|
||||
output: str,
|
||||
base_dir: Path | None,
|
||||
cswtch_map: dict[str, list[tuple[str, int]]],
|
||||
) -> None:
|
||||
"""Parse nm output for CSWTCH symbols and add to cswtch_map.
|
||||
def _scan_cswtch_in_objects(
|
||||
self, obj_dir: Path
|
||||
) -> dict[str, list[tuple[str, int]]]:
|
||||
"""Scan object files for CSWTCH symbols using a single nm invocation.
|
||||
|
||||
Handles both ``.o`` files and ``.a`` archives.
|
||||
|
||||
nm output formats::
|
||||
|
||||
.o files: /path/file.o:hex_addr hex_size type name
|
||||
.a files: /path/lib.a:member.o:hex_addr hex_size type name
|
||||
|
||||
For ``.o`` files, paths are made relative to *base_dir* when possible.
|
||||
For ``.a`` archives (detected by ``:`` in the file portion), paths are
|
||||
formatted as ``archive_stem/member.o`` (e.g. ``liblwip2-536-feat/lwip-esp.o``).
|
||||
Uses ``nm --print-file-name -S`` on all ``.o`` files at once.
|
||||
Output format: ``/path/to/file.o:address size type name``
|
||||
|
||||
Args:
|
||||
output: Raw stdout from ``nm --print-file-name -S``.
|
||||
base_dir: Base directory for computing relative paths of ``.o`` files.
|
||||
Pass ``None`` when scanning archives outside the build tree.
|
||||
cswtch_map: Dict to populate, mapping ``"CSWTCH$N:size"`` to source list.
|
||||
obj_dir: Directory containing object files (.pioenvs/<env>/)
|
||||
|
||||
Returns:
|
||||
Dict mapping "CSWTCH$NNN:size" to list of (source_file, size) tuples.
|
||||
"""
|
||||
for line in output.splitlines():
|
||||
cswtch_map: dict[str, list[tuple[str, int]]] = defaultdict(list)
|
||||
|
||||
if not self.nm_path:
|
||||
return cswtch_map
|
||||
|
||||
# Find all .o files recursively, sorted for deterministic output
|
||||
obj_files = sorted(obj_dir.rglob("*.o"))
|
||||
if not obj_files:
|
||||
return cswtch_map
|
||||
|
||||
_LOGGER.debug("Scanning %d object files for CSWTCH symbols", len(obj_files))
|
||||
|
||||
# Single nm call with --print-file-name for all object files
|
||||
result = run_tool(
|
||||
[self.nm_path, "--print-file-name", "-S"] + [str(f) for f in obj_files],
|
||||
timeout=30,
|
||||
)
|
||||
if result is None or result.returncode != 0:
|
||||
return cswtch_map
|
||||
|
||||
for line in result.stdout.splitlines():
|
||||
if "CSWTCH$" not in line:
|
||||
continue
|
||||
|
||||
# Split on last ":" that precedes a hex address.
|
||||
# For .o: "filepath.o" : "hex_addr hex_size type name"
|
||||
# For .a: "filepath.a:member.o" : "hex_addr hex_size type name"
|
||||
# nm --print-file-name format: filepath:hex_addr hex_size type name
|
||||
# We split from the right: find the last colon followed by hex digits.
|
||||
parts_after_colon = line.rsplit(":", 1)
|
||||
if len(parts_after_colon) != 2:
|
||||
continue
|
||||
@@ -792,89 +457,16 @@ class MemoryAnalyzer:
|
||||
except ValueError:
|
||||
continue
|
||||
|
||||
# Determine readable source path
|
||||
# Use ".a:" to detect archive format (not bare ":" which matches
|
||||
# Windows drive letters like "C:\...\file.o").
|
||||
if ".a:" in file_path:
|
||||
# Archive format: "archive.a:member.o" → "archive_stem/member.o"
|
||||
archive_part, member = file_path.rsplit(":", 1)
|
||||
archive_name = Path(archive_part).stem
|
||||
rel_path = f"{archive_name}/{member}"
|
||||
elif base_dir is not None:
|
||||
try:
|
||||
rel_path = str(Path(file_path).relative_to(base_dir))
|
||||
except ValueError:
|
||||
rel_path = file_path
|
||||
else:
|
||||
# Get relative path from obj_dir for readability
|
||||
try:
|
||||
rel_path = str(Path(file_path).relative_to(obj_dir))
|
||||
except ValueError:
|
||||
rel_path = file_path
|
||||
|
||||
key = f"{sym_name}:{size}"
|
||||
cswtch_map[key].append((rel_path, size))
|
||||
|
||||
def _run_nm_cswtch_scan(
|
||||
self,
|
||||
files: list[Path],
|
||||
base_dir: Path | None,
|
||||
cswtch_map: dict[str, list[tuple[str, int]]],
|
||||
) -> None:
|
||||
"""Run nm on *files* and add any CSWTCH symbols to *cswtch_map*.
|
||||
|
||||
Args:
|
||||
files: Object (``.o``) or archive (``.a``) files to scan.
|
||||
base_dir: Base directory for relative path computation (see
|
||||
:meth:`_parse_nm_cswtch_output`).
|
||||
cswtch_map: Dict to populate with results.
|
||||
"""
|
||||
if not self.nm_path or not files:
|
||||
return
|
||||
|
||||
_LOGGER.debug("Scanning %d files for CSWTCH symbols", len(files))
|
||||
|
||||
result = run_tool(
|
||||
[self.nm_path, "--print-file-name", "-S"] + [str(f) for f in files],
|
||||
timeout=30,
|
||||
)
|
||||
if result is None or result.returncode != 0:
|
||||
_LOGGER.debug(
|
||||
"nm failed or timed out scanning %d files for CSWTCH symbols",
|
||||
len(files),
|
||||
)
|
||||
return
|
||||
|
||||
self._parse_nm_cswtch_output(result.stdout, base_dir, cswtch_map)
|
||||
|
||||
def _scan_cswtch_in_sdk_archives(
|
||||
self, cswtch_map: dict[str, list[tuple[str, int]]]
|
||||
) -> None:
|
||||
"""Scan SDK library archives (.a) for CSWTCH symbols.
|
||||
|
||||
Prebuilt SDK libraries (e.g. lwip, bearssl) are not compiled from source,
|
||||
so their CSWTCH symbols only exist inside ``.a`` archives. Results are
|
||||
merged into *cswtch_map* for keys not already found in ``.o`` files.
|
||||
|
||||
The same source file (e.g. ``lwip-esp.o``) often appears in multiple
|
||||
library variants (``liblwip2-536.a``, ``liblwip2-1460-feat.a``, etc.),
|
||||
so results are deduplicated by member name.
|
||||
"""
|
||||
sdk_dirs = self._find_sdk_library_dirs()
|
||||
if not sdk_dirs:
|
||||
return
|
||||
|
||||
sdk_archives = sorted(a for sdk_dir in sdk_dirs for a in sdk_dir.glob("*.a"))
|
||||
|
||||
sdk_map: dict[str, list[tuple[str, int]]] = defaultdict(list)
|
||||
self._run_nm_cswtch_scan(sdk_archives, None, sdk_map)
|
||||
|
||||
# Merge SDK results, deduplicating by member name.
|
||||
for key, sources in sdk_map.items():
|
||||
if key in cswtch_map:
|
||||
continue
|
||||
seen: dict[str, tuple[str, int]] = {}
|
||||
for path, sz in sources:
|
||||
member = Path(path).name
|
||||
if member not in seen:
|
||||
seen[member] = (path, sz)
|
||||
cswtch_map[key] = list(seen.values())
|
||||
return cswtch_map
|
||||
|
||||
def _source_file_to_component(self, source_file: str) -> str:
|
||||
"""Map a source object file path to its component name.
|
||||
@@ -903,21 +495,9 @@ class MemoryAnalyzer:
|
||||
if "esphome" in parts and "components" not in parts:
|
||||
return _COMPONENT_CORE
|
||||
|
||||
# Framework/library files - check for PlatformIO library hash dirs
|
||||
# e.g., lib65b/ESPAsyncTCP/... -> [lib]espasynctcp
|
||||
if parts and parts[0] in self._lib_hash_to_name:
|
||||
return f"{_COMPONENT_PREFIX_LIB}{self._lib_hash_to_name[parts[0]]}"
|
||||
|
||||
# ESP-IDF managed components: managed_components/espressif__mdns/... -> [lib]mdns
|
||||
if (
|
||||
len(parts) >= 2
|
||||
and parts[0] == "managed_components"
|
||||
and parts[1] in self._lib_hash_to_name
|
||||
):
|
||||
return f"{_COMPONENT_PREFIX_LIB}{self._lib_hash_to_name[parts[1]]}"
|
||||
|
||||
# Other framework/library files - return the first path component
|
||||
# e.g., FrameworkArduino/... -> FrameworkArduino
|
||||
# Framework/library files - return the first path component
|
||||
# e.g., lib65b/ESPAsyncTCP/... -> lib65b
|
||||
# FrameworkArduino/... -> FrameworkArduino
|
||||
return parts[0] if parts else source_file
|
||||
|
||||
def _analyze_cswtch_symbols(self) -> None:
|
||||
@@ -925,25 +505,17 @@ class MemoryAnalyzer:
|
||||
|
||||
CSWTCH symbols are compiler-generated lookup tables for switch statements.
|
||||
They are local symbols, so the same name can appear in different object files.
|
||||
This method scans .o files and SDK archives to attribute them to their
|
||||
source components.
|
||||
This method scans .o files to attribute them to their source components.
|
||||
"""
|
||||
obj_dir = self._find_object_files_dir()
|
||||
if obj_dir is None:
|
||||
_LOGGER.debug("No object files directory found, skipping CSWTCH analysis")
|
||||
return
|
||||
|
||||
# Scan build-dir object files for CSWTCH symbols
|
||||
cswtch_map: dict[str, list[tuple[str, int]]] = defaultdict(list)
|
||||
self._run_nm_cswtch_scan(sorted(obj_dir.rglob("*.o")), obj_dir, cswtch_map)
|
||||
|
||||
# Also scan SDK library archives (.a) for CSWTCH symbols.
|
||||
# Prebuilt SDK libraries (e.g. lwip, bearssl) are not compiled from source
|
||||
# so their symbols only exist inside .a archives, not as loose .o files.
|
||||
self._scan_cswtch_in_sdk_archives(cswtch_map)
|
||||
|
||||
# Scan object files for CSWTCH symbols
|
||||
cswtch_map = self._scan_cswtch_in_objects(obj_dir)
|
||||
if not cswtch_map:
|
||||
_LOGGER.debug("No CSWTCH symbols found in object files or SDK archives")
|
||||
_LOGGER.debug("No CSWTCH symbols found in object files")
|
||||
return
|
||||
|
||||
# Collect CSWTCH symbols from the ELF (already parsed in sections)
|
||||
|
||||
@@ -14,7 +14,6 @@ from . import (
|
||||
_COMPONENT_CORE,
|
||||
_COMPONENT_PREFIX_ESPHOME,
|
||||
_COMPONENT_PREFIX_EXTERNAL,
|
||||
_COMPONENT_PREFIX_LIB,
|
||||
RAM_SECTIONS,
|
||||
MemoryAnalyzer,
|
||||
)
|
||||
@@ -408,11 +407,6 @@ class MemoryAnalyzerCLI(MemoryAnalyzer):
|
||||
for name, mem in components
|
||||
if name.startswith(_COMPONENT_PREFIX_EXTERNAL)
|
||||
]
|
||||
library_components = [
|
||||
(name, mem)
|
||||
for name, mem in components
|
||||
if name.startswith(_COMPONENT_PREFIX_LIB)
|
||||
]
|
||||
|
||||
top_esphome_components = sorted(
|
||||
esphome_components, key=lambda x: x[1].flash_total, reverse=True
|
||||
@@ -423,11 +417,6 @@ class MemoryAnalyzerCLI(MemoryAnalyzer):
|
||||
external_components, key=lambda x: x[1].flash_total, reverse=True
|
||||
)
|
||||
|
||||
# Include all library components
|
||||
top_library_components = sorted(
|
||||
library_components, key=lambda x: x[1].flash_total, reverse=True
|
||||
)
|
||||
|
||||
# Check if API component exists and ensure it's included
|
||||
api_component = None
|
||||
for name, mem in components:
|
||||
@@ -446,11 +435,10 @@ class MemoryAnalyzerCLI(MemoryAnalyzer):
|
||||
if name in system_components_to_include
|
||||
]
|
||||
|
||||
# Combine all components to analyze: top ESPHome + all external + libraries + API if not already included + system components
|
||||
# Combine all components to analyze: top ESPHome + all external + API if not already included + system components
|
||||
components_to_analyze = (
|
||||
list(top_esphome_components)
|
||||
+ list(top_external_components)
|
||||
+ list(top_library_components)
|
||||
+ system_components
|
||||
)
|
||||
if api_component and api_component not in components_to_analyze:
|
||||
|
||||
@@ -1,699 +0,0 @@
|
||||
"""Config bundle creator and extractor for ESPHome.
|
||||
|
||||
A bundle is a self-contained .tar.gz archive containing a YAML config
|
||||
and every local file it depends on. Bundles can be created from a config
|
||||
and compiled directly: ``esphome compile my_device.esphomebundle.tar.gz``
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from enum import StrEnum
|
||||
import io
|
||||
import json
|
||||
import logging
|
||||
from pathlib import Path
|
||||
import re
|
||||
import shutil
|
||||
import tarfile
|
||||
from typing import Any
|
||||
|
||||
from esphome import const, yaml_util
|
||||
from esphome.const import (
|
||||
CONF_ESPHOME,
|
||||
CONF_EXTERNAL_COMPONENTS,
|
||||
CONF_INCLUDES,
|
||||
CONF_INCLUDES_C,
|
||||
CONF_PATH,
|
||||
CONF_SOURCE,
|
||||
CONF_TYPE,
|
||||
)
|
||||
from esphome.core import CORE, EsphomeError
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
BUNDLE_EXTENSION = ".esphomebundle.tar.gz"
|
||||
MANIFEST_FILENAME = "manifest.json"
|
||||
CURRENT_MANIFEST_VERSION = 1
|
||||
MAX_DECOMPRESSED_SIZE = 500 * 1024 * 1024 # 500 MB
|
||||
MAX_MANIFEST_SIZE = 1024 * 1024 # 1 MB
|
||||
|
||||
# Directories preserved across bundle extractions (build caches)
|
||||
_PRESERVE_DIRS = (".esphome", ".pioenvs", ".pio")
|
||||
_BUNDLE_STAGING_DIR = ".bundle_staging"
|
||||
|
||||
|
||||
class ManifestKey(StrEnum):
|
||||
"""Keys used in bundle manifest.json."""
|
||||
|
||||
MANIFEST_VERSION = "manifest_version"
|
||||
ESPHOME_VERSION = "esphome_version"
|
||||
CONFIG_FILENAME = "config_filename"
|
||||
FILES = "files"
|
||||
HAS_SECRETS = "has_secrets"
|
||||
|
||||
|
||||
# String prefixes that are never local file paths
|
||||
_NON_PATH_PREFIXES = ("http://", "https://", "ftp://", "mdi:", "<")
|
||||
|
||||
# File extensions recognized when resolving relative path strings.
|
||||
# A relative string with one of these extensions is resolved against the
|
||||
# config directory and included if the file exists.
|
||||
_KNOWN_FILE_EXTENSIONS = frozenset(
|
||||
{
|
||||
# Fonts
|
||||
".ttf",
|
||||
".otf",
|
||||
".woff",
|
||||
".woff2",
|
||||
".pcf",
|
||||
".bdf",
|
||||
# Images
|
||||
".png",
|
||||
".jpg",
|
||||
".jpeg",
|
||||
".bmp",
|
||||
".gif",
|
||||
".svg",
|
||||
".ico",
|
||||
".webp",
|
||||
# Certificates
|
||||
".pem",
|
||||
".crt",
|
||||
".key",
|
||||
".der",
|
||||
".p12",
|
||||
".pfx",
|
||||
# C/C++ includes
|
||||
".h",
|
||||
".hpp",
|
||||
".c",
|
||||
".cpp",
|
||||
".ino",
|
||||
# Web assets
|
||||
".css",
|
||||
".js",
|
||||
".html",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
# Matches !secret references in YAML text. This is intentionally a simple
|
||||
# regex scan rather than a YAML parse — it may match inside comments or
|
||||
# multi-line strings, which is the conservative direction (include more
|
||||
# secrets rather than fewer).
|
||||
_SECRET_RE = re.compile(r"!secret\s+(\S+)")
|
||||
|
||||
|
||||
def _find_used_secret_keys(yaml_files: list[Path]) -> set[str]:
|
||||
"""Scan YAML files for ``!secret <key>`` references."""
|
||||
keys: set[str] = set()
|
||||
for fpath in yaml_files:
|
||||
try:
|
||||
text = fpath.read_text(encoding="utf-8")
|
||||
except (OSError, UnicodeDecodeError):
|
||||
continue
|
||||
for match in _SECRET_RE.finditer(text):
|
||||
keys.add(match.group(1))
|
||||
return keys
|
||||
|
||||
|
||||
@dataclass
|
||||
class BundleFile:
|
||||
"""A file to include in the bundle."""
|
||||
|
||||
path: str # Relative path inside the archive
|
||||
source: Path # Absolute path on disk
|
||||
|
||||
|
||||
@dataclass
|
||||
class BundleResult:
|
||||
"""Result of creating a bundle."""
|
||||
|
||||
data: bytes
|
||||
manifest: dict[str, Any]
|
||||
files: list[BundleFile]
|
||||
|
||||
|
||||
@dataclass
|
||||
class BundleManifest:
|
||||
"""Parsed and validated bundle manifest."""
|
||||
|
||||
manifest_version: int
|
||||
esphome_version: str
|
||||
config_filename: str
|
||||
files: list[str]
|
||||
has_secrets: bool
|
||||
|
||||
|
||||
class ConfigBundleCreator:
|
||||
"""Creates a self-contained bundle from an ESPHome config."""
|
||||
|
||||
def __init__(self, config: dict[str, Any]) -> None:
|
||||
self._config = config
|
||||
self._config_dir = CORE.config_dir
|
||||
self._config_path = CORE.config_path
|
||||
self._files: list[BundleFile] = []
|
||||
self._seen_paths: set[Path] = set()
|
||||
self._secrets_paths: set[Path] = set()
|
||||
|
||||
def discover_files(self) -> list[BundleFile]:
|
||||
"""Discover all files needed for the bundle."""
|
||||
self._files = []
|
||||
self._seen_paths = set()
|
||||
self._secrets_paths = set()
|
||||
|
||||
# The main config file
|
||||
self._add_file(self._config_path)
|
||||
|
||||
# Phase 1: YAML includes (tracked during config loading)
|
||||
self._discover_yaml_includes()
|
||||
|
||||
# Phase 2: Component-referenced files from validated config
|
||||
self._discover_component_files()
|
||||
|
||||
return list(self._files)
|
||||
|
||||
def create_bundle(self) -> BundleResult:
|
||||
"""Create the bundle archive."""
|
||||
files = self.discover_files()
|
||||
|
||||
# Determine which secret keys are actually referenced by the
|
||||
# bundled YAML files so we only ship those, not the entire
|
||||
# secrets.yaml which may contain secrets for other devices.
|
||||
yaml_sources = [
|
||||
bf.source for bf in files if bf.source.suffix in (".yaml", ".yml")
|
||||
]
|
||||
used_secret_keys = _find_used_secret_keys(yaml_sources)
|
||||
filtered_secrets = self._build_filtered_secrets(used_secret_keys)
|
||||
|
||||
has_secrets = bool(filtered_secrets)
|
||||
if has_secrets:
|
||||
_LOGGER.warning(
|
||||
"Bundle contains secrets (e.g. Wi-Fi passwords). "
|
||||
"Do not share it with untrusted parties."
|
||||
)
|
||||
|
||||
manifest = self._build_manifest(files, has_secrets=has_secrets)
|
||||
|
||||
buf = io.BytesIO()
|
||||
with tarfile.open(fileobj=buf, mode="w:gz") as tar:
|
||||
# Add manifest first
|
||||
manifest_data = json.dumps(manifest, indent=2).encode("utf-8")
|
||||
_add_bytes_to_tar(tar, MANIFEST_FILENAME, manifest_data)
|
||||
|
||||
# Add filtered secrets files
|
||||
for rel_path, data in sorted(filtered_secrets.items()):
|
||||
_add_bytes_to_tar(tar, rel_path, data)
|
||||
|
||||
# Add files in sorted order for determinism, skipping secrets
|
||||
# files which were already added above with filtered content
|
||||
for bf in sorted(files, key=lambda f: f.path):
|
||||
if bf.source in self._secrets_paths:
|
||||
continue
|
||||
self._add_to_tar(tar, bf)
|
||||
|
||||
return BundleResult(data=buf.getvalue(), manifest=manifest, files=files)
|
||||
|
||||
def _add_file(self, abs_path: Path) -> bool:
|
||||
"""Add a file to the bundle. Returns False if already added."""
|
||||
abs_path = abs_path.resolve()
|
||||
if abs_path in self._seen_paths:
|
||||
return False
|
||||
if not abs_path.is_file():
|
||||
_LOGGER.warning("Bundle: skipping missing file %s", abs_path)
|
||||
return False
|
||||
|
||||
rel_path = self._relative_to_config_dir(abs_path)
|
||||
if rel_path is None:
|
||||
_LOGGER.warning(
|
||||
"Bundle: skipping file outside config directory: %s", abs_path
|
||||
)
|
||||
return False
|
||||
|
||||
self._seen_paths.add(abs_path)
|
||||
self._files.append(BundleFile(path=rel_path, source=abs_path))
|
||||
return True
|
||||
|
||||
def _add_directory(self, abs_path: Path) -> None:
|
||||
"""Recursively add all files in a directory."""
|
||||
abs_path = abs_path.resolve()
|
||||
if not abs_path.is_dir():
|
||||
_LOGGER.warning("Bundle: skipping missing directory %s", abs_path)
|
||||
return
|
||||
for child in sorted(abs_path.rglob("*")):
|
||||
if child.is_file() and "__pycache__" not in child.parts:
|
||||
self._add_file(child)
|
||||
|
||||
def _relative_to_config_dir(self, abs_path: Path) -> str | None:
|
||||
"""Get a path relative to the config directory. Returns None if outside.
|
||||
|
||||
Always uses forward slashes for consistency in tar archives.
|
||||
"""
|
||||
try:
|
||||
return abs_path.relative_to(self._config_dir).as_posix()
|
||||
except ValueError:
|
||||
return None
|
||||
|
||||
def _discover_yaml_includes(self) -> None:
|
||||
"""Discover YAML files loaded during config parsing.
|
||||
|
||||
We track files by wrapping _load_yaml_internal. The config has already
|
||||
been loaded at this point (bundle is a POST_CONFIG_ACTION), so we
|
||||
re-load just to discover the file list.
|
||||
|
||||
Secrets files are tracked separately so we can filter them to
|
||||
only include the keys this config actually references.
|
||||
"""
|
||||
with yaml_util.track_yaml_loads() as loaded_files:
|
||||
try:
|
||||
yaml_util.load_yaml(self._config_path)
|
||||
except EsphomeError:
|
||||
_LOGGER.debug(
|
||||
"Bundle: re-loading YAML for include discovery failed, "
|
||||
"proceeding with partial file list"
|
||||
)
|
||||
|
||||
for fpath in loaded_files:
|
||||
if fpath == self._config_path.resolve():
|
||||
continue # Already added as config
|
||||
if fpath.name in const.SECRETS_FILES:
|
||||
self._secrets_paths.add(fpath)
|
||||
self._add_file(fpath)
|
||||
|
||||
def _discover_component_files(self) -> None:
|
||||
"""Walk the validated config for file references.
|
||||
|
||||
Uses a generic recursive walk to find file paths instead of
|
||||
hardcoding per-component knowledge about config dict formats.
|
||||
After validation, components typically resolve paths to absolute
|
||||
using CORE.relative_config_path() or cv.file_(). Relative paths
|
||||
with known file extensions are also resolved and checked.
|
||||
|
||||
Core ESPHome concepts that use relative paths or directories
|
||||
are handled explicitly.
|
||||
"""
|
||||
config = self._config
|
||||
|
||||
# Generic walk: find all file paths in the validated config
|
||||
self._walk_config_for_files(config)
|
||||
|
||||
# --- Core ESPHome concepts needing explicit handling ---
|
||||
|
||||
# esphome.includes / includes_c - can be relative paths and directories
|
||||
esphome_conf = config.get(CONF_ESPHOME, {})
|
||||
for include_path in esphome_conf.get(CONF_INCLUDES, []):
|
||||
resolved = _resolve_include_path(include_path)
|
||||
if resolved is None:
|
||||
continue
|
||||
if resolved.is_dir():
|
||||
self._add_directory(resolved)
|
||||
else:
|
||||
self._add_file(resolved)
|
||||
for include_path in esphome_conf.get(CONF_INCLUDES_C, []):
|
||||
resolved = _resolve_include_path(include_path)
|
||||
if resolved is not None:
|
||||
self._add_file(resolved)
|
||||
|
||||
# external_components with source: local - directories
|
||||
for ext_conf in config.get(CONF_EXTERNAL_COMPONENTS, []):
|
||||
source = ext_conf.get(CONF_SOURCE, {})
|
||||
if not isinstance(source, dict):
|
||||
continue
|
||||
if source.get(CONF_TYPE) != "local":
|
||||
continue
|
||||
path = source.get(CONF_PATH)
|
||||
if not path:
|
||||
continue
|
||||
p = Path(path)
|
||||
if not p.is_absolute():
|
||||
p = CORE.relative_config_path(p)
|
||||
self._add_directory(p)
|
||||
|
||||
def _walk_config_for_files(self, obj: Any) -> None:
|
||||
"""Recursively walk the config dict looking for file path references."""
|
||||
if isinstance(obj, dict):
|
||||
for value in obj.values():
|
||||
self._walk_config_for_files(value)
|
||||
elif isinstance(obj, (list, tuple)):
|
||||
for item in obj:
|
||||
self._walk_config_for_files(item)
|
||||
elif isinstance(obj, Path):
|
||||
if obj.is_absolute() and obj.is_file():
|
||||
self._add_file(obj)
|
||||
elif isinstance(obj, str):
|
||||
self._check_string_path(obj)
|
||||
|
||||
def _check_string_path(self, value: str) -> None:
|
||||
"""Check if a string value is a local file reference."""
|
||||
# Fast exits for strings that cannot be file paths
|
||||
if len(value) < 2 or "\n" in value:
|
||||
return
|
||||
if value.startswith(_NON_PATH_PREFIXES):
|
||||
return
|
||||
# File paths must contain a path separator or a dot (for extension)
|
||||
if "/" not in value and "\\" not in value and "." not in value:
|
||||
return
|
||||
|
||||
p = Path(value)
|
||||
|
||||
# Absolute path - check if it points to an existing file
|
||||
if p.is_absolute():
|
||||
if p.is_file():
|
||||
self._add_file(p)
|
||||
return
|
||||
|
||||
# Relative path with a known file extension - likely a component
|
||||
# validator that forgot to resolve to absolute via cv.file_() or
|
||||
# CORE.relative_config_path(). Warn and try to resolve.
|
||||
if p.suffix.lower() in _KNOWN_FILE_EXTENSIONS:
|
||||
_LOGGER.warning(
|
||||
"Bundle: non-absolute path in validated config: %s "
|
||||
"(component validator should return absolute paths)",
|
||||
value,
|
||||
)
|
||||
resolved = CORE.relative_config_path(p)
|
||||
if resolved.is_file():
|
||||
self._add_file(resolved)
|
||||
|
||||
def _build_filtered_secrets(self, used_keys: set[str]) -> dict[str, bytes]:
|
||||
"""Build filtered secrets files containing only the referenced keys.
|
||||
|
||||
Returns a dict mapping relative archive path to YAML bytes.
|
||||
"""
|
||||
if not used_keys or not self._secrets_paths:
|
||||
return {}
|
||||
|
||||
result: dict[str, bytes] = {}
|
||||
for secrets_path in self._secrets_paths:
|
||||
rel_path = self._relative_to_config_dir(secrets_path)
|
||||
if rel_path is None:
|
||||
continue
|
||||
try:
|
||||
all_secrets = yaml_util.load_yaml(secrets_path, clear_secrets=False)
|
||||
except EsphomeError:
|
||||
_LOGGER.warning("Bundle: failed to load secrets file %s", secrets_path)
|
||||
continue
|
||||
if not isinstance(all_secrets, dict):
|
||||
continue
|
||||
filtered = {k: v for k, v in all_secrets.items() if k in used_keys}
|
||||
if filtered:
|
||||
data = yaml_util.dump(filtered, show_secrets=True).encode("utf-8")
|
||||
result[rel_path] = data
|
||||
return result
|
||||
|
||||
def _build_manifest(
|
||||
self, files: list[BundleFile], *, has_secrets: bool
|
||||
) -> dict[str, Any]:
|
||||
"""Build the manifest.json content."""
|
||||
return {
|
||||
ManifestKey.MANIFEST_VERSION: CURRENT_MANIFEST_VERSION,
|
||||
ManifestKey.ESPHOME_VERSION: const.__version__,
|
||||
ManifestKey.CONFIG_FILENAME: self._config_path.name,
|
||||
ManifestKey.FILES: [f.path for f in files],
|
||||
ManifestKey.HAS_SECRETS: has_secrets,
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _add_to_tar(tar: tarfile.TarFile, bf: BundleFile) -> None:
|
||||
"""Add a BundleFile to the tar archive with deterministic metadata."""
|
||||
with open(bf.source, "rb") as f:
|
||||
_add_bytes_to_tar(tar, bf.path, f.read())
|
||||
|
||||
|
||||
def extract_bundle(
|
||||
bundle_path: Path,
|
||||
target_dir: Path | None = None,
|
||||
) -> Path:
|
||||
"""Extract a bundle archive and return the path to the config YAML.
|
||||
|
||||
Sanity checks reject path traversal, symlinks, absolute paths, and
|
||||
oversized archives to prevent accidental file overwrites or extraction
|
||||
outside the target directory. These are **not** a security boundary —
|
||||
bundles are assumed to come from the user's own machine or a trusted
|
||||
build pipeline.
|
||||
|
||||
Args:
|
||||
bundle_path: Path to the .tar.gz bundle file.
|
||||
target_dir: Directory to extract into. If None, extracts next to
|
||||
the bundle file in a directory named after it.
|
||||
|
||||
Returns:
|
||||
Absolute path to the extracted config YAML file.
|
||||
|
||||
Raises:
|
||||
EsphomeError: If the bundle is invalid or extraction fails.
|
||||
"""
|
||||
|
||||
bundle_path = bundle_path.resolve()
|
||||
if not bundle_path.is_file():
|
||||
raise EsphomeError(f"Bundle file not found: {bundle_path}")
|
||||
|
||||
if target_dir is None:
|
||||
target_dir = _default_target_dir(bundle_path)
|
||||
|
||||
target_dir = target_dir.resolve()
|
||||
target_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Read and validate the archive
|
||||
try:
|
||||
with tarfile.open(bundle_path, "r:gz") as tar:
|
||||
manifest = _read_manifest_from_tar(tar)
|
||||
_validate_tar_members(tar, target_dir)
|
||||
tar.extractall(path=target_dir, filter="data")
|
||||
except tarfile.TarError as err:
|
||||
raise EsphomeError(f"Failed to extract bundle: {err}") from err
|
||||
|
||||
config_filename = manifest[ManifestKey.CONFIG_FILENAME]
|
||||
config_path = target_dir / config_filename
|
||||
if not config_path.is_file():
|
||||
raise EsphomeError(
|
||||
f"Bundle manifest references config '{config_filename}' "
|
||||
f"but it was not found in the archive"
|
||||
)
|
||||
|
||||
return config_path
|
||||
|
||||
|
||||
def read_bundle_manifest(bundle_path: Path) -> BundleManifest:
|
||||
"""Read and validate the manifest from a bundle without full extraction.
|
||||
|
||||
Args:
|
||||
bundle_path: Path to the .tar.gz bundle file.
|
||||
|
||||
Returns:
|
||||
Parsed BundleManifest.
|
||||
|
||||
Raises:
|
||||
EsphomeError: If the manifest is missing, invalid, or version unsupported.
|
||||
"""
|
||||
|
||||
try:
|
||||
with tarfile.open(bundle_path, "r:gz") as tar:
|
||||
manifest = _read_manifest_from_tar(tar)
|
||||
except tarfile.TarError as err:
|
||||
raise EsphomeError(f"Failed to read bundle: {err}") from err
|
||||
|
||||
return BundleManifest(
|
||||
manifest_version=manifest[ManifestKey.MANIFEST_VERSION],
|
||||
esphome_version=manifest.get(ManifestKey.ESPHOME_VERSION, "unknown"),
|
||||
config_filename=manifest[ManifestKey.CONFIG_FILENAME],
|
||||
files=manifest.get(ManifestKey.FILES, []),
|
||||
has_secrets=manifest.get(ManifestKey.HAS_SECRETS, False),
|
||||
)
|
||||
|
||||
|
||||
def _read_manifest_from_tar(tar: tarfile.TarFile) -> dict[str, Any]:
|
||||
"""Read and validate manifest.json from an open tar archive."""
|
||||
|
||||
try:
|
||||
member = tar.getmember(MANIFEST_FILENAME)
|
||||
except KeyError:
|
||||
raise EsphomeError("Invalid bundle: missing manifest.json") from None
|
||||
|
||||
f = tar.extractfile(member)
|
||||
if f is None:
|
||||
raise EsphomeError("Invalid bundle: manifest.json is not a regular file")
|
||||
|
||||
if member.size > MAX_MANIFEST_SIZE:
|
||||
raise EsphomeError(
|
||||
f"Invalid bundle: manifest.json too large "
|
||||
f"({member.size} bytes, max {MAX_MANIFEST_SIZE})"
|
||||
)
|
||||
|
||||
try:
|
||||
manifest = json.loads(f.read())
|
||||
except (json.JSONDecodeError, UnicodeDecodeError) as err:
|
||||
raise EsphomeError(f"Invalid bundle: malformed manifest.json: {err}") from err
|
||||
|
||||
# Version check
|
||||
version = manifest.get(ManifestKey.MANIFEST_VERSION)
|
||||
if version is None:
|
||||
raise EsphomeError("Invalid bundle: manifest.json missing 'manifest_version'")
|
||||
if not isinstance(version, int) or version < 1:
|
||||
raise EsphomeError(
|
||||
f"Invalid bundle: manifest_version must be a positive integer, got {version!r}"
|
||||
)
|
||||
if version > CURRENT_MANIFEST_VERSION:
|
||||
raise EsphomeError(
|
||||
f"Bundle manifest version {version} is newer than this ESPHome "
|
||||
f"version supports (max {CURRENT_MANIFEST_VERSION}). "
|
||||
f"Please upgrade ESPHome to compile this bundle."
|
||||
)
|
||||
|
||||
# Required fields
|
||||
if ManifestKey.CONFIG_FILENAME not in manifest:
|
||||
raise EsphomeError("Invalid bundle: manifest.json missing 'config_filename'")
|
||||
|
||||
return manifest
|
||||
|
||||
|
||||
def _validate_tar_members(tar: tarfile.TarFile, target_dir: Path) -> None:
|
||||
"""Sanity-check tar members to prevent mistakes and accidental overwrites.
|
||||
|
||||
This is not a security boundary — bundles are created locally or come
|
||||
from a trusted build pipeline. The checks catch malformed archives
|
||||
and common mistakes (stray absolute paths, ``..`` components) that
|
||||
could silently overwrite unrelated files.
|
||||
"""
|
||||
|
||||
total_size = 0
|
||||
for member in tar.getmembers():
|
||||
# Reject absolute paths (Unix and Windows)
|
||||
if member.name.startswith(("/", "\\")):
|
||||
raise EsphomeError(
|
||||
f"Invalid bundle: absolute path in archive: {member.name}"
|
||||
)
|
||||
|
||||
# Reject path traversal (split on both / and \ for cross-platform)
|
||||
parts = re.split(r"[/\\]", member.name)
|
||||
if ".." in parts:
|
||||
raise EsphomeError(
|
||||
f"Invalid bundle: path traversal in archive: {member.name}"
|
||||
)
|
||||
|
||||
# Reject symlinks
|
||||
if member.issym() or member.islnk():
|
||||
raise EsphomeError(f"Invalid bundle: symlink in archive: {member.name}")
|
||||
|
||||
# Ensure extraction stays within target_dir
|
||||
target_path = (target_dir / member.name).resolve()
|
||||
if not target_path.is_relative_to(target_dir):
|
||||
raise EsphomeError(
|
||||
f"Invalid bundle: file would extract outside target: {member.name}"
|
||||
)
|
||||
|
||||
# Track total decompressed size
|
||||
total_size += member.size
|
||||
if total_size > MAX_DECOMPRESSED_SIZE:
|
||||
raise EsphomeError(
|
||||
f"Invalid bundle: decompressed size exceeds "
|
||||
f"{MAX_DECOMPRESSED_SIZE // (1024 * 1024)}MB limit"
|
||||
)
|
||||
|
||||
|
||||
def is_bundle_path(path: Path) -> bool:
|
||||
"""Check if a path looks like a bundle file."""
|
||||
return path.name.lower().endswith(BUNDLE_EXTENSION)
|
||||
|
||||
|
||||
def _add_bytes_to_tar(tar: tarfile.TarFile, name: str, data: bytes) -> None:
|
||||
"""Add in-memory bytes to a tar archive with deterministic metadata."""
|
||||
info = tarfile.TarInfo(name=name)
|
||||
info.size = len(data)
|
||||
info.mtime = 0
|
||||
info.uid = 0
|
||||
info.gid = 0
|
||||
info.mode = 0o644
|
||||
tar.addfile(info, io.BytesIO(data))
|
||||
|
||||
|
||||
def _resolve_include_path(include_path: Any) -> Path | None:
|
||||
"""Resolve an include path to absolute, skipping system includes."""
|
||||
if isinstance(include_path, str) and include_path.startswith("<"):
|
||||
return None # System include, not a local file
|
||||
p = Path(include_path)
|
||||
if not p.is_absolute():
|
||||
p = CORE.relative_config_path(p)
|
||||
return p
|
||||
|
||||
|
||||
def _default_target_dir(bundle_path: Path) -> Path:
|
||||
"""Compute the default extraction directory for a bundle."""
|
||||
name = bundle_path.name
|
||||
if name.lower().endswith(BUNDLE_EXTENSION):
|
||||
name = name[: -len(BUNDLE_EXTENSION)]
|
||||
return bundle_path.parent / name
|
||||
|
||||
|
||||
def _restore_preserved_dirs(preserved: dict[str, Path], target_dir: Path) -> None:
|
||||
"""Move preserved build cache directories back into target_dir.
|
||||
|
||||
If the bundle contained entries under a preserved directory name,
|
||||
the extracted copy is removed so the original cache always wins.
|
||||
"""
|
||||
for dirname, src in preserved.items():
|
||||
dst = target_dir / dirname
|
||||
if dst.exists():
|
||||
shutil.rmtree(dst)
|
||||
shutil.move(str(src), str(dst))
|
||||
|
||||
|
||||
def prepare_bundle_for_compile(
|
||||
bundle_path: Path,
|
||||
target_dir: Path | None = None,
|
||||
) -> Path:
|
||||
"""Extract a bundle for compilation, preserving build caches.
|
||||
|
||||
Unlike extract_bundle(), this preserves .esphome/ and .pioenvs/
|
||||
directories in the target if they already exist (for incremental builds).
|
||||
|
||||
Args:
|
||||
bundle_path: Path to the .tar.gz bundle file.
|
||||
target_dir: Directory to extract into. Must be specified for
|
||||
build server use.
|
||||
|
||||
Returns:
|
||||
Absolute path to the extracted config YAML file.
|
||||
"""
|
||||
|
||||
bundle_path = bundle_path.resolve()
|
||||
if not bundle_path.is_file():
|
||||
raise EsphomeError(f"Bundle file not found: {bundle_path}")
|
||||
|
||||
if target_dir is None:
|
||||
target_dir = _default_target_dir(bundle_path)
|
||||
|
||||
target_dir = target_dir.resolve()
|
||||
target_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
preserved: dict[str, Path] = {}
|
||||
|
||||
# Temporarily move preserved dirs out of the way
|
||||
staging = target_dir / _BUNDLE_STAGING_DIR
|
||||
for dirname in _PRESERVE_DIRS:
|
||||
src = target_dir / dirname
|
||||
if src.is_dir():
|
||||
dst = staging / dirname
|
||||
dst.parent.mkdir(parents=True, exist_ok=True)
|
||||
shutil.move(str(src), str(dst))
|
||||
preserved[dirname] = dst
|
||||
|
||||
try:
|
||||
# Clean non-preserved content and extract fresh
|
||||
for item in target_dir.iterdir():
|
||||
if item.name == _BUNDLE_STAGING_DIR:
|
||||
continue
|
||||
if item.is_dir():
|
||||
shutil.rmtree(item)
|
||||
else:
|
||||
item.unlink()
|
||||
|
||||
config_path = extract_bundle(bundle_path, target_dir)
|
||||
finally:
|
||||
# Restore preserved dirs (idempotent) and clean staging
|
||||
_restore_preserved_dirs(preserved, target_dir)
|
||||
if staging.is_dir():
|
||||
shutil.rmtree(staging)
|
||||
|
||||
return config_path
|
||||
@@ -87,7 +87,6 @@ from esphome.cpp_types import ( # noqa: F401
|
||||
size_t,
|
||||
std_ns,
|
||||
std_shared_ptr,
|
||||
std_span,
|
||||
std_string,
|
||||
std_string_ref,
|
||||
std_vector,
|
||||
|
||||
@@ -283,7 +283,7 @@ void APIConnection::loop() {
|
||||
#endif
|
||||
}
|
||||
|
||||
bool APIConnection::send_disconnect_response_() {
|
||||
bool APIConnection::send_disconnect_response(const DisconnectRequest &msg) {
|
||||
// remote initiated disconnect_client
|
||||
// don't close yet, we still need to send the disconnect response
|
||||
// close will happen on next loop
|
||||
@@ -292,7 +292,7 @@ bool APIConnection::send_disconnect_response_() {
|
||||
DisconnectResponse resp;
|
||||
return this->send_message(resp, DisconnectResponse::MESSAGE_TYPE);
|
||||
}
|
||||
void APIConnection::on_disconnect_response() {
|
||||
void APIConnection::on_disconnect_response(const DisconnectResponse &value) {
|
||||
this->helper_->close();
|
||||
this->flags_.remove = true;
|
||||
}
|
||||
@@ -406,7 +406,7 @@ uint16_t APIConnection::try_send_cover_info(EntityBase *entity, APIConnection *c
|
||||
msg.device_class = cover->get_device_class_ref();
|
||||
return fill_and_encode_entity_info(cover, msg, ListEntitiesCoverResponse::MESSAGE_TYPE, conn, remaining_size);
|
||||
}
|
||||
void APIConnection::on_cover_command_request(const CoverCommandRequest &msg) {
|
||||
void APIConnection::cover_command(const CoverCommandRequest &msg) {
|
||||
ENTITY_COMMAND_MAKE_CALL(cover::Cover, cover, cover)
|
||||
if (msg.has_position)
|
||||
call.set_position(msg.position);
|
||||
@@ -449,7 +449,7 @@ uint16_t APIConnection::try_send_fan_info(EntityBase *entity, APIConnection *con
|
||||
msg.supported_preset_modes = &traits.supported_preset_modes();
|
||||
return fill_and_encode_entity_info(fan, msg, ListEntitiesFanResponse::MESSAGE_TYPE, conn, remaining_size);
|
||||
}
|
||||
void APIConnection::on_fan_command_request(const FanCommandRequest &msg) {
|
||||
void APIConnection::fan_command(const FanCommandRequest &msg) {
|
||||
ENTITY_COMMAND_MAKE_CALL(fan::Fan, fan, fan)
|
||||
if (msg.has_state)
|
||||
call.set_state(msg.state);
|
||||
@@ -517,7 +517,7 @@ uint16_t APIConnection::try_send_light_info(EntityBase *entity, APIConnection *c
|
||||
msg.effects = &effects_list;
|
||||
return fill_and_encode_entity_info(light, msg, ListEntitiesLightResponse::MESSAGE_TYPE, conn, remaining_size);
|
||||
}
|
||||
void APIConnection::on_light_command_request(const LightCommandRequest &msg) {
|
||||
void APIConnection::light_command(const LightCommandRequest &msg) {
|
||||
ENTITY_COMMAND_MAKE_CALL(light::LightState, light, light)
|
||||
if (msg.has_state)
|
||||
call.set_state(msg.state);
|
||||
@@ -594,7 +594,7 @@ uint16_t APIConnection::try_send_switch_info(EntityBase *entity, APIConnection *
|
||||
msg.device_class = a_switch->get_device_class_ref();
|
||||
return fill_and_encode_entity_info(a_switch, msg, ListEntitiesSwitchResponse::MESSAGE_TYPE, conn, remaining_size);
|
||||
}
|
||||
void APIConnection::on_switch_command_request(const SwitchCommandRequest &msg) {
|
||||
void APIConnection::switch_command(const SwitchCommandRequest &msg) {
|
||||
ENTITY_COMMAND_GET(switch_::Switch, a_switch, switch)
|
||||
|
||||
if (msg.state) {
|
||||
@@ -692,7 +692,7 @@ uint16_t APIConnection::try_send_climate_info(EntityBase *entity, APIConnection
|
||||
msg.supported_swing_modes = &traits.get_supported_swing_modes();
|
||||
return fill_and_encode_entity_info(climate, msg, ListEntitiesClimateResponse::MESSAGE_TYPE, conn, remaining_size);
|
||||
}
|
||||
void APIConnection::on_climate_command_request(const ClimateCommandRequest &msg) {
|
||||
void APIConnection::climate_command(const ClimateCommandRequest &msg) {
|
||||
ENTITY_COMMAND_MAKE_CALL(climate::Climate, climate, climate)
|
||||
if (msg.has_mode)
|
||||
call.set_mode(static_cast<climate::ClimateMode>(msg.mode));
|
||||
@@ -742,7 +742,7 @@ uint16_t APIConnection::try_send_number_info(EntityBase *entity, APIConnection *
|
||||
msg.step = number->traits.get_step();
|
||||
return fill_and_encode_entity_info(number, msg, ListEntitiesNumberResponse::MESSAGE_TYPE, conn, remaining_size);
|
||||
}
|
||||
void APIConnection::on_number_command_request(const NumberCommandRequest &msg) {
|
||||
void APIConnection::number_command(const NumberCommandRequest &msg) {
|
||||
ENTITY_COMMAND_MAKE_CALL(number::Number, number, number)
|
||||
call.set_value(msg.state);
|
||||
call.perform();
|
||||
@@ -767,7 +767,7 @@ uint16_t APIConnection::try_send_date_info(EntityBase *entity, APIConnection *co
|
||||
ListEntitiesDateResponse msg;
|
||||
return fill_and_encode_entity_info(date, msg, ListEntitiesDateResponse::MESSAGE_TYPE, conn, remaining_size);
|
||||
}
|
||||
void APIConnection::on_date_command_request(const DateCommandRequest &msg) {
|
||||
void APIConnection::date_command(const DateCommandRequest &msg) {
|
||||
ENTITY_COMMAND_MAKE_CALL(datetime::DateEntity, date, date)
|
||||
call.set_date(msg.year, msg.month, msg.day);
|
||||
call.perform();
|
||||
@@ -792,7 +792,7 @@ uint16_t APIConnection::try_send_time_info(EntityBase *entity, APIConnection *co
|
||||
ListEntitiesTimeResponse msg;
|
||||
return fill_and_encode_entity_info(time, msg, ListEntitiesTimeResponse::MESSAGE_TYPE, conn, remaining_size);
|
||||
}
|
||||
void APIConnection::on_time_command_request(const TimeCommandRequest &msg) {
|
||||
void APIConnection::time_command(const TimeCommandRequest &msg) {
|
||||
ENTITY_COMMAND_MAKE_CALL(datetime::TimeEntity, time, time)
|
||||
call.set_time(msg.hour, msg.minute, msg.second);
|
||||
call.perform();
|
||||
@@ -819,7 +819,7 @@ uint16_t APIConnection::try_send_datetime_info(EntityBase *entity, APIConnection
|
||||
ListEntitiesDateTimeResponse msg;
|
||||
return fill_and_encode_entity_info(datetime, msg, ListEntitiesDateTimeResponse::MESSAGE_TYPE, conn, remaining_size);
|
||||
}
|
||||
void APIConnection::on_date_time_command_request(const DateTimeCommandRequest &msg) {
|
||||
void APIConnection::datetime_command(const DateTimeCommandRequest &msg) {
|
||||
ENTITY_COMMAND_MAKE_CALL(datetime::DateTimeEntity, datetime, datetime)
|
||||
call.set_datetime(msg.epoch_seconds);
|
||||
call.perform();
|
||||
@@ -848,7 +848,7 @@ uint16_t APIConnection::try_send_text_info(EntityBase *entity, APIConnection *co
|
||||
msg.pattern = text->traits.get_pattern_ref();
|
||||
return fill_and_encode_entity_info(text, msg, ListEntitiesTextResponse::MESSAGE_TYPE, conn, remaining_size);
|
||||
}
|
||||
void APIConnection::on_text_command_request(const TextCommandRequest &msg) {
|
||||
void APIConnection::text_command(const TextCommandRequest &msg) {
|
||||
ENTITY_COMMAND_MAKE_CALL(text::Text, text, text)
|
||||
call.set_value(msg.state);
|
||||
call.perform();
|
||||
@@ -874,7 +874,7 @@ uint16_t APIConnection::try_send_select_info(EntityBase *entity, APIConnection *
|
||||
msg.options = &select->traits.get_options();
|
||||
return fill_and_encode_entity_info(select, msg, ListEntitiesSelectResponse::MESSAGE_TYPE, conn, remaining_size);
|
||||
}
|
||||
void APIConnection::on_select_command_request(const SelectCommandRequest &msg) {
|
||||
void APIConnection::select_command(const SelectCommandRequest &msg) {
|
||||
ENTITY_COMMAND_MAKE_CALL(select::Select, select, select)
|
||||
call.set_option(msg.state.c_str(), msg.state.size());
|
||||
call.perform();
|
||||
@@ -888,7 +888,7 @@ uint16_t APIConnection::try_send_button_info(EntityBase *entity, APIConnection *
|
||||
msg.device_class = button->get_device_class_ref();
|
||||
return fill_and_encode_entity_info(button, msg, ListEntitiesButtonResponse::MESSAGE_TYPE, conn, remaining_size);
|
||||
}
|
||||
void esphome::api::APIConnection::on_button_command_request(const ButtonCommandRequest &msg) {
|
||||
void esphome::api::APIConnection::button_command(const ButtonCommandRequest &msg) {
|
||||
ENTITY_COMMAND_GET(button::Button, button, button)
|
||||
button->press();
|
||||
}
|
||||
@@ -914,7 +914,7 @@ uint16_t APIConnection::try_send_lock_info(EntityBase *entity, APIConnection *co
|
||||
msg.requires_code = a_lock->traits.get_requires_code();
|
||||
return fill_and_encode_entity_info(a_lock, msg, ListEntitiesLockResponse::MESSAGE_TYPE, conn, remaining_size);
|
||||
}
|
||||
void APIConnection::on_lock_command_request(const LockCommandRequest &msg) {
|
||||
void APIConnection::lock_command(const LockCommandRequest &msg) {
|
||||
ENTITY_COMMAND_GET(lock::Lock, a_lock, lock)
|
||||
|
||||
switch (msg.command) {
|
||||
@@ -952,7 +952,7 @@ uint16_t APIConnection::try_send_valve_info(EntityBase *entity, APIConnection *c
|
||||
msg.supports_stop = traits.get_supports_stop();
|
||||
return fill_and_encode_entity_info(valve, msg, ListEntitiesValveResponse::MESSAGE_TYPE, conn, remaining_size);
|
||||
}
|
||||
void APIConnection::on_valve_command_request(const ValveCommandRequest &msg) {
|
||||
void APIConnection::valve_command(const ValveCommandRequest &msg) {
|
||||
ENTITY_COMMAND_MAKE_CALL(valve::Valve, valve, valve)
|
||||
if (msg.has_position)
|
||||
call.set_position(msg.position);
|
||||
@@ -996,7 +996,7 @@ uint16_t APIConnection::try_send_media_player_info(EntityBase *entity, APIConnec
|
||||
return fill_and_encode_entity_info(media_player, msg, ListEntitiesMediaPlayerResponse::MESSAGE_TYPE, conn,
|
||||
remaining_size);
|
||||
}
|
||||
void APIConnection::on_media_player_command_request(const MediaPlayerCommandRequest &msg) {
|
||||
void APIConnection::media_player_command(const MediaPlayerCommandRequest &msg) {
|
||||
ENTITY_COMMAND_MAKE_CALL(media_player::MediaPlayer, media_player, media_player)
|
||||
if (msg.has_command) {
|
||||
call.set_command(static_cast<media_player::MediaPlayerCommand>(msg.command));
|
||||
@@ -1063,7 +1063,7 @@ uint16_t APIConnection::try_send_camera_info(EntityBase *entity, APIConnection *
|
||||
ListEntitiesCameraResponse msg;
|
||||
return fill_and_encode_entity_info(camera, msg, ListEntitiesCameraResponse::MESSAGE_TYPE, conn, remaining_size);
|
||||
}
|
||||
void APIConnection::on_camera_image_request(const CameraImageRequest &msg) {
|
||||
void APIConnection::camera_image(const CameraImageRequest &msg) {
|
||||
if (camera::Camera::instance() == nullptr)
|
||||
return;
|
||||
|
||||
@@ -1092,47 +1092,42 @@ void APIConnection::on_get_time_response(const GetTimeResponse &value) {
|
||||
#endif
|
||||
|
||||
#ifdef USE_BLUETOOTH_PROXY
|
||||
void APIConnection::on_subscribe_bluetooth_le_advertisements_request(
|
||||
const SubscribeBluetoothLEAdvertisementsRequest &msg) {
|
||||
void APIConnection::subscribe_bluetooth_le_advertisements(const SubscribeBluetoothLEAdvertisementsRequest &msg) {
|
||||
bluetooth_proxy::global_bluetooth_proxy->subscribe_api_connection(this, msg.flags);
|
||||
}
|
||||
void APIConnection::on_unsubscribe_bluetooth_le_advertisements_request() {
|
||||
void APIConnection::unsubscribe_bluetooth_le_advertisements(const UnsubscribeBluetoothLEAdvertisementsRequest &msg) {
|
||||
bluetooth_proxy::global_bluetooth_proxy->unsubscribe_api_connection(this);
|
||||
}
|
||||
void APIConnection::on_bluetooth_device_request(const BluetoothDeviceRequest &msg) {
|
||||
void APIConnection::bluetooth_device_request(const BluetoothDeviceRequest &msg) {
|
||||
bluetooth_proxy::global_bluetooth_proxy->bluetooth_device_request(msg);
|
||||
}
|
||||
void APIConnection::on_bluetooth_gatt_read_request(const BluetoothGATTReadRequest &msg) {
|
||||
void APIConnection::bluetooth_gatt_read(const BluetoothGATTReadRequest &msg) {
|
||||
bluetooth_proxy::global_bluetooth_proxy->bluetooth_gatt_read(msg);
|
||||
}
|
||||
void APIConnection::on_bluetooth_gatt_write_request(const BluetoothGATTWriteRequest &msg) {
|
||||
void APIConnection::bluetooth_gatt_write(const BluetoothGATTWriteRequest &msg) {
|
||||
bluetooth_proxy::global_bluetooth_proxy->bluetooth_gatt_write(msg);
|
||||
}
|
||||
void APIConnection::on_bluetooth_gatt_read_descriptor_request(const BluetoothGATTReadDescriptorRequest &msg) {
|
||||
void APIConnection::bluetooth_gatt_read_descriptor(const BluetoothGATTReadDescriptorRequest &msg) {
|
||||
bluetooth_proxy::global_bluetooth_proxy->bluetooth_gatt_read_descriptor(msg);
|
||||
}
|
||||
void APIConnection::on_bluetooth_gatt_write_descriptor_request(const BluetoothGATTWriteDescriptorRequest &msg) {
|
||||
void APIConnection::bluetooth_gatt_write_descriptor(const BluetoothGATTWriteDescriptorRequest &msg) {
|
||||
bluetooth_proxy::global_bluetooth_proxy->bluetooth_gatt_write_descriptor(msg);
|
||||
}
|
||||
void APIConnection::on_bluetooth_gatt_get_services_request(const BluetoothGATTGetServicesRequest &msg) {
|
||||
void APIConnection::bluetooth_gatt_get_services(const BluetoothGATTGetServicesRequest &msg) {
|
||||
bluetooth_proxy::global_bluetooth_proxy->bluetooth_gatt_send_services(msg);
|
||||
}
|
||||
|
||||
void APIConnection::on_bluetooth_gatt_notify_request(const BluetoothGATTNotifyRequest &msg) {
|
||||
void APIConnection::bluetooth_gatt_notify(const BluetoothGATTNotifyRequest &msg) {
|
||||
bluetooth_proxy::global_bluetooth_proxy->bluetooth_gatt_notify(msg);
|
||||
}
|
||||
|
||||
bool APIConnection::send_subscribe_bluetooth_connections_free_response_() {
|
||||
bool APIConnection::send_subscribe_bluetooth_connections_free_response(
|
||||
const SubscribeBluetoothConnectionsFreeRequest &msg) {
|
||||
bluetooth_proxy::global_bluetooth_proxy->send_connections_free(this);
|
||||
return true;
|
||||
}
|
||||
void APIConnection::on_subscribe_bluetooth_connections_free_request() {
|
||||
if (!this->send_subscribe_bluetooth_connections_free_response_()) {
|
||||
this->on_fatal_error();
|
||||
}
|
||||
}
|
||||
|
||||
void APIConnection::on_bluetooth_scanner_set_mode_request(const BluetoothScannerSetModeRequest &msg) {
|
||||
void APIConnection::bluetooth_scanner_set_mode(const BluetoothScannerSetModeRequest &msg) {
|
||||
bluetooth_proxy::global_bluetooth_proxy->bluetooth_scanner_set_mode(
|
||||
msg.mode == enums::BluetoothScannerMode::BLUETOOTH_SCANNER_MODE_ACTIVE);
|
||||
}
|
||||
@@ -1144,7 +1139,7 @@ bool APIConnection::check_voice_assistant_api_connection_() const {
|
||||
voice_assistant::global_voice_assistant->get_api_connection() == this;
|
||||
}
|
||||
|
||||
void APIConnection::on_subscribe_voice_assistant_request(const SubscribeVoiceAssistantRequest &msg) {
|
||||
void APIConnection::subscribe_voice_assistant(const SubscribeVoiceAssistantRequest &msg) {
|
||||
if (voice_assistant::global_voice_assistant != nullptr) {
|
||||
voice_assistant::global_voice_assistant->client_subscription(this, msg.subscribe);
|
||||
}
|
||||
@@ -1190,7 +1185,7 @@ void APIConnection::on_voice_assistant_announce_request(const VoiceAssistantAnno
|
||||
}
|
||||
}
|
||||
|
||||
bool APIConnection::send_voice_assistant_get_configuration_response_(const VoiceAssistantConfigurationRequest &msg) {
|
||||
bool APIConnection::send_voice_assistant_get_configuration_response(const VoiceAssistantConfigurationRequest &msg) {
|
||||
VoiceAssistantConfigurationResponse resp;
|
||||
if (!this->check_voice_assistant_api_connection_()) {
|
||||
return this->send_message(resp, VoiceAssistantConfigurationResponse::MESSAGE_TYPE);
|
||||
@@ -1227,13 +1222,8 @@ bool APIConnection::send_voice_assistant_get_configuration_response_(const Voice
|
||||
resp.max_active_wake_words = config.max_active_wake_words;
|
||||
return this->send_message(resp, VoiceAssistantConfigurationResponse::MESSAGE_TYPE);
|
||||
}
|
||||
void APIConnection::on_voice_assistant_configuration_request(const VoiceAssistantConfigurationRequest &msg) {
|
||||
if (!this->send_voice_assistant_get_configuration_response_(msg)) {
|
||||
this->on_fatal_error();
|
||||
}
|
||||
}
|
||||
|
||||
void APIConnection::on_voice_assistant_set_configuration(const VoiceAssistantSetConfiguration &msg) {
|
||||
void APIConnection::voice_assistant_set_configuration(const VoiceAssistantSetConfiguration &msg) {
|
||||
if (this->check_voice_assistant_api_connection_()) {
|
||||
voice_assistant::global_voice_assistant->on_set_configuration(msg.active_wake_words);
|
||||
}
|
||||
@@ -1241,11 +1231,11 @@ void APIConnection::on_voice_assistant_set_configuration(const VoiceAssistantSet
|
||||
#endif
|
||||
|
||||
#ifdef USE_ZWAVE_PROXY
|
||||
void APIConnection::on_z_wave_proxy_frame(const ZWaveProxyFrame &msg) {
|
||||
void APIConnection::zwave_proxy_frame(const ZWaveProxyFrame &msg) {
|
||||
zwave_proxy::global_zwave_proxy->send_frame(msg.data, msg.data_len);
|
||||
}
|
||||
|
||||
void APIConnection::on_z_wave_proxy_request(const ZWaveProxyRequest &msg) {
|
||||
void APIConnection::zwave_proxy_request(const ZWaveProxyRequest &msg) {
|
||||
zwave_proxy::global_zwave_proxy->zwave_proxy_request(this, msg.type);
|
||||
}
|
||||
#endif
|
||||
@@ -1273,7 +1263,7 @@ uint16_t APIConnection::try_send_alarm_control_panel_info(EntityBase *entity, AP
|
||||
return fill_and_encode_entity_info(a_alarm_control_panel, msg, ListEntitiesAlarmControlPanelResponse::MESSAGE_TYPE,
|
||||
conn, remaining_size);
|
||||
}
|
||||
void APIConnection::on_alarm_control_panel_command_request(const AlarmControlPanelCommandRequest &msg) {
|
||||
void APIConnection::alarm_control_panel_command(const AlarmControlPanelCommandRequest &msg) {
|
||||
ENTITY_COMMAND_MAKE_CALL(alarm_control_panel::AlarmControlPanel, a_alarm_control_panel, alarm_control_panel)
|
||||
switch (msg.command) {
|
||||
case enums::ALARM_CONTROL_PANEL_DISARM:
|
||||
@@ -1333,7 +1323,7 @@ uint16_t APIConnection::try_send_water_heater_info(EntityBase *entity, APIConnec
|
||||
return fill_and_encode_entity_info(wh, msg, ListEntitiesWaterHeaterResponse::MESSAGE_TYPE, conn, remaining_size);
|
||||
}
|
||||
|
||||
void APIConnection::on_water_heater_command_request(const WaterHeaterCommandRequest &msg) {
|
||||
void APIConnection::water_heater_command(const WaterHeaterCommandRequest &msg) {
|
||||
ENTITY_COMMAND_MAKE_CALL(water_heater::WaterHeater, water_heater, water_heater)
|
||||
if (msg.has_fields & enums::WATER_HEATER_COMMAND_HAS_MODE)
|
||||
call.set_mode(static_cast<water_heater::WaterHeaterMode>(msg.mode));
|
||||
@@ -1375,7 +1365,7 @@ uint16_t APIConnection::try_send_event_info(EntityBase *entity, APIConnection *c
|
||||
#endif
|
||||
|
||||
#ifdef USE_IR_RF
|
||||
void APIConnection::on_infrared_rf_transmit_raw_timings_request(const InfraredRFTransmitRawTimingsRequest &msg) {
|
||||
void APIConnection::infrared_rf_transmit_raw_timings(const InfraredRFTransmitRawTimingsRequest &msg) {
|
||||
// TODO: When RF is implemented, add a field to the message to distinguish IR vs RF
|
||||
// and dispatch to the appropriate entity type based on that field.
|
||||
#ifdef USE_INFRARED
|
||||
@@ -1429,7 +1419,7 @@ uint16_t APIConnection::try_send_update_info(EntityBase *entity, APIConnection *
|
||||
msg.device_class = update->get_device_class_ref();
|
||||
return fill_and_encode_entity_info(update, msg, ListEntitiesUpdateResponse::MESSAGE_TYPE, conn, remaining_size);
|
||||
}
|
||||
void APIConnection::on_update_command_request(const UpdateCommandRequest &msg) {
|
||||
void APIConnection::update_command(const UpdateCommandRequest &msg) {
|
||||
ENTITY_COMMAND_GET(update::UpdateEntity, update, update)
|
||||
|
||||
switch (msg.command) {
|
||||
@@ -1480,7 +1470,7 @@ void APIConnection::complete_authentication_() {
|
||||
#endif
|
||||
}
|
||||
|
||||
bool APIConnection::send_hello_response_(const HelloRequest &msg) {
|
||||
bool APIConnection::send_hello_response(const HelloRequest &msg) {
|
||||
// Copy client name with truncation if needed (set_client_name handles truncation)
|
||||
this->helper_->set_client_name(msg.client_info.c_str(), msg.client_info.size());
|
||||
this->client_api_version_major_ = msg.api_version_major;
|
||||
@@ -1501,12 +1491,12 @@ bool APIConnection::send_hello_response_(const HelloRequest &msg) {
|
||||
return this->send_message(resp, HelloResponse::MESSAGE_TYPE);
|
||||
}
|
||||
|
||||
bool APIConnection::send_ping_response_() {
|
||||
bool APIConnection::send_ping_response(const PingRequest &msg) {
|
||||
PingResponse resp;
|
||||
return this->send_message(resp, PingResponse::MESSAGE_TYPE);
|
||||
}
|
||||
|
||||
bool APIConnection::send_device_info_response_() {
|
||||
bool APIConnection::send_device_info_response(const DeviceInfoRequest &msg) {
|
||||
DeviceInfoResponse resp{};
|
||||
resp.name = StringRef(App.get_name());
|
||||
resp.friendly_name = StringRef(App.get_friendly_name());
|
||||
@@ -1629,26 +1619,6 @@ bool APIConnection::send_device_info_response_() {
|
||||
|
||||
return this->send_message(resp, DeviceInfoResponse::MESSAGE_TYPE);
|
||||
}
|
||||
void APIConnection::on_hello_request(const HelloRequest &msg) {
|
||||
if (!this->send_hello_response_(msg)) {
|
||||
this->on_fatal_error();
|
||||
}
|
||||
}
|
||||
void APIConnection::on_disconnect_request() {
|
||||
if (!this->send_disconnect_response_()) {
|
||||
this->on_fatal_error();
|
||||
}
|
||||
}
|
||||
void APIConnection::on_ping_request() {
|
||||
if (!this->send_ping_response_()) {
|
||||
this->on_fatal_error();
|
||||
}
|
||||
}
|
||||
void APIConnection::on_device_info_request() {
|
||||
if (!this->send_device_info_response_()) {
|
||||
this->on_fatal_error();
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef USE_API_HOMEASSISTANT_STATES
|
||||
void APIConnection::on_home_assistant_state_response(const HomeAssistantStateResponse &msg) {
|
||||
@@ -1687,7 +1657,7 @@ void APIConnection::on_home_assistant_state_response(const HomeAssistantStateRes
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_API_USER_DEFINED_ACTIONS
|
||||
void APIConnection::on_execute_service_request(const ExecuteServiceRequest &msg) {
|
||||
void APIConnection::execute_service(const ExecuteServiceRequest &msg) {
|
||||
bool found = false;
|
||||
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES
|
||||
// Register the call and get a unique server-generated action_call_id
|
||||
@@ -1753,7 +1723,7 @@ void APIConnection::on_homeassistant_action_response(const HomeassistantActionRe
|
||||
};
|
||||
#endif
|
||||
#ifdef USE_API_NOISE
|
||||
bool APIConnection::send_noise_encryption_set_key_response_(const NoiseEncryptionSetKeyRequest &msg) {
|
||||
bool APIConnection::send_noise_encryption_set_key_response(const NoiseEncryptionSetKeyRequest &msg) {
|
||||
NoiseEncryptionSetKeyResponse resp;
|
||||
resp.success = false;
|
||||
|
||||
@@ -1774,14 +1744,11 @@ bool APIConnection::send_noise_encryption_set_key_response_(const NoiseEncryptio
|
||||
|
||||
return this->send_message(resp, NoiseEncryptionSetKeyResponse::MESSAGE_TYPE);
|
||||
}
|
||||
void APIConnection::on_noise_encryption_set_key_request(const NoiseEncryptionSetKeyRequest &msg) {
|
||||
if (!this->send_noise_encryption_set_key_response_(msg)) {
|
||||
this->on_fatal_error();
|
||||
}
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_API_HOMEASSISTANT_STATES
|
||||
void APIConnection::on_subscribe_home_assistant_states_request() { state_subs_at_ = 0; }
|
||||
void APIConnection::subscribe_home_assistant_states(const SubscribeHomeAssistantStatesRequest &msg) {
|
||||
state_subs_at_ = 0;
|
||||
}
|
||||
#endif
|
||||
bool APIConnection::try_to_clear_buffer(bool log_out_of_space) {
|
||||
if (this->flags_.remove)
|
||||
|
||||
@@ -28,7 +28,7 @@ static constexpr size_t MAX_INITIAL_PER_BATCH = 34; // For clients >= AP
|
||||
static_assert(MAX_MESSAGES_PER_BATCH >= MAX_INITIAL_PER_BATCH,
|
||||
"MAX_MESSAGES_PER_BATCH must be >= MAX_INITIAL_PER_BATCH");
|
||||
|
||||
class APIConnection final : public APIServerConnectionBase {
|
||||
class APIConnection final : public APIServerConnection {
|
||||
public:
|
||||
friend class APIServer;
|
||||
friend class ListEntitiesIterator;
|
||||
@@ -47,72 +47,72 @@ class APIConnection final : public APIServerConnectionBase {
|
||||
#endif
|
||||
#ifdef USE_COVER
|
||||
bool send_cover_state(cover::Cover *cover);
|
||||
void on_cover_command_request(const CoverCommandRequest &msg) override;
|
||||
void cover_command(const CoverCommandRequest &msg) override;
|
||||
#endif
|
||||
#ifdef USE_FAN
|
||||
bool send_fan_state(fan::Fan *fan);
|
||||
void on_fan_command_request(const FanCommandRequest &msg) override;
|
||||
void fan_command(const FanCommandRequest &msg) override;
|
||||
#endif
|
||||
#ifdef USE_LIGHT
|
||||
bool send_light_state(light::LightState *light);
|
||||
void on_light_command_request(const LightCommandRequest &msg) override;
|
||||
void light_command(const LightCommandRequest &msg) override;
|
||||
#endif
|
||||
#ifdef USE_SENSOR
|
||||
bool send_sensor_state(sensor::Sensor *sensor);
|
||||
#endif
|
||||
#ifdef USE_SWITCH
|
||||
bool send_switch_state(switch_::Switch *a_switch);
|
||||
void on_switch_command_request(const SwitchCommandRequest &msg) override;
|
||||
void switch_command(const SwitchCommandRequest &msg) override;
|
||||
#endif
|
||||
#ifdef USE_TEXT_SENSOR
|
||||
bool send_text_sensor_state(text_sensor::TextSensor *text_sensor);
|
||||
#endif
|
||||
#ifdef USE_CAMERA
|
||||
void set_camera_state(std::shared_ptr<camera::CameraImage> image);
|
||||
void on_camera_image_request(const CameraImageRequest &msg) override;
|
||||
void camera_image(const CameraImageRequest &msg) override;
|
||||
#endif
|
||||
#ifdef USE_CLIMATE
|
||||
bool send_climate_state(climate::Climate *climate);
|
||||
void on_climate_command_request(const ClimateCommandRequest &msg) override;
|
||||
void climate_command(const ClimateCommandRequest &msg) override;
|
||||
#endif
|
||||
#ifdef USE_NUMBER
|
||||
bool send_number_state(number::Number *number);
|
||||
void on_number_command_request(const NumberCommandRequest &msg) override;
|
||||
void number_command(const NumberCommandRequest &msg) override;
|
||||
#endif
|
||||
#ifdef USE_DATETIME_DATE
|
||||
bool send_date_state(datetime::DateEntity *date);
|
||||
void on_date_command_request(const DateCommandRequest &msg) override;
|
||||
void date_command(const DateCommandRequest &msg) override;
|
||||
#endif
|
||||
#ifdef USE_DATETIME_TIME
|
||||
bool send_time_state(datetime::TimeEntity *time);
|
||||
void on_time_command_request(const TimeCommandRequest &msg) override;
|
||||
void time_command(const TimeCommandRequest &msg) override;
|
||||
#endif
|
||||
#ifdef USE_DATETIME_DATETIME
|
||||
bool send_datetime_state(datetime::DateTimeEntity *datetime);
|
||||
void on_date_time_command_request(const DateTimeCommandRequest &msg) override;
|
||||
void datetime_command(const DateTimeCommandRequest &msg) override;
|
||||
#endif
|
||||
#ifdef USE_TEXT
|
||||
bool send_text_state(text::Text *text);
|
||||
void on_text_command_request(const TextCommandRequest &msg) override;
|
||||
void text_command(const TextCommandRequest &msg) override;
|
||||
#endif
|
||||
#ifdef USE_SELECT
|
||||
bool send_select_state(select::Select *select);
|
||||
void on_select_command_request(const SelectCommandRequest &msg) override;
|
||||
void select_command(const SelectCommandRequest &msg) override;
|
||||
#endif
|
||||
#ifdef USE_BUTTON
|
||||
void on_button_command_request(const ButtonCommandRequest &msg) override;
|
||||
void button_command(const ButtonCommandRequest &msg) override;
|
||||
#endif
|
||||
#ifdef USE_LOCK
|
||||
bool send_lock_state(lock::Lock *a_lock);
|
||||
void on_lock_command_request(const LockCommandRequest &msg) override;
|
||||
void lock_command(const LockCommandRequest &msg) override;
|
||||
#endif
|
||||
#ifdef USE_VALVE
|
||||
bool send_valve_state(valve::Valve *valve);
|
||||
void on_valve_command_request(const ValveCommandRequest &msg) override;
|
||||
void valve_command(const ValveCommandRequest &msg) override;
|
||||
#endif
|
||||
#ifdef USE_MEDIA_PLAYER
|
||||
bool send_media_player_state(media_player::MediaPlayer *media_player);
|
||||
void on_media_player_command_request(const MediaPlayerCommandRequest &msg) override;
|
||||
void media_player_command(const MediaPlayerCommandRequest &msg) override;
|
||||
#endif
|
||||
bool try_send_log_message(int level, const char *tag, const char *line, size_t message_len);
|
||||
#ifdef USE_API_HOMEASSISTANT_SERVICES
|
||||
@@ -126,18 +126,18 @@ class APIConnection final : public APIServerConnectionBase {
|
||||
#endif // USE_API_HOMEASSISTANT_ACTION_RESPONSES
|
||||
#endif // USE_API_HOMEASSISTANT_SERVICES
|
||||
#ifdef USE_BLUETOOTH_PROXY
|
||||
void on_subscribe_bluetooth_le_advertisements_request(const SubscribeBluetoothLEAdvertisementsRequest &msg) override;
|
||||
void on_unsubscribe_bluetooth_le_advertisements_request() override;
|
||||
void subscribe_bluetooth_le_advertisements(const SubscribeBluetoothLEAdvertisementsRequest &msg) override;
|
||||
void unsubscribe_bluetooth_le_advertisements(const UnsubscribeBluetoothLEAdvertisementsRequest &msg) override;
|
||||
|
||||
void on_bluetooth_device_request(const BluetoothDeviceRequest &msg) override;
|
||||
void on_bluetooth_gatt_read_request(const BluetoothGATTReadRequest &msg) override;
|
||||
void on_bluetooth_gatt_write_request(const BluetoothGATTWriteRequest &msg) override;
|
||||
void on_bluetooth_gatt_read_descriptor_request(const BluetoothGATTReadDescriptorRequest &msg) override;
|
||||
void on_bluetooth_gatt_write_descriptor_request(const BluetoothGATTWriteDescriptorRequest &msg) override;
|
||||
void on_bluetooth_gatt_get_services_request(const BluetoothGATTGetServicesRequest &msg) override;
|
||||
void on_bluetooth_gatt_notify_request(const BluetoothGATTNotifyRequest &msg) override;
|
||||
void on_subscribe_bluetooth_connections_free_request() override;
|
||||
void on_bluetooth_scanner_set_mode_request(const BluetoothScannerSetModeRequest &msg) override;
|
||||
void bluetooth_device_request(const BluetoothDeviceRequest &msg) override;
|
||||
void bluetooth_gatt_read(const BluetoothGATTReadRequest &msg) override;
|
||||
void bluetooth_gatt_write(const BluetoothGATTWriteRequest &msg) override;
|
||||
void bluetooth_gatt_read_descriptor(const BluetoothGATTReadDescriptorRequest &msg) override;
|
||||
void bluetooth_gatt_write_descriptor(const BluetoothGATTWriteDescriptorRequest &msg) override;
|
||||
void bluetooth_gatt_get_services(const BluetoothGATTGetServicesRequest &msg) override;
|
||||
void bluetooth_gatt_notify(const BluetoothGATTNotifyRequest &msg) override;
|
||||
bool send_subscribe_bluetooth_connections_free_response(const SubscribeBluetoothConnectionsFreeRequest &msg) override;
|
||||
void bluetooth_scanner_set_mode(const BluetoothScannerSetModeRequest &msg) override;
|
||||
|
||||
#endif
|
||||
#ifdef USE_HOMEASSISTANT_TIME
|
||||
@@ -148,33 +148,33 @@ class APIConnection final : public APIServerConnectionBase {
|
||||
#endif
|
||||
|
||||
#ifdef USE_VOICE_ASSISTANT
|
||||
void on_subscribe_voice_assistant_request(const SubscribeVoiceAssistantRequest &msg) override;
|
||||
void subscribe_voice_assistant(const SubscribeVoiceAssistantRequest &msg) override;
|
||||
void on_voice_assistant_response(const VoiceAssistantResponse &msg) override;
|
||||
void on_voice_assistant_event_response(const VoiceAssistantEventResponse &msg) override;
|
||||
void on_voice_assistant_audio(const VoiceAssistantAudio &msg) override;
|
||||
void on_voice_assistant_timer_event_response(const VoiceAssistantTimerEventResponse &msg) override;
|
||||
void on_voice_assistant_announce_request(const VoiceAssistantAnnounceRequest &msg) override;
|
||||
void on_voice_assistant_configuration_request(const VoiceAssistantConfigurationRequest &msg) override;
|
||||
void on_voice_assistant_set_configuration(const VoiceAssistantSetConfiguration &msg) override;
|
||||
bool send_voice_assistant_get_configuration_response(const VoiceAssistantConfigurationRequest &msg) override;
|
||||
void voice_assistant_set_configuration(const VoiceAssistantSetConfiguration &msg) override;
|
||||
#endif
|
||||
|
||||
#ifdef USE_ZWAVE_PROXY
|
||||
void on_z_wave_proxy_frame(const ZWaveProxyFrame &msg) override;
|
||||
void on_z_wave_proxy_request(const ZWaveProxyRequest &msg) override;
|
||||
void zwave_proxy_frame(const ZWaveProxyFrame &msg) override;
|
||||
void zwave_proxy_request(const ZWaveProxyRequest &msg) override;
|
||||
#endif
|
||||
|
||||
#ifdef USE_ALARM_CONTROL_PANEL
|
||||
bool send_alarm_control_panel_state(alarm_control_panel::AlarmControlPanel *a_alarm_control_panel);
|
||||
void on_alarm_control_panel_command_request(const AlarmControlPanelCommandRequest &msg) override;
|
||||
void alarm_control_panel_command(const AlarmControlPanelCommandRequest &msg) override;
|
||||
#endif
|
||||
|
||||
#ifdef USE_WATER_HEATER
|
||||
bool send_water_heater_state(water_heater::WaterHeater *water_heater);
|
||||
void on_water_heater_command_request(const WaterHeaterCommandRequest &msg) override;
|
||||
void water_heater_command(const WaterHeaterCommandRequest &msg) override;
|
||||
#endif
|
||||
|
||||
#ifdef USE_IR_RF
|
||||
void on_infrared_rf_transmit_raw_timings_request(const InfraredRFTransmitRawTimingsRequest &msg) override;
|
||||
void infrared_rf_transmit_raw_timings(const InfraredRFTransmitRawTimingsRequest &msg) override;
|
||||
void send_infrared_rf_receive_event(const InfraredRFReceiveEvent &msg);
|
||||
#endif
|
||||
|
||||
@@ -184,11 +184,11 @@ class APIConnection final : public APIServerConnectionBase {
|
||||
|
||||
#ifdef USE_UPDATE
|
||||
bool send_update_state(update::UpdateEntity *update);
|
||||
void on_update_command_request(const UpdateCommandRequest &msg) override;
|
||||
void update_command(const UpdateCommandRequest &msg) override;
|
||||
#endif
|
||||
|
||||
void on_disconnect_response() override;
|
||||
void on_ping_response() override {
|
||||
void on_disconnect_response(const DisconnectResponse &value) override;
|
||||
void on_ping_response(const PingResponse &value) override {
|
||||
// we initiated ping
|
||||
this->flags_.sent_ping = false;
|
||||
}
|
||||
@@ -198,12 +198,12 @@ class APIConnection final : public APIServerConnectionBase {
|
||||
#ifdef USE_HOMEASSISTANT_TIME
|
||||
void on_get_time_response(const GetTimeResponse &value) override;
|
||||
#endif
|
||||
void on_hello_request(const HelloRequest &msg) override;
|
||||
void on_disconnect_request() override;
|
||||
void on_ping_request() override;
|
||||
void on_device_info_request() override;
|
||||
void on_list_entities_request() override { this->begin_iterator_(ActiveIterator::LIST_ENTITIES); }
|
||||
void on_subscribe_states_request() override {
|
||||
bool send_hello_response(const HelloRequest &msg) override;
|
||||
bool send_disconnect_response(const DisconnectRequest &msg) override;
|
||||
bool send_ping_response(const PingRequest &msg) override;
|
||||
bool send_device_info_response(const DeviceInfoRequest &msg) override;
|
||||
void list_entities(const ListEntitiesRequest &msg) override { this->begin_iterator_(ActiveIterator::LIST_ENTITIES); }
|
||||
void subscribe_states(const SubscribeStatesRequest &msg) override {
|
||||
this->flags_.state_subscription = true;
|
||||
// Start initial state iterator only if no iterator is active
|
||||
// If list_entities is running, we'll start initial_state when it completes
|
||||
@@ -211,19 +211,21 @@ class APIConnection final : public APIServerConnectionBase {
|
||||
this->begin_iterator_(ActiveIterator::INITIAL_STATE);
|
||||
}
|
||||
}
|
||||
void on_subscribe_logs_request(const SubscribeLogsRequest &msg) override {
|
||||
void subscribe_logs(const SubscribeLogsRequest &msg) override {
|
||||
this->flags_.log_subscription = msg.level;
|
||||
if (msg.dump_config)
|
||||
App.schedule_dump_config();
|
||||
}
|
||||
#ifdef USE_API_HOMEASSISTANT_SERVICES
|
||||
void on_subscribe_homeassistant_services_request() override { this->flags_.service_call_subscription = true; }
|
||||
void subscribe_homeassistant_services(const SubscribeHomeassistantServicesRequest &msg) override {
|
||||
this->flags_.service_call_subscription = true;
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_API_HOMEASSISTANT_STATES
|
||||
void on_subscribe_home_assistant_states_request() override;
|
||||
void subscribe_home_assistant_states(const SubscribeHomeAssistantStatesRequest &msg) override;
|
||||
#endif
|
||||
#ifdef USE_API_USER_DEFINED_ACTIONS
|
||||
void on_execute_service_request(const ExecuteServiceRequest &msg) override;
|
||||
void execute_service(const ExecuteServiceRequest &msg) override;
|
||||
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES
|
||||
void send_execute_service_response(uint32_t call_id, bool success, StringRef error_message);
|
||||
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES_JSON
|
||||
@@ -233,7 +235,7 @@ class APIConnection final : public APIServerConnectionBase {
|
||||
#endif // USE_API_USER_DEFINED_ACTION_RESPONSES
|
||||
#endif
|
||||
#ifdef USE_API_NOISE
|
||||
void on_noise_encryption_set_key_request(const NoiseEncryptionSetKeyRequest &msg) override;
|
||||
bool send_noise_encryption_set_key_response(const NoiseEncryptionSetKeyRequest &msg) override;
|
||||
#endif
|
||||
|
||||
bool is_authenticated() override {
|
||||
@@ -283,21 +285,6 @@ class APIConnection final : public APIServerConnectionBase {
|
||||
// Helper function to handle authentication completion
|
||||
void complete_authentication_();
|
||||
|
||||
// Pattern B helpers: send response and return success/failure
|
||||
bool send_hello_response_(const HelloRequest &msg);
|
||||
bool send_disconnect_response_();
|
||||
bool send_ping_response_();
|
||||
bool send_device_info_response_();
|
||||
#ifdef USE_API_NOISE
|
||||
bool send_noise_encryption_set_key_response_(const NoiseEncryptionSetKeyRequest &msg);
|
||||
#endif
|
||||
#ifdef USE_BLUETOOTH_PROXY
|
||||
bool send_subscribe_bluetooth_connections_free_response_();
|
||||
#endif
|
||||
#ifdef USE_VOICE_ASSISTANT
|
||||
bool send_voice_assistant_get_configuration_response_(const VoiceAssistantConfigurationRequest &msg);
|
||||
#endif
|
||||
|
||||
#ifdef USE_CAMERA
|
||||
void try_send_camera_image_();
|
||||
#endif
|
||||
|
||||
@@ -440,6 +440,19 @@ class PingResponse final : public ProtoMessage {
|
||||
|
||||
protected:
|
||||
};
|
||||
class DeviceInfoRequest final : public ProtoMessage {
|
||||
public:
|
||||
static constexpr uint8_t MESSAGE_TYPE = 9;
|
||||
static constexpr uint8_t ESTIMATED_SIZE = 0;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
const char *message_name() const override { return "device_info_request"; }
|
||||
#endif
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
const char *dump_to(DumpBuffer &out) const override;
|
||||
#endif
|
||||
|
||||
protected:
|
||||
};
|
||||
#ifdef USE_AREAS
|
||||
class AreaInfo final : public ProtoMessage {
|
||||
public:
|
||||
@@ -533,6 +546,19 @@ class DeviceInfoResponse final : public ProtoMessage {
|
||||
|
||||
protected:
|
||||
};
|
||||
class ListEntitiesRequest final : public ProtoMessage {
|
||||
public:
|
||||
static constexpr uint8_t MESSAGE_TYPE = 11;
|
||||
static constexpr uint8_t ESTIMATED_SIZE = 0;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
const char *message_name() const override { return "list_entities_request"; }
|
||||
#endif
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
const char *dump_to(DumpBuffer &out) const override;
|
||||
#endif
|
||||
|
||||
protected:
|
||||
};
|
||||
class ListEntitiesDoneResponse final : public ProtoMessage {
|
||||
public:
|
||||
static constexpr uint8_t MESSAGE_TYPE = 19;
|
||||
@@ -546,6 +572,19 @@ class ListEntitiesDoneResponse final : public ProtoMessage {
|
||||
|
||||
protected:
|
||||
};
|
||||
class SubscribeStatesRequest final : public ProtoMessage {
|
||||
public:
|
||||
static constexpr uint8_t MESSAGE_TYPE = 20;
|
||||
static constexpr uint8_t ESTIMATED_SIZE = 0;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
const char *message_name() const override { return "subscribe_states_request"; }
|
||||
#endif
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
const char *dump_to(DumpBuffer &out) const override;
|
||||
#endif
|
||||
|
||||
protected:
|
||||
};
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
class ListEntitiesBinarySensorResponse final : public InfoResponseProtoMessage {
|
||||
public:
|
||||
@@ -998,6 +1037,19 @@ class NoiseEncryptionSetKeyResponse final : public ProtoMessage {
|
||||
};
|
||||
#endif
|
||||
#ifdef USE_API_HOMEASSISTANT_SERVICES
|
||||
class SubscribeHomeassistantServicesRequest final : public ProtoMessage {
|
||||
public:
|
||||
static constexpr uint8_t MESSAGE_TYPE = 34;
|
||||
static constexpr uint8_t ESTIMATED_SIZE = 0;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
const char *message_name() const override { return "subscribe_homeassistant_services_request"; }
|
||||
#endif
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
const char *dump_to(DumpBuffer &out) const override;
|
||||
#endif
|
||||
|
||||
protected:
|
||||
};
|
||||
class HomeassistantServiceMap final : public ProtoMessage {
|
||||
public:
|
||||
StringRef key{};
|
||||
@@ -1065,6 +1117,19 @@ class HomeassistantActionResponse final : public ProtoDecodableMessage {
|
||||
};
|
||||
#endif
|
||||
#ifdef USE_API_HOMEASSISTANT_STATES
|
||||
class SubscribeHomeAssistantStatesRequest final : public ProtoMessage {
|
||||
public:
|
||||
static constexpr uint8_t MESSAGE_TYPE = 38;
|
||||
static constexpr uint8_t ESTIMATED_SIZE = 0;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
const char *message_name() const override { return "subscribe_home_assistant_states_request"; }
|
||||
#endif
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
const char *dump_to(DumpBuffer &out) const override;
|
||||
#endif
|
||||
|
||||
protected:
|
||||
};
|
||||
class SubscribeHomeAssistantStateResponse final : public ProtoMessage {
|
||||
public:
|
||||
static constexpr uint8_t MESSAGE_TYPE = 39;
|
||||
@@ -2095,6 +2160,19 @@ class BluetoothGATTNotifyDataResponse final : public ProtoMessage {
|
||||
|
||||
protected:
|
||||
};
|
||||
class SubscribeBluetoothConnectionsFreeRequest final : public ProtoMessage {
|
||||
public:
|
||||
static constexpr uint8_t MESSAGE_TYPE = 80;
|
||||
static constexpr uint8_t ESTIMATED_SIZE = 0;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
const char *message_name() const override { return "subscribe_bluetooth_connections_free_request"; }
|
||||
#endif
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
const char *dump_to(DumpBuffer &out) const override;
|
||||
#endif
|
||||
|
||||
protected:
|
||||
};
|
||||
class BluetoothConnectionsFreeResponse final : public ProtoMessage {
|
||||
public:
|
||||
static constexpr uint8_t MESSAGE_TYPE = 81;
|
||||
@@ -2201,6 +2279,19 @@ class BluetoothDeviceUnpairingResponse final : public ProtoMessage {
|
||||
|
||||
protected:
|
||||
};
|
||||
class UnsubscribeBluetoothLEAdvertisementsRequest final : public ProtoMessage {
|
||||
public:
|
||||
static constexpr uint8_t MESSAGE_TYPE = 87;
|
||||
static constexpr uint8_t ESTIMATED_SIZE = 0;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
const char *message_name() const override { return "unsubscribe_bluetooth_le_advertisements_request"; }
|
||||
#endif
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
const char *dump_to(DumpBuffer &out) const override;
|
||||
#endif
|
||||
|
||||
protected:
|
||||
};
|
||||
class BluetoothDeviceClearCacheResponse final : public ProtoMessage {
|
||||
public:
|
||||
static constexpr uint8_t MESSAGE_TYPE = 88;
|
||||
|
||||
@@ -764,6 +764,10 @@ const char *PingResponse::dump_to(DumpBuffer &out) const {
|
||||
out.append("PingResponse {}");
|
||||
return out.c_str();
|
||||
}
|
||||
const char *DeviceInfoRequest::dump_to(DumpBuffer &out) const {
|
||||
out.append("DeviceInfoRequest {}");
|
||||
return out.c_str();
|
||||
}
|
||||
#ifdef USE_AREAS
|
||||
const char *AreaInfo::dump_to(DumpBuffer &out) const {
|
||||
MessageDumpHelper helper(out, "AreaInfo");
|
||||
@@ -844,10 +848,18 @@ const char *DeviceInfoResponse::dump_to(DumpBuffer &out) const {
|
||||
#endif
|
||||
return out.c_str();
|
||||
}
|
||||
const char *ListEntitiesRequest::dump_to(DumpBuffer &out) const {
|
||||
out.append("ListEntitiesRequest {}");
|
||||
return out.c_str();
|
||||
}
|
||||
const char *ListEntitiesDoneResponse::dump_to(DumpBuffer &out) const {
|
||||
out.append("ListEntitiesDoneResponse {}");
|
||||
return out.c_str();
|
||||
}
|
||||
const char *SubscribeStatesRequest::dump_to(DumpBuffer &out) const {
|
||||
out.append("SubscribeStatesRequest {}");
|
||||
return out.c_str();
|
||||
}
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
const char *ListEntitiesBinarySensorResponse::dump_to(DumpBuffer &out) const {
|
||||
MessageDumpHelper helper(out, "ListEntitiesBinarySensorResponse");
|
||||
@@ -1179,6 +1191,10 @@ const char *NoiseEncryptionSetKeyResponse::dump_to(DumpBuffer &out) const {
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_API_HOMEASSISTANT_SERVICES
|
||||
const char *SubscribeHomeassistantServicesRequest::dump_to(DumpBuffer &out) const {
|
||||
out.append("SubscribeHomeassistantServicesRequest {}");
|
||||
return out.c_str();
|
||||
}
|
||||
const char *HomeassistantServiceMap::dump_to(DumpBuffer &out) const {
|
||||
MessageDumpHelper helper(out, "HomeassistantServiceMap");
|
||||
dump_field(out, "key", this->key);
|
||||
@@ -1229,6 +1245,10 @@ const char *HomeassistantActionResponse::dump_to(DumpBuffer &out) const {
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_API_HOMEASSISTANT_STATES
|
||||
const char *SubscribeHomeAssistantStatesRequest::dump_to(DumpBuffer &out) const {
|
||||
out.append("SubscribeHomeAssistantStatesRequest {}");
|
||||
return out.c_str();
|
||||
}
|
||||
const char *SubscribeHomeAssistantStateResponse::dump_to(DumpBuffer &out) const {
|
||||
MessageDumpHelper helper(out, "SubscribeHomeAssistantStateResponse");
|
||||
dump_field(out, "entity_id", this->entity_id);
|
||||
@@ -1904,6 +1924,10 @@ const char *BluetoothGATTNotifyDataResponse::dump_to(DumpBuffer &out) const {
|
||||
dump_bytes_field(out, "data", this->data_ptr_, this->data_len_);
|
||||
return out.c_str();
|
||||
}
|
||||
const char *SubscribeBluetoothConnectionsFreeRequest::dump_to(DumpBuffer &out) const {
|
||||
out.append("SubscribeBluetoothConnectionsFreeRequest {}");
|
||||
return out.c_str();
|
||||
}
|
||||
const char *BluetoothConnectionsFreeResponse::dump_to(DumpBuffer &out) const {
|
||||
MessageDumpHelper helper(out, "BluetoothConnectionsFreeResponse");
|
||||
dump_field(out, "free", this->free);
|
||||
@@ -1946,6 +1970,10 @@ const char *BluetoothDeviceUnpairingResponse::dump_to(DumpBuffer &out) const {
|
||||
dump_field(out, "error", this->error);
|
||||
return out.c_str();
|
||||
}
|
||||
const char *UnsubscribeBluetoothLEAdvertisementsRequest::dump_to(DumpBuffer &out) const {
|
||||
out.append("UnsubscribeBluetoothLEAdvertisementsRequest {}");
|
||||
return out.c_str();
|
||||
}
|
||||
const char *BluetoothDeviceClearCacheResponse::dump_to(DumpBuffer &out) const {
|
||||
MessageDumpHelper helper(out, "BluetoothDeviceClearCacheResponse");
|
||||
dump_field(out, "address", this->address);
|
||||
|
||||
@@ -15,29 +15,9 @@ void APIServerConnectionBase::log_receive_message_(const LogString *name, const
|
||||
DumpBuffer dump_buf;
|
||||
ESP_LOGVV(TAG, "%s: %s", LOG_STR_ARG(name), msg.dump_to(dump_buf));
|
||||
}
|
||||
void APIServerConnectionBase::log_receive_message_(const LogString *name) {
|
||||
ESP_LOGVV(TAG, "%s: {}", LOG_STR_ARG(name));
|
||||
}
|
||||
#endif
|
||||
|
||||
void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, const uint8_t *msg_data) {
|
||||
// Check authentication/connection requirements
|
||||
switch (msg_type) {
|
||||
case HelloRequest::MESSAGE_TYPE: // No setup required
|
||||
case DisconnectRequest::MESSAGE_TYPE: // No setup required
|
||||
case PingRequest::MESSAGE_TYPE: // No setup required
|
||||
break;
|
||||
case 9 /* DeviceInfoRequest is empty */: // Connection setup only
|
||||
if (!this->check_connection_setup_()) {
|
||||
return;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
if (!this->check_authenticated_()) {
|
||||
return;
|
||||
}
|
||||
break;
|
||||
}
|
||||
switch (msg_type) {
|
||||
case HelloRequest::MESSAGE_TYPE: {
|
||||
HelloRequest msg;
|
||||
@@ -49,52 +29,66 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
|
||||
break;
|
||||
}
|
||||
case DisconnectRequest::MESSAGE_TYPE: {
|
||||
DisconnectRequest msg;
|
||||
// Empty message: no decode needed
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
this->log_receive_message_(LOG_STR("on_disconnect_request"));
|
||||
this->log_receive_message_(LOG_STR("on_disconnect_request"), msg);
|
||||
#endif
|
||||
this->on_disconnect_request();
|
||||
this->on_disconnect_request(msg);
|
||||
break;
|
||||
}
|
||||
case DisconnectResponse::MESSAGE_TYPE: {
|
||||
DisconnectResponse msg;
|
||||
// Empty message: no decode needed
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
this->log_receive_message_(LOG_STR("on_disconnect_response"));
|
||||
this->log_receive_message_(LOG_STR("on_disconnect_response"), msg);
|
||||
#endif
|
||||
this->on_disconnect_response();
|
||||
this->on_disconnect_response(msg);
|
||||
break;
|
||||
}
|
||||
case PingRequest::MESSAGE_TYPE: {
|
||||
PingRequest msg;
|
||||
// Empty message: no decode needed
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
this->log_receive_message_(LOG_STR("on_ping_request"));
|
||||
this->log_receive_message_(LOG_STR("on_ping_request"), msg);
|
||||
#endif
|
||||
this->on_ping_request();
|
||||
this->on_ping_request(msg);
|
||||
break;
|
||||
}
|
||||
case PingResponse::MESSAGE_TYPE: {
|
||||
PingResponse msg;
|
||||
// Empty message: no decode needed
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
this->log_receive_message_(LOG_STR("on_ping_response"));
|
||||
this->log_receive_message_(LOG_STR("on_ping_response"), msg);
|
||||
#endif
|
||||
this->on_ping_response();
|
||||
this->on_ping_response(msg);
|
||||
break;
|
||||
}
|
||||
case 9 /* DeviceInfoRequest is empty */: {
|
||||
case DeviceInfoRequest::MESSAGE_TYPE: {
|
||||
DeviceInfoRequest msg;
|
||||
// Empty message: no decode needed
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
this->log_receive_message_(LOG_STR("on_device_info_request"));
|
||||
this->log_receive_message_(LOG_STR("on_device_info_request"), msg);
|
||||
#endif
|
||||
this->on_device_info_request();
|
||||
this->on_device_info_request(msg);
|
||||
break;
|
||||
}
|
||||
case 11 /* ListEntitiesRequest is empty */: {
|
||||
case ListEntitiesRequest::MESSAGE_TYPE: {
|
||||
ListEntitiesRequest msg;
|
||||
// Empty message: no decode needed
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
this->log_receive_message_(LOG_STR("on_list_entities_request"));
|
||||
this->log_receive_message_(LOG_STR("on_list_entities_request"), msg);
|
||||
#endif
|
||||
this->on_list_entities_request();
|
||||
this->on_list_entities_request(msg);
|
||||
break;
|
||||
}
|
||||
case 20 /* SubscribeStatesRequest is empty */: {
|
||||
case SubscribeStatesRequest::MESSAGE_TYPE: {
|
||||
SubscribeStatesRequest msg;
|
||||
// Empty message: no decode needed
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
this->log_receive_message_(LOG_STR("on_subscribe_states_request"));
|
||||
this->log_receive_message_(LOG_STR("on_subscribe_states_request"), msg);
|
||||
#endif
|
||||
this->on_subscribe_states_request();
|
||||
this->on_subscribe_states_request(msg);
|
||||
break;
|
||||
}
|
||||
case SubscribeLogsRequest::MESSAGE_TYPE: {
|
||||
@@ -151,11 +145,13 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_API_HOMEASSISTANT_SERVICES
|
||||
case 34 /* SubscribeHomeassistantServicesRequest is empty */: {
|
||||
case SubscribeHomeassistantServicesRequest::MESSAGE_TYPE: {
|
||||
SubscribeHomeassistantServicesRequest msg;
|
||||
// Empty message: no decode needed
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
this->log_receive_message_(LOG_STR("on_subscribe_homeassistant_services_request"));
|
||||
this->log_receive_message_(LOG_STR("on_subscribe_homeassistant_services_request"), msg);
|
||||
#endif
|
||||
this->on_subscribe_homeassistant_services_request();
|
||||
this->on_subscribe_homeassistant_services_request(msg);
|
||||
break;
|
||||
}
|
||||
#endif
|
||||
@@ -169,11 +165,13 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
|
||||
break;
|
||||
}
|
||||
#ifdef USE_API_HOMEASSISTANT_STATES
|
||||
case 38 /* SubscribeHomeAssistantStatesRequest is empty */: {
|
||||
case SubscribeHomeAssistantStatesRequest::MESSAGE_TYPE: {
|
||||
SubscribeHomeAssistantStatesRequest msg;
|
||||
// Empty message: no decode needed
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
this->log_receive_message_(LOG_STR("on_subscribe_home_assistant_states_request"));
|
||||
this->log_receive_message_(LOG_STR("on_subscribe_home_assistant_states_request"), msg);
|
||||
#endif
|
||||
this->on_subscribe_home_assistant_states_request();
|
||||
this->on_subscribe_home_assistant_states_request(msg);
|
||||
break;
|
||||
}
|
||||
#endif
|
||||
@@ -376,20 +374,24 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_BLUETOOTH_PROXY
|
||||
case 80 /* SubscribeBluetoothConnectionsFreeRequest is empty */: {
|
||||
case SubscribeBluetoothConnectionsFreeRequest::MESSAGE_TYPE: {
|
||||
SubscribeBluetoothConnectionsFreeRequest msg;
|
||||
// Empty message: no decode needed
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
this->log_receive_message_(LOG_STR("on_subscribe_bluetooth_connections_free_request"));
|
||||
this->log_receive_message_(LOG_STR("on_subscribe_bluetooth_connections_free_request"), msg);
|
||||
#endif
|
||||
this->on_subscribe_bluetooth_connections_free_request();
|
||||
this->on_subscribe_bluetooth_connections_free_request(msg);
|
||||
break;
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_BLUETOOTH_PROXY
|
||||
case 87 /* UnsubscribeBluetoothLEAdvertisementsRequest is empty */: {
|
||||
case UnsubscribeBluetoothLEAdvertisementsRequest::MESSAGE_TYPE: {
|
||||
UnsubscribeBluetoothLEAdvertisementsRequest msg;
|
||||
// Empty message: no decode needed
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
this->log_receive_message_(LOG_STR("on_unsubscribe_bluetooth_le_advertisements_request"));
|
||||
this->log_receive_message_(LOG_STR("on_unsubscribe_bluetooth_le_advertisements_request"), msg);
|
||||
#endif
|
||||
this->on_unsubscribe_bluetooth_le_advertisements_request();
|
||||
this->on_unsubscribe_bluetooth_le_advertisements_request(msg);
|
||||
break;
|
||||
}
|
||||
#endif
|
||||
@@ -640,4 +642,231 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
|
||||
}
|
||||
}
|
||||
|
||||
void APIServerConnection::on_hello_request(const HelloRequest &msg) {
|
||||
if (!this->send_hello_response(msg)) {
|
||||
this->on_fatal_error();
|
||||
}
|
||||
}
|
||||
void APIServerConnection::on_disconnect_request(const DisconnectRequest &msg) {
|
||||
if (!this->send_disconnect_response(msg)) {
|
||||
this->on_fatal_error();
|
||||
}
|
||||
}
|
||||
void APIServerConnection::on_ping_request(const PingRequest &msg) {
|
||||
if (!this->send_ping_response(msg)) {
|
||||
this->on_fatal_error();
|
||||
}
|
||||
}
|
||||
void APIServerConnection::on_device_info_request(const DeviceInfoRequest &msg) {
|
||||
if (!this->send_device_info_response(msg)) {
|
||||
this->on_fatal_error();
|
||||
}
|
||||
}
|
||||
void APIServerConnection::on_list_entities_request(const ListEntitiesRequest &msg) { this->list_entities(msg); }
|
||||
void APIServerConnection::on_subscribe_states_request(const SubscribeStatesRequest &msg) {
|
||||
this->subscribe_states(msg);
|
||||
}
|
||||
void APIServerConnection::on_subscribe_logs_request(const SubscribeLogsRequest &msg) { this->subscribe_logs(msg); }
|
||||
#ifdef USE_API_HOMEASSISTANT_SERVICES
|
||||
void APIServerConnection::on_subscribe_homeassistant_services_request(
|
||||
const SubscribeHomeassistantServicesRequest &msg) {
|
||||
this->subscribe_homeassistant_services(msg);
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_API_HOMEASSISTANT_STATES
|
||||
void APIServerConnection::on_subscribe_home_assistant_states_request(const SubscribeHomeAssistantStatesRequest &msg) {
|
||||
this->subscribe_home_assistant_states(msg);
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_API_USER_DEFINED_ACTIONS
|
||||
void APIServerConnection::on_execute_service_request(const ExecuteServiceRequest &msg) { this->execute_service(msg); }
|
||||
#endif
|
||||
#ifdef USE_API_NOISE
|
||||
void APIServerConnection::on_noise_encryption_set_key_request(const NoiseEncryptionSetKeyRequest &msg) {
|
||||
if (!this->send_noise_encryption_set_key_response(msg)) {
|
||||
this->on_fatal_error();
|
||||
}
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_BUTTON
|
||||
void APIServerConnection::on_button_command_request(const ButtonCommandRequest &msg) { this->button_command(msg); }
|
||||
#endif
|
||||
#ifdef USE_CAMERA
|
||||
void APIServerConnection::on_camera_image_request(const CameraImageRequest &msg) { this->camera_image(msg); }
|
||||
#endif
|
||||
#ifdef USE_CLIMATE
|
||||
void APIServerConnection::on_climate_command_request(const ClimateCommandRequest &msg) { this->climate_command(msg); }
|
||||
#endif
|
||||
#ifdef USE_COVER
|
||||
void APIServerConnection::on_cover_command_request(const CoverCommandRequest &msg) { this->cover_command(msg); }
|
||||
#endif
|
||||
#ifdef USE_DATETIME_DATE
|
||||
void APIServerConnection::on_date_command_request(const DateCommandRequest &msg) { this->date_command(msg); }
|
||||
#endif
|
||||
#ifdef USE_DATETIME_DATETIME
|
||||
void APIServerConnection::on_date_time_command_request(const DateTimeCommandRequest &msg) {
|
||||
this->datetime_command(msg);
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_FAN
|
||||
void APIServerConnection::on_fan_command_request(const FanCommandRequest &msg) { this->fan_command(msg); }
|
||||
#endif
|
||||
#ifdef USE_LIGHT
|
||||
void APIServerConnection::on_light_command_request(const LightCommandRequest &msg) { this->light_command(msg); }
|
||||
#endif
|
||||
#ifdef USE_LOCK
|
||||
void APIServerConnection::on_lock_command_request(const LockCommandRequest &msg) { this->lock_command(msg); }
|
||||
#endif
|
||||
#ifdef USE_MEDIA_PLAYER
|
||||
void APIServerConnection::on_media_player_command_request(const MediaPlayerCommandRequest &msg) {
|
||||
this->media_player_command(msg);
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_NUMBER
|
||||
void APIServerConnection::on_number_command_request(const NumberCommandRequest &msg) { this->number_command(msg); }
|
||||
#endif
|
||||
#ifdef USE_SELECT
|
||||
void APIServerConnection::on_select_command_request(const SelectCommandRequest &msg) { this->select_command(msg); }
|
||||
#endif
|
||||
#ifdef USE_SIREN
|
||||
void APIServerConnection::on_siren_command_request(const SirenCommandRequest &msg) { this->siren_command(msg); }
|
||||
#endif
|
||||
#ifdef USE_SWITCH
|
||||
void APIServerConnection::on_switch_command_request(const SwitchCommandRequest &msg) { this->switch_command(msg); }
|
||||
#endif
|
||||
#ifdef USE_TEXT
|
||||
void APIServerConnection::on_text_command_request(const TextCommandRequest &msg) { this->text_command(msg); }
|
||||
#endif
|
||||
#ifdef USE_DATETIME_TIME
|
||||
void APIServerConnection::on_time_command_request(const TimeCommandRequest &msg) { this->time_command(msg); }
|
||||
#endif
|
||||
#ifdef USE_UPDATE
|
||||
void APIServerConnection::on_update_command_request(const UpdateCommandRequest &msg) { this->update_command(msg); }
|
||||
#endif
|
||||
#ifdef USE_VALVE
|
||||
void APIServerConnection::on_valve_command_request(const ValveCommandRequest &msg) { this->valve_command(msg); }
|
||||
#endif
|
||||
#ifdef USE_WATER_HEATER
|
||||
void APIServerConnection::on_water_heater_command_request(const WaterHeaterCommandRequest &msg) {
|
||||
this->water_heater_command(msg);
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_BLUETOOTH_PROXY
|
||||
void APIServerConnection::on_subscribe_bluetooth_le_advertisements_request(
|
||||
const SubscribeBluetoothLEAdvertisementsRequest &msg) {
|
||||
this->subscribe_bluetooth_le_advertisements(msg);
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_BLUETOOTH_PROXY
|
||||
void APIServerConnection::on_bluetooth_device_request(const BluetoothDeviceRequest &msg) {
|
||||
this->bluetooth_device_request(msg);
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_BLUETOOTH_PROXY
|
||||
void APIServerConnection::on_bluetooth_gatt_get_services_request(const BluetoothGATTGetServicesRequest &msg) {
|
||||
this->bluetooth_gatt_get_services(msg);
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_BLUETOOTH_PROXY
|
||||
void APIServerConnection::on_bluetooth_gatt_read_request(const BluetoothGATTReadRequest &msg) {
|
||||
this->bluetooth_gatt_read(msg);
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_BLUETOOTH_PROXY
|
||||
void APIServerConnection::on_bluetooth_gatt_write_request(const BluetoothGATTWriteRequest &msg) {
|
||||
this->bluetooth_gatt_write(msg);
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_BLUETOOTH_PROXY
|
||||
void APIServerConnection::on_bluetooth_gatt_read_descriptor_request(const BluetoothGATTReadDescriptorRequest &msg) {
|
||||
this->bluetooth_gatt_read_descriptor(msg);
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_BLUETOOTH_PROXY
|
||||
void APIServerConnection::on_bluetooth_gatt_write_descriptor_request(const BluetoothGATTWriteDescriptorRequest &msg) {
|
||||
this->bluetooth_gatt_write_descriptor(msg);
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_BLUETOOTH_PROXY
|
||||
void APIServerConnection::on_bluetooth_gatt_notify_request(const BluetoothGATTNotifyRequest &msg) {
|
||||
this->bluetooth_gatt_notify(msg);
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_BLUETOOTH_PROXY
|
||||
void APIServerConnection::on_subscribe_bluetooth_connections_free_request(
|
||||
const SubscribeBluetoothConnectionsFreeRequest &msg) {
|
||||
if (!this->send_subscribe_bluetooth_connections_free_response(msg)) {
|
||||
this->on_fatal_error();
|
||||
}
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_BLUETOOTH_PROXY
|
||||
void APIServerConnection::on_unsubscribe_bluetooth_le_advertisements_request(
|
||||
const UnsubscribeBluetoothLEAdvertisementsRequest &msg) {
|
||||
this->unsubscribe_bluetooth_le_advertisements(msg);
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_BLUETOOTH_PROXY
|
||||
void APIServerConnection::on_bluetooth_scanner_set_mode_request(const BluetoothScannerSetModeRequest &msg) {
|
||||
this->bluetooth_scanner_set_mode(msg);
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_VOICE_ASSISTANT
|
||||
void APIServerConnection::on_subscribe_voice_assistant_request(const SubscribeVoiceAssistantRequest &msg) {
|
||||
this->subscribe_voice_assistant(msg);
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_VOICE_ASSISTANT
|
||||
void APIServerConnection::on_voice_assistant_configuration_request(const VoiceAssistantConfigurationRequest &msg) {
|
||||
if (!this->send_voice_assistant_get_configuration_response(msg)) {
|
||||
this->on_fatal_error();
|
||||
}
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_VOICE_ASSISTANT
|
||||
void APIServerConnection::on_voice_assistant_set_configuration(const VoiceAssistantSetConfiguration &msg) {
|
||||
this->voice_assistant_set_configuration(msg);
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_ALARM_CONTROL_PANEL
|
||||
void APIServerConnection::on_alarm_control_panel_command_request(const AlarmControlPanelCommandRequest &msg) {
|
||||
this->alarm_control_panel_command(msg);
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_ZWAVE_PROXY
|
||||
void APIServerConnection::on_z_wave_proxy_frame(const ZWaveProxyFrame &msg) { this->zwave_proxy_frame(msg); }
|
||||
#endif
|
||||
#ifdef USE_ZWAVE_PROXY
|
||||
void APIServerConnection::on_z_wave_proxy_request(const ZWaveProxyRequest &msg) { this->zwave_proxy_request(msg); }
|
||||
#endif
|
||||
#ifdef USE_IR_RF
|
||||
void APIServerConnection::on_infrared_rf_transmit_raw_timings_request(const InfraredRFTransmitRawTimingsRequest &msg) {
|
||||
this->infrared_rf_transmit_raw_timings(msg);
|
||||
}
|
||||
#endif
|
||||
|
||||
void APIServerConnection::read_message(uint32_t msg_size, uint32_t msg_type, const uint8_t *msg_data) {
|
||||
// Check authentication/connection requirements for messages
|
||||
switch (msg_type) {
|
||||
case HelloRequest::MESSAGE_TYPE: // No setup required
|
||||
case DisconnectRequest::MESSAGE_TYPE: // No setup required
|
||||
case PingRequest::MESSAGE_TYPE: // No setup required
|
||||
break; // Skip all checks for these messages
|
||||
case DeviceInfoRequest::MESSAGE_TYPE: // Connection setup only
|
||||
if (!this->check_connection_setup_()) {
|
||||
return; // Connection not setup
|
||||
}
|
||||
break;
|
||||
default:
|
||||
// All other messages require authentication (which includes connection check)
|
||||
if (!this->check_authenticated_()) {
|
||||
return; // Authentication failed
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// Call base implementation to process the message
|
||||
APIServerConnectionBase::read_message(msg_size, msg_type, msg_data);
|
||||
}
|
||||
|
||||
} // namespace esphome::api
|
||||
|
||||
@@ -14,7 +14,6 @@ class APIServerConnectionBase : public ProtoService {
|
||||
protected:
|
||||
void log_send_message_(const char *name, const char *dump);
|
||||
void log_receive_message_(const LogString *name, const ProtoMessage &msg);
|
||||
void log_receive_message_(const LogString *name);
|
||||
|
||||
public:
|
||||
#endif
|
||||
@@ -29,15 +28,15 @@ class APIServerConnectionBase : public ProtoService {
|
||||
|
||||
virtual void on_hello_request(const HelloRequest &value){};
|
||||
|
||||
virtual void on_disconnect_request(){};
|
||||
virtual void on_disconnect_response(){};
|
||||
virtual void on_ping_request(){};
|
||||
virtual void on_ping_response(){};
|
||||
virtual void on_device_info_request(){};
|
||||
virtual void on_disconnect_request(const DisconnectRequest &value){};
|
||||
virtual void on_disconnect_response(const DisconnectResponse &value){};
|
||||
virtual void on_ping_request(const PingRequest &value){};
|
||||
virtual void on_ping_response(const PingResponse &value){};
|
||||
virtual void on_device_info_request(const DeviceInfoRequest &value){};
|
||||
|
||||
virtual void on_list_entities_request(){};
|
||||
virtual void on_list_entities_request(const ListEntitiesRequest &value){};
|
||||
|
||||
virtual void on_subscribe_states_request(){};
|
||||
virtual void on_subscribe_states_request(const SubscribeStatesRequest &value){};
|
||||
|
||||
#ifdef USE_COVER
|
||||
virtual void on_cover_command_request(const CoverCommandRequest &value){};
|
||||
@@ -62,14 +61,14 @@ class APIServerConnectionBase : public ProtoService {
|
||||
#endif
|
||||
|
||||
#ifdef USE_API_HOMEASSISTANT_SERVICES
|
||||
virtual void on_subscribe_homeassistant_services_request(){};
|
||||
virtual void on_subscribe_homeassistant_services_request(const SubscribeHomeassistantServicesRequest &value){};
|
||||
#endif
|
||||
|
||||
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
|
||||
virtual void on_homeassistant_action_response(const HomeassistantActionResponse &value){};
|
||||
#endif
|
||||
#ifdef USE_API_HOMEASSISTANT_STATES
|
||||
virtual void on_subscribe_home_assistant_states_request(){};
|
||||
virtual void on_subscribe_home_assistant_states_request(const SubscribeHomeAssistantStatesRequest &value){};
|
||||
#endif
|
||||
|
||||
#ifdef USE_API_HOMEASSISTANT_STATES
|
||||
@@ -148,11 +147,12 @@ class APIServerConnectionBase : public ProtoService {
|
||||
#endif
|
||||
|
||||
#ifdef USE_BLUETOOTH_PROXY
|
||||
virtual void on_subscribe_bluetooth_connections_free_request(){};
|
||||
virtual void on_subscribe_bluetooth_connections_free_request(const SubscribeBluetoothConnectionsFreeRequest &value){};
|
||||
#endif
|
||||
|
||||
#ifdef USE_BLUETOOTH_PROXY
|
||||
virtual void on_unsubscribe_bluetooth_le_advertisements_request(){};
|
||||
virtual void on_unsubscribe_bluetooth_le_advertisements_request(
|
||||
const UnsubscribeBluetoothLEAdvertisementsRequest &value){};
|
||||
#endif
|
||||
|
||||
#ifdef USE_BLUETOOTH_PROXY
|
||||
@@ -228,4 +228,272 @@ class APIServerConnectionBase : public ProtoService {
|
||||
void read_message(uint32_t msg_size, uint32_t msg_type, const uint8_t *msg_data) override;
|
||||
};
|
||||
|
||||
class APIServerConnection : public APIServerConnectionBase {
|
||||
public:
|
||||
virtual bool send_hello_response(const HelloRequest &msg) = 0;
|
||||
virtual bool send_disconnect_response(const DisconnectRequest &msg) = 0;
|
||||
virtual bool send_ping_response(const PingRequest &msg) = 0;
|
||||
virtual bool send_device_info_response(const DeviceInfoRequest &msg) = 0;
|
||||
virtual void list_entities(const ListEntitiesRequest &msg) = 0;
|
||||
virtual void subscribe_states(const SubscribeStatesRequest &msg) = 0;
|
||||
virtual void subscribe_logs(const SubscribeLogsRequest &msg) = 0;
|
||||
#ifdef USE_API_HOMEASSISTANT_SERVICES
|
||||
virtual void subscribe_homeassistant_services(const SubscribeHomeassistantServicesRequest &msg) = 0;
|
||||
#endif
|
||||
#ifdef USE_API_HOMEASSISTANT_STATES
|
||||
virtual void subscribe_home_assistant_states(const SubscribeHomeAssistantStatesRequest &msg) = 0;
|
||||
#endif
|
||||
#ifdef USE_API_USER_DEFINED_ACTIONS
|
||||
virtual void execute_service(const ExecuteServiceRequest &msg) = 0;
|
||||
#endif
|
||||
#ifdef USE_API_NOISE
|
||||
virtual bool send_noise_encryption_set_key_response(const NoiseEncryptionSetKeyRequest &msg) = 0;
|
||||
#endif
|
||||
#ifdef USE_BUTTON
|
||||
virtual void button_command(const ButtonCommandRequest &msg) = 0;
|
||||
#endif
|
||||
#ifdef USE_CAMERA
|
||||
virtual void camera_image(const CameraImageRequest &msg) = 0;
|
||||
#endif
|
||||
#ifdef USE_CLIMATE
|
||||
virtual void climate_command(const ClimateCommandRequest &msg) = 0;
|
||||
#endif
|
||||
#ifdef USE_COVER
|
||||
virtual void cover_command(const CoverCommandRequest &msg) = 0;
|
||||
#endif
|
||||
#ifdef USE_DATETIME_DATE
|
||||
virtual void date_command(const DateCommandRequest &msg) = 0;
|
||||
#endif
|
||||
#ifdef USE_DATETIME_DATETIME
|
||||
virtual void datetime_command(const DateTimeCommandRequest &msg) = 0;
|
||||
#endif
|
||||
#ifdef USE_FAN
|
||||
virtual void fan_command(const FanCommandRequest &msg) = 0;
|
||||
#endif
|
||||
#ifdef USE_LIGHT
|
||||
virtual void light_command(const LightCommandRequest &msg) = 0;
|
||||
#endif
|
||||
#ifdef USE_LOCK
|
||||
virtual void lock_command(const LockCommandRequest &msg) = 0;
|
||||
#endif
|
||||
#ifdef USE_MEDIA_PLAYER
|
||||
virtual void media_player_command(const MediaPlayerCommandRequest &msg) = 0;
|
||||
#endif
|
||||
#ifdef USE_NUMBER
|
||||
virtual void number_command(const NumberCommandRequest &msg) = 0;
|
||||
#endif
|
||||
#ifdef USE_SELECT
|
||||
virtual void select_command(const SelectCommandRequest &msg) = 0;
|
||||
#endif
|
||||
#ifdef USE_SIREN
|
||||
virtual void siren_command(const SirenCommandRequest &msg) = 0;
|
||||
#endif
|
||||
#ifdef USE_SWITCH
|
||||
virtual void switch_command(const SwitchCommandRequest &msg) = 0;
|
||||
#endif
|
||||
#ifdef USE_TEXT
|
||||
virtual void text_command(const TextCommandRequest &msg) = 0;
|
||||
#endif
|
||||
#ifdef USE_DATETIME_TIME
|
||||
virtual void time_command(const TimeCommandRequest &msg) = 0;
|
||||
#endif
|
||||
#ifdef USE_UPDATE
|
||||
virtual void update_command(const UpdateCommandRequest &msg) = 0;
|
||||
#endif
|
||||
#ifdef USE_VALVE
|
||||
virtual void valve_command(const ValveCommandRequest &msg) = 0;
|
||||
#endif
|
||||
#ifdef USE_WATER_HEATER
|
||||
virtual void water_heater_command(const WaterHeaterCommandRequest &msg) = 0;
|
||||
#endif
|
||||
#ifdef USE_BLUETOOTH_PROXY
|
||||
virtual void subscribe_bluetooth_le_advertisements(const SubscribeBluetoothLEAdvertisementsRequest &msg) = 0;
|
||||
#endif
|
||||
#ifdef USE_BLUETOOTH_PROXY
|
||||
virtual void bluetooth_device_request(const BluetoothDeviceRequest &msg) = 0;
|
||||
#endif
|
||||
#ifdef USE_BLUETOOTH_PROXY
|
||||
virtual void bluetooth_gatt_get_services(const BluetoothGATTGetServicesRequest &msg) = 0;
|
||||
#endif
|
||||
#ifdef USE_BLUETOOTH_PROXY
|
||||
virtual void bluetooth_gatt_read(const BluetoothGATTReadRequest &msg) = 0;
|
||||
#endif
|
||||
#ifdef USE_BLUETOOTH_PROXY
|
||||
virtual void bluetooth_gatt_write(const BluetoothGATTWriteRequest &msg) = 0;
|
||||
#endif
|
||||
#ifdef USE_BLUETOOTH_PROXY
|
||||
virtual void bluetooth_gatt_read_descriptor(const BluetoothGATTReadDescriptorRequest &msg) = 0;
|
||||
#endif
|
||||
#ifdef USE_BLUETOOTH_PROXY
|
||||
virtual void bluetooth_gatt_write_descriptor(const BluetoothGATTWriteDescriptorRequest &msg) = 0;
|
||||
#endif
|
||||
#ifdef USE_BLUETOOTH_PROXY
|
||||
virtual void bluetooth_gatt_notify(const BluetoothGATTNotifyRequest &msg) = 0;
|
||||
#endif
|
||||
#ifdef USE_BLUETOOTH_PROXY
|
||||
virtual bool send_subscribe_bluetooth_connections_free_response(
|
||||
const SubscribeBluetoothConnectionsFreeRequest &msg) = 0;
|
||||
#endif
|
||||
#ifdef USE_BLUETOOTH_PROXY
|
||||
virtual void unsubscribe_bluetooth_le_advertisements(const UnsubscribeBluetoothLEAdvertisementsRequest &msg) = 0;
|
||||
#endif
|
||||
#ifdef USE_BLUETOOTH_PROXY
|
||||
virtual void bluetooth_scanner_set_mode(const BluetoothScannerSetModeRequest &msg) = 0;
|
||||
#endif
|
||||
#ifdef USE_VOICE_ASSISTANT
|
||||
virtual void subscribe_voice_assistant(const SubscribeVoiceAssistantRequest &msg) = 0;
|
||||
#endif
|
||||
#ifdef USE_VOICE_ASSISTANT
|
||||
virtual bool send_voice_assistant_get_configuration_response(const VoiceAssistantConfigurationRequest &msg) = 0;
|
||||
#endif
|
||||
#ifdef USE_VOICE_ASSISTANT
|
||||
virtual void voice_assistant_set_configuration(const VoiceAssistantSetConfiguration &msg) = 0;
|
||||
#endif
|
||||
#ifdef USE_ALARM_CONTROL_PANEL
|
||||
virtual void alarm_control_panel_command(const AlarmControlPanelCommandRequest &msg) = 0;
|
||||
#endif
|
||||
#ifdef USE_ZWAVE_PROXY
|
||||
virtual void zwave_proxy_frame(const ZWaveProxyFrame &msg) = 0;
|
||||
#endif
|
||||
#ifdef USE_ZWAVE_PROXY
|
||||
virtual void zwave_proxy_request(const ZWaveProxyRequest &msg) = 0;
|
||||
#endif
|
||||
#ifdef USE_IR_RF
|
||||
virtual void infrared_rf_transmit_raw_timings(const InfraredRFTransmitRawTimingsRequest &msg) = 0;
|
||||
#endif
|
||||
protected:
|
||||
void on_hello_request(const HelloRequest &msg) override;
|
||||
void on_disconnect_request(const DisconnectRequest &msg) override;
|
||||
void on_ping_request(const PingRequest &msg) override;
|
||||
void on_device_info_request(const DeviceInfoRequest &msg) override;
|
||||
void on_list_entities_request(const ListEntitiesRequest &msg) override;
|
||||
void on_subscribe_states_request(const SubscribeStatesRequest &msg) override;
|
||||
void on_subscribe_logs_request(const SubscribeLogsRequest &msg) override;
|
||||
#ifdef USE_API_HOMEASSISTANT_SERVICES
|
||||
void on_subscribe_homeassistant_services_request(const SubscribeHomeassistantServicesRequest &msg) override;
|
||||
#endif
|
||||
#ifdef USE_API_HOMEASSISTANT_STATES
|
||||
void on_subscribe_home_assistant_states_request(const SubscribeHomeAssistantStatesRequest &msg) override;
|
||||
#endif
|
||||
#ifdef USE_API_USER_DEFINED_ACTIONS
|
||||
void on_execute_service_request(const ExecuteServiceRequest &msg) override;
|
||||
#endif
|
||||
#ifdef USE_API_NOISE
|
||||
void on_noise_encryption_set_key_request(const NoiseEncryptionSetKeyRequest &msg) override;
|
||||
#endif
|
||||
#ifdef USE_BUTTON
|
||||
void on_button_command_request(const ButtonCommandRequest &msg) override;
|
||||
#endif
|
||||
#ifdef USE_CAMERA
|
||||
void on_camera_image_request(const CameraImageRequest &msg) override;
|
||||
#endif
|
||||
#ifdef USE_CLIMATE
|
||||
void on_climate_command_request(const ClimateCommandRequest &msg) override;
|
||||
#endif
|
||||
#ifdef USE_COVER
|
||||
void on_cover_command_request(const CoverCommandRequest &msg) override;
|
||||
#endif
|
||||
#ifdef USE_DATETIME_DATE
|
||||
void on_date_command_request(const DateCommandRequest &msg) override;
|
||||
#endif
|
||||
#ifdef USE_DATETIME_DATETIME
|
||||
void on_date_time_command_request(const DateTimeCommandRequest &msg) override;
|
||||
#endif
|
||||
#ifdef USE_FAN
|
||||
void on_fan_command_request(const FanCommandRequest &msg) override;
|
||||
#endif
|
||||
#ifdef USE_LIGHT
|
||||
void on_light_command_request(const LightCommandRequest &msg) override;
|
||||
#endif
|
||||
#ifdef USE_LOCK
|
||||
void on_lock_command_request(const LockCommandRequest &msg) override;
|
||||
#endif
|
||||
#ifdef USE_MEDIA_PLAYER
|
||||
void on_media_player_command_request(const MediaPlayerCommandRequest &msg) override;
|
||||
#endif
|
||||
#ifdef USE_NUMBER
|
||||
void on_number_command_request(const NumberCommandRequest &msg) override;
|
||||
#endif
|
||||
#ifdef USE_SELECT
|
||||
void on_select_command_request(const SelectCommandRequest &msg) override;
|
||||
#endif
|
||||
#ifdef USE_SIREN
|
||||
void on_siren_command_request(const SirenCommandRequest &msg) override;
|
||||
#endif
|
||||
#ifdef USE_SWITCH
|
||||
void on_switch_command_request(const SwitchCommandRequest &msg) override;
|
||||
#endif
|
||||
#ifdef USE_TEXT
|
||||
void on_text_command_request(const TextCommandRequest &msg) override;
|
||||
#endif
|
||||
#ifdef USE_DATETIME_TIME
|
||||
void on_time_command_request(const TimeCommandRequest &msg) override;
|
||||
#endif
|
||||
#ifdef USE_UPDATE
|
||||
void on_update_command_request(const UpdateCommandRequest &msg) override;
|
||||
#endif
|
||||
#ifdef USE_VALVE
|
||||
void on_valve_command_request(const ValveCommandRequest &msg) override;
|
||||
#endif
|
||||
#ifdef USE_WATER_HEATER
|
||||
void on_water_heater_command_request(const WaterHeaterCommandRequest &msg) override;
|
||||
#endif
|
||||
#ifdef USE_BLUETOOTH_PROXY
|
||||
void on_subscribe_bluetooth_le_advertisements_request(const SubscribeBluetoothLEAdvertisementsRequest &msg) override;
|
||||
#endif
|
||||
#ifdef USE_BLUETOOTH_PROXY
|
||||
void on_bluetooth_device_request(const BluetoothDeviceRequest &msg) override;
|
||||
#endif
|
||||
#ifdef USE_BLUETOOTH_PROXY
|
||||
void on_bluetooth_gatt_get_services_request(const BluetoothGATTGetServicesRequest &msg) override;
|
||||
#endif
|
||||
#ifdef USE_BLUETOOTH_PROXY
|
||||
void on_bluetooth_gatt_read_request(const BluetoothGATTReadRequest &msg) override;
|
||||
#endif
|
||||
#ifdef USE_BLUETOOTH_PROXY
|
||||
void on_bluetooth_gatt_write_request(const BluetoothGATTWriteRequest &msg) override;
|
||||
#endif
|
||||
#ifdef USE_BLUETOOTH_PROXY
|
||||
void on_bluetooth_gatt_read_descriptor_request(const BluetoothGATTReadDescriptorRequest &msg) override;
|
||||
#endif
|
||||
#ifdef USE_BLUETOOTH_PROXY
|
||||
void on_bluetooth_gatt_write_descriptor_request(const BluetoothGATTWriteDescriptorRequest &msg) override;
|
||||
#endif
|
||||
#ifdef USE_BLUETOOTH_PROXY
|
||||
void on_bluetooth_gatt_notify_request(const BluetoothGATTNotifyRequest &msg) override;
|
||||
#endif
|
||||
#ifdef USE_BLUETOOTH_PROXY
|
||||
void on_subscribe_bluetooth_connections_free_request(const SubscribeBluetoothConnectionsFreeRequest &msg) override;
|
||||
#endif
|
||||
#ifdef USE_BLUETOOTH_PROXY
|
||||
void on_unsubscribe_bluetooth_le_advertisements_request(
|
||||
const UnsubscribeBluetoothLEAdvertisementsRequest &msg) override;
|
||||
#endif
|
||||
#ifdef USE_BLUETOOTH_PROXY
|
||||
void on_bluetooth_scanner_set_mode_request(const BluetoothScannerSetModeRequest &msg) override;
|
||||
#endif
|
||||
#ifdef USE_VOICE_ASSISTANT
|
||||
void on_subscribe_voice_assistant_request(const SubscribeVoiceAssistantRequest &msg) override;
|
||||
#endif
|
||||
#ifdef USE_VOICE_ASSISTANT
|
||||
void on_voice_assistant_configuration_request(const VoiceAssistantConfigurationRequest &msg) override;
|
||||
#endif
|
||||
#ifdef USE_VOICE_ASSISTANT
|
||||
void on_voice_assistant_set_configuration(const VoiceAssistantSetConfiguration &msg) override;
|
||||
#endif
|
||||
#ifdef USE_ALARM_CONTROL_PANEL
|
||||
void on_alarm_control_panel_command_request(const AlarmControlPanelCommandRequest &msg) override;
|
||||
#endif
|
||||
#ifdef USE_ZWAVE_PROXY
|
||||
void on_z_wave_proxy_frame(const ZWaveProxyFrame &msg) override;
|
||||
#endif
|
||||
#ifdef USE_ZWAVE_PROXY
|
||||
void on_z_wave_proxy_request(const ZWaveProxyRequest &msg) override;
|
||||
#endif
|
||||
#ifdef USE_IR_RF
|
||||
void on_infrared_rf_transmit_raw_timings_request(const InfraredRFTransmitRawTimingsRequest &msg) override;
|
||||
#endif
|
||||
void read_message(uint32_t msg_size, uint32_t msg_type, const uint8_t *msg_data) override;
|
||||
};
|
||||
|
||||
} // namespace esphome::api
|
||||
|
||||
@@ -25,9 +25,7 @@ template<typename... X> class TemplatableStringValue : public TemplatableValue<s
|
||||
|
||||
private:
|
||||
// Helper to convert value to string - handles the case where value is already a string
|
||||
template<typename T> static std::string value_to_string(T &&val) {
|
||||
return to_string(std::forward<T>(val)); // NOLINT
|
||||
}
|
||||
template<typename T> static std::string value_to_string(T &&val) { return to_string(std::forward<T>(val)); }
|
||||
|
||||
// Overloads for string types - needed because std::to_string doesn't support them
|
||||
static std::string value_to_string(char *val) {
|
||||
|
||||
@@ -112,12 +112,8 @@ class ProtoVarInt {
|
||||
uint64_t result = buffer[0] & 0x7F;
|
||||
uint8_t bitpos = 7;
|
||||
|
||||
// A 64-bit varint is at most 10 bytes (ceil(64/7)). Reject overlong encodings
|
||||
// to avoid undefined behavior from shifting uint64_t by >= 64 bits.
|
||||
uint32_t max_len = std::min(len, uint32_t(10));
|
||||
|
||||
// Start from the second byte since we've already processed the first
|
||||
for (uint32_t i = 1; i < max_len; i++) {
|
||||
for (uint32_t i = 1; i < len; i++) {
|
||||
uint8_t val = buffer[i];
|
||||
result |= uint64_t(val & 0x7F) << uint64_t(bitpos);
|
||||
bitpos += 7;
|
||||
|
||||
@@ -5,14 +5,6 @@ namespace esphome::binary_sensor {
|
||||
|
||||
static const char *const TAG = "binary_sensor.automation";
|
||||
|
||||
// MultiClickTrigger timeout IDs.
|
||||
// MultiClickTrigger is its own Component instance, so the scheduler scopes
|
||||
// IDs by component pointer — no risk of collisions between instances.
|
||||
constexpr uint32_t MULTICLICK_TRIGGER_ID = 0;
|
||||
constexpr uint32_t MULTICLICK_COOLDOWN_ID = 1;
|
||||
constexpr uint32_t MULTICLICK_IS_VALID_ID = 2;
|
||||
constexpr uint32_t MULTICLICK_IS_NOT_VALID_ID = 3;
|
||||
|
||||
void MultiClickTrigger::on_state_(bool state) {
|
||||
// Handle duplicate events
|
||||
if (state == this->last_state_) {
|
||||
@@ -35,7 +27,7 @@ void MultiClickTrigger::on_state_(bool state) {
|
||||
evt.min_length, evt.max_length);
|
||||
this->at_index_ = 1;
|
||||
if (this->timing_.size() == 1 && evt.max_length == 4294967294UL) {
|
||||
this->set_timeout(MULTICLICK_TRIGGER_ID, evt.min_length, [this]() { this->trigger_(); });
|
||||
this->set_timeout("trigger", evt.min_length, [this]() { this->trigger_(); });
|
||||
} else {
|
||||
this->schedule_is_valid_(evt.min_length);
|
||||
this->schedule_is_not_valid_(evt.max_length);
|
||||
@@ -65,13 +57,13 @@ void MultiClickTrigger::on_state_(bool state) {
|
||||
this->schedule_is_not_valid_(evt.max_length);
|
||||
} else if (*this->at_index_ + 1 != this->timing_.size()) {
|
||||
ESP_LOGV(TAG, "B i=%zu min=%" PRIu32, *this->at_index_, evt.min_length); // NOLINT
|
||||
this->cancel_timeout(MULTICLICK_IS_NOT_VALID_ID);
|
||||
this->cancel_timeout("is_not_valid");
|
||||
this->schedule_is_valid_(evt.min_length);
|
||||
} else {
|
||||
ESP_LOGV(TAG, "C i=%zu min=%" PRIu32, *this->at_index_, evt.min_length); // NOLINT
|
||||
this->is_valid_ = false;
|
||||
this->cancel_timeout(MULTICLICK_IS_NOT_VALID_ID);
|
||||
this->set_timeout(MULTICLICK_TRIGGER_ID, evt.min_length, [this]() { this->trigger_(); });
|
||||
this->cancel_timeout("is_not_valid");
|
||||
this->set_timeout("trigger", evt.min_length, [this]() { this->trigger_(); });
|
||||
}
|
||||
|
||||
*this->at_index_ = *this->at_index_ + 1;
|
||||
@@ -79,14 +71,14 @@ void MultiClickTrigger::on_state_(bool state) {
|
||||
void MultiClickTrigger::schedule_cooldown_() {
|
||||
ESP_LOGV(TAG, "Multi Click: Invalid length of press, starting cooldown of %" PRIu32 " ms", this->invalid_cooldown_);
|
||||
this->is_in_cooldown_ = true;
|
||||
this->set_timeout(MULTICLICK_COOLDOWN_ID, this->invalid_cooldown_, [this]() {
|
||||
this->set_timeout("cooldown", this->invalid_cooldown_, [this]() {
|
||||
ESP_LOGV(TAG, "Multi Click: Cooldown ended, matching is now enabled again.");
|
||||
this->is_in_cooldown_ = false;
|
||||
});
|
||||
this->at_index_.reset();
|
||||
this->cancel_timeout(MULTICLICK_TRIGGER_ID);
|
||||
this->cancel_timeout(MULTICLICK_IS_VALID_ID);
|
||||
this->cancel_timeout(MULTICLICK_IS_NOT_VALID_ID);
|
||||
this->cancel_timeout("trigger");
|
||||
this->cancel_timeout("is_valid");
|
||||
this->cancel_timeout("is_not_valid");
|
||||
}
|
||||
void MultiClickTrigger::schedule_is_valid_(uint32_t min_length) {
|
||||
if (min_length == 0) {
|
||||
@@ -94,13 +86,13 @@ void MultiClickTrigger::schedule_is_valid_(uint32_t min_length) {
|
||||
return;
|
||||
}
|
||||
this->is_valid_ = false;
|
||||
this->set_timeout(MULTICLICK_IS_VALID_ID, min_length, [this]() {
|
||||
this->set_timeout("is_valid", min_length, [this]() {
|
||||
ESP_LOGV(TAG, "Multi Click: You can now %s the button.", this->parent_->state ? "RELEASE" : "PRESS");
|
||||
this->is_valid_ = true;
|
||||
});
|
||||
}
|
||||
void MultiClickTrigger::schedule_is_not_valid_(uint32_t max_length) {
|
||||
this->set_timeout(MULTICLICK_IS_NOT_VALID_ID, max_length, [this]() {
|
||||
this->set_timeout("is_not_valid", max_length, [this]() {
|
||||
ESP_LOGV(TAG, "Multi Click: You waited too long to %s.", this->parent_->state ? "RELEASE" : "PRESS");
|
||||
this->is_valid_ = false;
|
||||
this->schedule_cooldown_();
|
||||
@@ -114,9 +106,9 @@ void MultiClickTrigger::cancel() {
|
||||
void MultiClickTrigger::trigger_() {
|
||||
ESP_LOGV(TAG, "Multi Click: Hooray, multi click is valid. Triggering!");
|
||||
this->at_index_.reset();
|
||||
this->cancel_timeout(MULTICLICK_TRIGGER_ID);
|
||||
this->cancel_timeout(MULTICLICK_IS_VALID_ID);
|
||||
this->cancel_timeout(MULTICLICK_IS_NOT_VALID_ID);
|
||||
this->cancel_timeout("trigger");
|
||||
this->cancel_timeout("is_valid");
|
||||
this->cancel_timeout("is_not_valid");
|
||||
this->trigger();
|
||||
}
|
||||
|
||||
|
||||
@@ -6,14 +6,6 @@ namespace esphome::binary_sensor {
|
||||
|
||||
static const char *const TAG = "sensor.filter";
|
||||
|
||||
// Timeout IDs for filter classes.
|
||||
// Each filter is its own Component instance, so the scheduler scopes
|
||||
// IDs by component pointer — no risk of collisions between instances.
|
||||
constexpr uint32_t FILTER_TIMEOUT_ID = 0;
|
||||
// AutorepeatFilter needs two distinct IDs (both timeouts on the same component)
|
||||
constexpr uint32_t AUTOREPEAT_TIMING_ID = 0;
|
||||
constexpr uint32_t AUTOREPEAT_ON_OFF_ID = 1;
|
||||
|
||||
void Filter::output(bool value) {
|
||||
if (this->next_ == nullptr) {
|
||||
this->parent_->send_state_internal(value);
|
||||
@@ -31,16 +23,16 @@ void Filter::input(bool value) {
|
||||
}
|
||||
|
||||
void TimeoutFilter::input(bool value) {
|
||||
this->set_timeout(FILTER_TIMEOUT_ID, this->timeout_delay_.value(), [this]() { this->parent_->invalidate_state(); });
|
||||
this->set_timeout("timeout", this->timeout_delay_.value(), [this]() { this->parent_->invalidate_state(); });
|
||||
// we do not de-dup here otherwise changes from invalid to valid state will not be output
|
||||
this->output(value);
|
||||
}
|
||||
|
||||
optional<bool> DelayedOnOffFilter::new_value(bool value) {
|
||||
if (value) {
|
||||
this->set_timeout(FILTER_TIMEOUT_ID, this->on_delay_.value(), [this]() { this->output(true); });
|
||||
this->set_timeout("ON_OFF", this->on_delay_.value(), [this]() { this->output(true); });
|
||||
} else {
|
||||
this->set_timeout(FILTER_TIMEOUT_ID, this->off_delay_.value(), [this]() { this->output(false); });
|
||||
this->set_timeout("ON_OFF", this->off_delay_.value(), [this]() { this->output(false); });
|
||||
}
|
||||
return {};
|
||||
}
|
||||
@@ -49,10 +41,10 @@ float DelayedOnOffFilter::get_setup_priority() const { return setup_priority::HA
|
||||
|
||||
optional<bool> DelayedOnFilter::new_value(bool value) {
|
||||
if (value) {
|
||||
this->set_timeout(FILTER_TIMEOUT_ID, this->delay_.value(), [this]() { this->output(true); });
|
||||
this->set_timeout("ON", this->delay_.value(), [this]() { this->output(true); });
|
||||
return {};
|
||||
} else {
|
||||
this->cancel_timeout(FILTER_TIMEOUT_ID);
|
||||
this->cancel_timeout("ON");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -61,10 +53,10 @@ float DelayedOnFilter::get_setup_priority() const { return setup_priority::HARDW
|
||||
|
||||
optional<bool> DelayedOffFilter::new_value(bool value) {
|
||||
if (!value) {
|
||||
this->set_timeout(FILTER_TIMEOUT_ID, this->delay_.value(), [this]() { this->output(false); });
|
||||
this->set_timeout("OFF", this->delay_.value(), [this]() { this->output(false); });
|
||||
return {};
|
||||
} else {
|
||||
this->cancel_timeout(FILTER_TIMEOUT_ID);
|
||||
this->cancel_timeout("OFF");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -84,8 +76,8 @@ optional<bool> AutorepeatFilter::new_value(bool value) {
|
||||
this->next_timing_();
|
||||
return true;
|
||||
} else {
|
||||
this->cancel_timeout(AUTOREPEAT_TIMING_ID);
|
||||
this->cancel_timeout(AUTOREPEAT_ON_OFF_ID);
|
||||
this->cancel_timeout("TIMING");
|
||||
this->cancel_timeout("ON_OFF");
|
||||
this->active_timing_ = 0;
|
||||
return false;
|
||||
}
|
||||
@@ -96,10 +88,8 @@ void AutorepeatFilter::next_timing_() {
|
||||
// 1st time: starts waiting the first delay
|
||||
// 2nd time: starts waiting the second delay and starts toggling with the first time_off / _on
|
||||
// last time: no delay to start but have to bump the index to reflect the last
|
||||
if (this->active_timing_ < this->timings_.size()) {
|
||||
this->set_timeout(AUTOREPEAT_TIMING_ID, this->timings_[this->active_timing_].delay,
|
||||
[this]() { this->next_timing_(); });
|
||||
}
|
||||
if (this->active_timing_ < this->timings_.size())
|
||||
this->set_timeout("TIMING", this->timings_[this->active_timing_].delay, [this]() { this->next_timing_(); });
|
||||
|
||||
if (this->active_timing_ <= this->timings_.size()) {
|
||||
this->active_timing_++;
|
||||
@@ -114,8 +104,7 @@ void AutorepeatFilter::next_timing_() {
|
||||
void AutorepeatFilter::next_value_(bool val) {
|
||||
const AutorepeatFilterTiming &timing = this->timings_[this->active_timing_ - 2];
|
||||
this->output(val); // This is at least the second one so not initial
|
||||
this->set_timeout(AUTOREPEAT_ON_OFF_ID, val ? timing.time_on : timing.time_off,
|
||||
[this, val]() { this->next_value_(!val); });
|
||||
this->set_timeout("ON_OFF", val ? timing.time_on : timing.time_off, [this, val]() { this->next_value_(!val); });
|
||||
}
|
||||
|
||||
float AutorepeatFilter::get_setup_priority() const { return setup_priority::HARDWARE; }
|
||||
@@ -126,7 +115,7 @@ optional<bool> LambdaFilter::new_value(bool value) { return this->f_(value); }
|
||||
|
||||
optional<bool> SettleFilter::new_value(bool value) {
|
||||
if (!this->steady_) {
|
||||
this->set_timeout(FILTER_TIMEOUT_ID, this->delay_.value(), [this, value]() {
|
||||
this->set_timeout("SETTLE", this->delay_.value(), [this, value]() {
|
||||
this->steady_ = true;
|
||||
this->output(value);
|
||||
});
|
||||
@@ -134,7 +123,7 @@ optional<bool> SettleFilter::new_value(bool value) {
|
||||
} else {
|
||||
this->steady_ = false;
|
||||
this->output(value);
|
||||
this->set_timeout(FILTER_TIMEOUT_ID, this->delay_.value(), [this]() { this->steady_ = true; });
|
||||
this->set_timeout("SETTLE", this->delay_.value(), [this]() { this->steady_ = true; });
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ namespace esphome {
|
||||
namespace cse7766 {
|
||||
|
||||
static const char *const TAG = "cse7766";
|
||||
static constexpr size_t CSE7766_RAW_DATA_SIZE = 24;
|
||||
|
||||
void CSE7766Component::loop() {
|
||||
const uint32_t now = App.get_loop_component_start_time();
|
||||
@@ -15,39 +16,25 @@ void CSE7766Component::loop() {
|
||||
this->raw_data_index_ = 0;
|
||||
}
|
||||
|
||||
// Early return prevents updating last_transmission_ when no data is available.
|
||||
int avail = this->available();
|
||||
if (avail <= 0) {
|
||||
if (this->available() == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
this->last_transmission_ = now;
|
||||
|
||||
// Read all available bytes in batches to reduce UART call overhead.
|
||||
// At 4800 baud (~480 bytes/sec) with ~122 Hz loop rate, typically ~4 bytes per call.
|
||||
uint8_t buf[CSE7766_RAW_DATA_SIZE];
|
||||
while (avail > 0) {
|
||||
size_t to_read = std::min(static_cast<size_t>(avail), sizeof(buf));
|
||||
if (!this->read_array(buf, to_read)) {
|
||||
break;
|
||||
while (this->available() != 0) {
|
||||
this->read_byte(&this->raw_data_[this->raw_data_index_]);
|
||||
if (!this->check_byte_()) {
|
||||
this->raw_data_index_ = 0;
|
||||
this->status_set_warning();
|
||||
continue;
|
||||
}
|
||||
avail -= to_read;
|
||||
|
||||
for (size_t i = 0; i < to_read; i++) {
|
||||
this->raw_data_[this->raw_data_index_] = buf[i];
|
||||
if (!this->check_byte_()) {
|
||||
this->raw_data_index_ = 0;
|
||||
this->status_set_warning();
|
||||
continue;
|
||||
}
|
||||
|
||||
if (this->raw_data_index_ == CSE7766_RAW_DATA_SIZE - 1) {
|
||||
this->parse_data_();
|
||||
this->status_clear_warning();
|
||||
}
|
||||
|
||||
this->raw_data_index_ = (this->raw_data_index_ + 1) % CSE7766_RAW_DATA_SIZE;
|
||||
if (this->raw_data_index_ == 23) {
|
||||
this->parse_data_();
|
||||
this->status_clear_warning();
|
||||
}
|
||||
|
||||
this->raw_data_index_ = (this->raw_data_index_ + 1) % 24;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,15 +53,14 @@ bool CSE7766Component::check_byte_() {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (index == CSE7766_RAW_DATA_SIZE - 1) {
|
||||
if (index == 23) {
|
||||
uint8_t checksum = 0;
|
||||
for (uint8_t i = 2; i < CSE7766_RAW_DATA_SIZE - 1; i++) {
|
||||
for (uint8_t i = 2; i < 23; i++) {
|
||||
checksum += this->raw_data_[i];
|
||||
}
|
||||
|
||||
if (checksum != this->raw_data_[CSE7766_RAW_DATA_SIZE - 1]) {
|
||||
ESP_LOGW(TAG, "Invalid checksum from CSE7766: 0x%02X != 0x%02X", checksum,
|
||||
this->raw_data_[CSE7766_RAW_DATA_SIZE - 1]);
|
||||
if (checksum != this->raw_data_[23]) {
|
||||
ESP_LOGW(TAG, "Invalid checksum from CSE7766: 0x%02X != 0x%02X", checksum, this->raw_data_[23]);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
|
||||
@@ -8,8 +8,6 @@
|
||||
namespace esphome {
|
||||
namespace cse7766 {
|
||||
|
||||
static constexpr size_t CSE7766_RAW_DATA_SIZE = 24;
|
||||
|
||||
class CSE7766Component : public Component, public uart::UARTDevice {
|
||||
public:
|
||||
void set_voltage_sensor(sensor::Sensor *voltage_sensor) { voltage_sensor_ = voltage_sensor; }
|
||||
@@ -35,7 +33,7 @@ class CSE7766Component : public Component, public uart::UARTDevice {
|
||||
this->raw_data_[start_index + 2]);
|
||||
}
|
||||
|
||||
uint8_t raw_data_[CSE7766_RAW_DATA_SIZE];
|
||||
uint8_t raw_data_[24];
|
||||
uint8_t raw_data_index_{0};
|
||||
uint32_t last_transmission_{0};
|
||||
sensor::Sensor *voltage_sensor_{nullptr};
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
#include "dfplayer.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome {
|
||||
@@ -132,149 +131,140 @@ void DFPlayer::send_cmd_(uint8_t cmd, uint16_t argument) {
|
||||
}
|
||||
|
||||
void DFPlayer::loop() {
|
||||
// Read all available bytes in batches to reduce UART call overhead.
|
||||
int avail = this->available();
|
||||
uint8_t buf[64];
|
||||
while (avail > 0) {
|
||||
size_t to_read = std::min(static_cast<size_t>(avail), sizeof(buf));
|
||||
if (!this->read_array(buf, to_read)) {
|
||||
break;
|
||||
}
|
||||
avail -= to_read;
|
||||
for (size_t bi = 0; bi < to_read; bi++) {
|
||||
uint8_t byte = buf[bi];
|
||||
// Read message
|
||||
while (this->available()) {
|
||||
uint8_t byte;
|
||||
this->read_byte(&byte);
|
||||
|
||||
if (this->read_pos_ == DFPLAYER_READ_BUFFER_LENGTH)
|
||||
this->read_pos_ = 0;
|
||||
if (this->read_pos_ == DFPLAYER_READ_BUFFER_LENGTH)
|
||||
this->read_pos_ = 0;
|
||||
|
||||
switch (this->read_pos_) {
|
||||
case 0: // Start mark
|
||||
if (byte != 0x7E)
|
||||
continue;
|
||||
break;
|
||||
case 1: // Version
|
||||
if (byte != 0xFF) {
|
||||
ESP_LOGW(TAG, "Expected Version 0xFF, got %#02x", byte);
|
||||
this->read_pos_ = 0;
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
case 2: // Buffer length
|
||||
if (byte != 0x06) {
|
||||
ESP_LOGW(TAG, "Expected Buffer length 0x06, got %#02x", byte);
|
||||
this->read_pos_ = 0;
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
case 9: // End byte
|
||||
#ifdef ESPHOME_LOG_HAS_VERY_VERBOSE
|
||||
char byte_sequence[100];
|
||||
byte_sequence[0] = '\0';
|
||||
for (size_t i = 0; i < this->read_pos_ + 1; ++i) {
|
||||
snprintf(byte_sequence + strlen(byte_sequence), sizeof(byte_sequence) - strlen(byte_sequence), "%02X ",
|
||||
this->read_buffer_[i]);
|
||||
}
|
||||
ESP_LOGVV(TAG, "Received byte sequence: %s", byte_sequence);
|
||||
#endif
|
||||
if (byte != 0xEF) {
|
||||
ESP_LOGW(TAG, "Expected end byte 0xEF, got %#02x", byte);
|
||||
this->read_pos_ = 0;
|
||||
continue;
|
||||
}
|
||||
// Parse valid received command
|
||||
uint8_t cmd = this->read_buffer_[3];
|
||||
uint16_t argument = (this->read_buffer_[5] << 8) | this->read_buffer_[6];
|
||||
|
||||
ESP_LOGV(TAG, "Received message cmd: %#02x arg %#04x", cmd, argument);
|
||||
|
||||
switch (cmd) {
|
||||
case 0x3A:
|
||||
if (argument == 1) {
|
||||
ESP_LOGI(TAG, "USB loaded");
|
||||
} else if (argument == 2) {
|
||||
ESP_LOGI(TAG, "TF Card loaded");
|
||||
}
|
||||
break;
|
||||
case 0x3B:
|
||||
if (argument == 1) {
|
||||
ESP_LOGI(TAG, "USB unloaded");
|
||||
} else if (argument == 2) {
|
||||
ESP_LOGI(TAG, "TF Card unloaded");
|
||||
}
|
||||
break;
|
||||
case 0x3F:
|
||||
if (argument == 1) {
|
||||
ESP_LOGI(TAG, "USB available");
|
||||
} else if (argument == 2) {
|
||||
ESP_LOGI(TAG, "TF Card available");
|
||||
} else if (argument == 3) {
|
||||
ESP_LOGI(TAG, "USB, TF Card available");
|
||||
}
|
||||
break;
|
||||
case 0x40:
|
||||
ESP_LOGV(TAG, "Nack");
|
||||
this->ack_set_is_playing_ = false;
|
||||
this->ack_reset_is_playing_ = false;
|
||||
switch (argument) {
|
||||
case 0x01:
|
||||
ESP_LOGE(TAG, "Module is busy or uninitialized");
|
||||
break;
|
||||
case 0x02:
|
||||
ESP_LOGE(TAG, "Module is in sleep mode");
|
||||
break;
|
||||
case 0x03:
|
||||
ESP_LOGE(TAG, "Serial receive error");
|
||||
break;
|
||||
case 0x04:
|
||||
ESP_LOGE(TAG, "Checksum incorrect");
|
||||
break;
|
||||
case 0x05:
|
||||
ESP_LOGE(TAG, "Specified track is out of current track scope");
|
||||
this->is_playing_ = false;
|
||||
break;
|
||||
case 0x06:
|
||||
ESP_LOGE(TAG, "Specified track is not found");
|
||||
this->is_playing_ = false;
|
||||
break;
|
||||
case 0x07:
|
||||
ESP_LOGE(TAG,
|
||||
"Insertion error (an inserting operation only can be done when a track is being played)");
|
||||
break;
|
||||
case 0x08:
|
||||
ESP_LOGE(TAG, "SD card reading failed (SD card pulled out or damaged)");
|
||||
break;
|
||||
case 0x09:
|
||||
ESP_LOGE(TAG, "Entered into sleep mode");
|
||||
this->is_playing_ = false;
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case 0x41:
|
||||
ESP_LOGV(TAG, "Ack ok");
|
||||
this->is_playing_ |= this->ack_set_is_playing_;
|
||||
this->is_playing_ &= !this->ack_reset_is_playing_;
|
||||
this->ack_set_is_playing_ = false;
|
||||
this->ack_reset_is_playing_ = false;
|
||||
break;
|
||||
case 0x3C:
|
||||
ESP_LOGV(TAG, "Playback finished (USB drive)");
|
||||
this->is_playing_ = false;
|
||||
this->on_finished_playback_callback_.call();
|
||||
case 0x3D:
|
||||
ESP_LOGV(TAG, "Playback finished (SD card)");
|
||||
this->is_playing_ = false;
|
||||
this->on_finished_playback_callback_.call();
|
||||
break;
|
||||
default:
|
||||
ESP_LOGE(TAG, "Received unknown cmd %#02x arg %#04x", cmd, argument);
|
||||
}
|
||||
this->sent_cmd_ = 0;
|
||||
switch (this->read_pos_) {
|
||||
case 0: // Start mark
|
||||
if (byte != 0x7E)
|
||||
continue;
|
||||
break;
|
||||
case 1: // Version
|
||||
if (byte != 0xFF) {
|
||||
ESP_LOGW(TAG, "Expected Version 0xFF, got %#02x", byte);
|
||||
this->read_pos_ = 0;
|
||||
continue;
|
||||
}
|
||||
this->read_buffer_[this->read_pos_] = byte;
|
||||
this->read_pos_++;
|
||||
}
|
||||
break;
|
||||
case 2: // Buffer length
|
||||
if (byte != 0x06) {
|
||||
ESP_LOGW(TAG, "Expected Buffer length 0x06, got %#02x", byte);
|
||||
this->read_pos_ = 0;
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
case 9: // End byte
|
||||
#ifdef ESPHOME_LOG_HAS_VERY_VERBOSE
|
||||
char byte_sequence[100];
|
||||
byte_sequence[0] = '\0';
|
||||
for (size_t i = 0; i < this->read_pos_ + 1; ++i) {
|
||||
snprintf(byte_sequence + strlen(byte_sequence), sizeof(byte_sequence) - strlen(byte_sequence), "%02X ",
|
||||
this->read_buffer_[i]);
|
||||
}
|
||||
ESP_LOGVV(TAG, "Received byte sequence: %s", byte_sequence);
|
||||
#endif
|
||||
if (byte != 0xEF) {
|
||||
ESP_LOGW(TAG, "Expected end byte 0xEF, got %#02x", byte);
|
||||
this->read_pos_ = 0;
|
||||
continue;
|
||||
}
|
||||
// Parse valid received command
|
||||
uint8_t cmd = this->read_buffer_[3];
|
||||
uint16_t argument = (this->read_buffer_[5] << 8) | this->read_buffer_[6];
|
||||
|
||||
ESP_LOGV(TAG, "Received message cmd: %#02x arg %#04x", cmd, argument);
|
||||
|
||||
switch (cmd) {
|
||||
case 0x3A:
|
||||
if (argument == 1) {
|
||||
ESP_LOGI(TAG, "USB loaded");
|
||||
} else if (argument == 2) {
|
||||
ESP_LOGI(TAG, "TF Card loaded");
|
||||
}
|
||||
break;
|
||||
case 0x3B:
|
||||
if (argument == 1) {
|
||||
ESP_LOGI(TAG, "USB unloaded");
|
||||
} else if (argument == 2) {
|
||||
ESP_LOGI(TAG, "TF Card unloaded");
|
||||
}
|
||||
break;
|
||||
case 0x3F:
|
||||
if (argument == 1) {
|
||||
ESP_LOGI(TAG, "USB available");
|
||||
} else if (argument == 2) {
|
||||
ESP_LOGI(TAG, "TF Card available");
|
||||
} else if (argument == 3) {
|
||||
ESP_LOGI(TAG, "USB, TF Card available");
|
||||
}
|
||||
break;
|
||||
case 0x40:
|
||||
ESP_LOGV(TAG, "Nack");
|
||||
this->ack_set_is_playing_ = false;
|
||||
this->ack_reset_is_playing_ = false;
|
||||
switch (argument) {
|
||||
case 0x01:
|
||||
ESP_LOGE(TAG, "Module is busy or uninitialized");
|
||||
break;
|
||||
case 0x02:
|
||||
ESP_LOGE(TAG, "Module is in sleep mode");
|
||||
break;
|
||||
case 0x03:
|
||||
ESP_LOGE(TAG, "Serial receive error");
|
||||
break;
|
||||
case 0x04:
|
||||
ESP_LOGE(TAG, "Checksum incorrect");
|
||||
break;
|
||||
case 0x05:
|
||||
ESP_LOGE(TAG, "Specified track is out of current track scope");
|
||||
this->is_playing_ = false;
|
||||
break;
|
||||
case 0x06:
|
||||
ESP_LOGE(TAG, "Specified track is not found");
|
||||
this->is_playing_ = false;
|
||||
break;
|
||||
case 0x07:
|
||||
ESP_LOGE(TAG, "Insertion error (an inserting operation only can be done when a track is being played)");
|
||||
break;
|
||||
case 0x08:
|
||||
ESP_LOGE(TAG, "SD card reading failed (SD card pulled out or damaged)");
|
||||
break;
|
||||
case 0x09:
|
||||
ESP_LOGE(TAG, "Entered into sleep mode");
|
||||
this->is_playing_ = false;
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case 0x41:
|
||||
ESP_LOGV(TAG, "Ack ok");
|
||||
this->is_playing_ |= this->ack_set_is_playing_;
|
||||
this->is_playing_ &= !this->ack_reset_is_playing_;
|
||||
this->ack_set_is_playing_ = false;
|
||||
this->ack_reset_is_playing_ = false;
|
||||
break;
|
||||
case 0x3C:
|
||||
ESP_LOGV(TAG, "Playback finished (USB drive)");
|
||||
this->is_playing_ = false;
|
||||
this->on_finished_playback_callback_.call();
|
||||
case 0x3D:
|
||||
ESP_LOGV(TAG, "Playback finished (SD card)");
|
||||
this->is_playing_ = false;
|
||||
this->on_finished_playback_callback_.call();
|
||||
break;
|
||||
default:
|
||||
ESP_LOGE(TAG, "Received unknown cmd %#02x arg %#04x", cmd, argument);
|
||||
}
|
||||
this->sent_cmd_ = 0;
|
||||
this->read_pos_ = 0;
|
||||
continue;
|
||||
}
|
||||
this->read_buffer_[this->read_pos_] = byte;
|
||||
this->read_pos_++;
|
||||
}
|
||||
}
|
||||
void DFPlayer::dump_config() {
|
||||
|
||||
@@ -66,7 +66,7 @@ async def to_code(config):
|
||||
cg.add_build_flag("-DDSMR_WATER_MBUS_ID=" + str(config[CONF_WATER_MBUS_ID]))
|
||||
|
||||
# DSMR Parser
|
||||
cg.add_library("esphome/dsmr_parser", "1.1.0")
|
||||
cg.add_library("esphome/dsmr_parser", "1.0.0")
|
||||
|
||||
# Crypto
|
||||
cg.add_library("polargoose/Crypto-no-arduino", "0.4.0")
|
||||
|
||||
@@ -718,6 +718,14 @@ CONFIG_SCHEMA = cv.Schema(
|
||||
device_class=DEVICE_CLASS_POWER,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional("fw_core_version"): sensor.sensor_schema(
|
||||
accuracy_decimals=3,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional("fw_module_version"): sensor.sensor_schema(
|
||||
accuracy_decimals=3,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
}
|
||||
).extend(cv.COMPONENT_SCHEMA)
|
||||
|
||||
|
||||
@@ -26,9 +26,7 @@ CONFIG_SCHEMA = cv.Schema(
|
||||
cv.Optional("sub_equipment_id"): text_sensor.text_sensor_schema(),
|
||||
cv.Optional("gas_delivered_text"): text_sensor.text_sensor_schema(),
|
||||
cv.Optional("fw_core_checksum"): text_sensor.text_sensor_schema(),
|
||||
cv.Optional("fw_core_version"): text_sensor.text_sensor_schema(),
|
||||
cv.Optional("fw_module_checksum"): text_sensor.text_sensor_schema(),
|
||||
cv.Optional("fw_module_version"): text_sensor.text_sensor_schema(),
|
||||
cv.Optional("telegram"): text_sensor.text_sensor_schema().extend(
|
||||
{cv.Optional(CONF_INTERNAL, default=True): cv.boolean}
|
||||
),
|
||||
|
||||
@@ -55,6 +55,7 @@ void E131Component::setup() {
|
||||
}
|
||||
|
||||
void E131Component::loop() {
|
||||
std::vector<uint8_t> payload;
|
||||
E131Packet packet;
|
||||
int universe = 0;
|
||||
uint8_t buf[1460];
|
||||
@@ -63,9 +64,11 @@ void E131Component::loop() {
|
||||
if (len == -1) {
|
||||
return;
|
||||
}
|
||||
payload.resize(len);
|
||||
memmove(&payload[0], buf, len);
|
||||
|
||||
if (!this->packet_(buf, (size_t) len, universe, packet)) {
|
||||
ESP_LOGV(TAG, "Invalid packet received of size %zd.", len);
|
||||
if (!this->packet_(payload, universe, packet)) {
|
||||
ESP_LOGV(TAG, "Invalid packet received of size %zu.", payload.size());
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@ class E131Component : public esphome::Component {
|
||||
void set_method(E131ListenMethod listen_method) { this->listen_method_ = listen_method; }
|
||||
|
||||
protected:
|
||||
bool packet_(const uint8_t *data, size_t len, int &universe, E131Packet &packet);
|
||||
bool packet_(const std::vector<uint8_t> &data, int &universe, E131Packet &packet);
|
||||
bool process_(int universe, const E131Packet &packet);
|
||||
bool join_igmp_groups_();
|
||||
void join_(int universe);
|
||||
|
||||
@@ -116,11 +116,11 @@ void E131Component::leave_(int universe) {
|
||||
ESP_LOGD(TAG, "Left %d universe for E1.31.", universe);
|
||||
}
|
||||
|
||||
bool E131Component::packet_(const uint8_t *data, size_t len, int &universe, E131Packet &packet) {
|
||||
if (len < E131_MIN_PACKET_SIZE)
|
||||
bool E131Component::packet_(const std::vector<uint8_t> &data, int &universe, E131Packet &packet) {
|
||||
if (data.size() < E131_MIN_PACKET_SIZE)
|
||||
return false;
|
||||
|
||||
auto *sbuff = reinterpret_cast<const E131RawPacket *>(data);
|
||||
auto *sbuff = reinterpret_cast<const E131RawPacket *>(&data[0]);
|
||||
|
||||
if (memcmp(sbuff->acn_id, ACN_ID, sizeof(sbuff->acn_id)) != 0)
|
||||
return false;
|
||||
|
||||
@@ -1,67 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <algorithm>
|
||||
#include "esphome/core/color.h"
|
||||
|
||||
/* Utility for converting internal \a Color RGB representation to supported IC hardware color keys
|
||||
*
|
||||
* Focus in driver layer is on efficiency.
|
||||
* For optimum output quality on RGB inputs consider offline color keying/dithering.
|
||||
* Also see e.g. Image component.
|
||||
*/
|
||||
|
||||
namespace esphome::epaper_spi {
|
||||
|
||||
/** Delta for when to regard as gray */
|
||||
static constexpr uint8_t COLORCONV_GRAY_THRESHOLD = 50;
|
||||
|
||||
/** Map RGB color to discrete BWYR hex 4 color key
|
||||
*
|
||||
* @tparam NATIVE_COLOR Type of native hardware color values
|
||||
* @param color RGB color to convert from
|
||||
* @param hw_black Native value for black
|
||||
* @param hw_white Native value for white
|
||||
* @param hw_yellow Native value for yellow
|
||||
* @param hw_red Native value for red
|
||||
* @return Converted native hardware color value
|
||||
* @internal Constexpr. Does not depend on side effects ("pure").
|
||||
*/
|
||||
template<typename NATIVE_COLOR>
|
||||
constexpr NATIVE_COLOR color_to_bwyr(Color color, NATIVE_COLOR hw_black, NATIVE_COLOR hw_white, NATIVE_COLOR hw_yellow,
|
||||
NATIVE_COLOR hw_red) {
|
||||
// --- Step 1: Check for Grayscale (Black or White) ---
|
||||
// We define "grayscale" as a color where the min and max components
|
||||
// are close to each other.
|
||||
|
||||
const auto [min_rgb, max_rgb] = std::minmax({color.r, color.g, color.b});
|
||||
|
||||
if ((max_rgb - min_rgb) < COLORCONV_GRAY_THRESHOLD) {
|
||||
// It's a shade of gray. Map to BLACK or WHITE.
|
||||
// We split the luminance at the halfway point (382 = (255*3)/2)
|
||||
if ((static_cast<int>(color.r) + color.g + color.b) > 382) {
|
||||
return hw_white;
|
||||
}
|
||||
return hw_black;
|
||||
}
|
||||
|
||||
// --- Step 2: Check for Primary/Secondary Colors ---
|
||||
// If it's not gray, it's a color. We check which components are
|
||||
// "on" (over 128) vs "off". This divides the RGB cube into 8 corners.
|
||||
const bool r_on = (color.r > 128);
|
||||
const bool g_on = (color.g > 128);
|
||||
const bool b_on = (color.b > 128);
|
||||
|
||||
if (r_on) {
|
||||
if (!b_on) {
|
||||
return g_on ? hw_yellow : hw_red;
|
||||
}
|
||||
|
||||
// At least red+blue high (but not gray) -> White
|
||||
return hw_white;
|
||||
} else {
|
||||
return (b_on && g_on) ? hw_white : hw_black;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace esphome::epaper_spi
|
||||
@@ -1,227 +0,0 @@
|
||||
#include "epaper_spi_jd79660.h"
|
||||
#include "colorconv.h"
|
||||
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome::epaper_spi {
|
||||
static constexpr const char *const TAG = "epaper_spi.jd79660";
|
||||
|
||||
/** Pixel color as 2bpp. Must match IC LUT values. */
|
||||
enum JD79660Color : uint8_t {
|
||||
BLACK = 0b00,
|
||||
WHITE = 0b01,
|
||||
YELLOW = 0b10,
|
||||
RED = 0b11,
|
||||
};
|
||||
|
||||
/** Map RGB color to JD79660 BWYR hex color keys */
|
||||
static JD79660Color HOT color_to_hex(Color color) {
|
||||
return color_to_bwyr(color, JD79660Color::BLACK, JD79660Color::WHITE, JD79660Color::YELLOW, JD79660Color::RED);
|
||||
}
|
||||
|
||||
void EPaperJD79660::fill(Color color) {
|
||||
// If clipping is active, fall back to base implementation
|
||||
if (this->get_clipping().is_set()) {
|
||||
EPaperBase::fill(color);
|
||||
return;
|
||||
}
|
||||
|
||||
const auto pixel_color = color_to_hex(color);
|
||||
|
||||
// We store 4 pixels per byte
|
||||
this->buffer_.fill(pixel_color | (pixel_color << 2) | (pixel_color << 4) | (pixel_color << 6));
|
||||
}
|
||||
|
||||
void HOT EPaperJD79660::draw_pixel_at(int x, int y, Color color) {
|
||||
if (!this->rotate_coordinates_(x, y))
|
||||
return;
|
||||
const auto pixel_bits = color_to_hex(color);
|
||||
const uint32_t pixel_position = x + y * this->get_width_internal();
|
||||
// We store 4 pixels per byte at LSB offsets 6, 4, 2, 0
|
||||
const uint32_t byte_position = pixel_position / 4;
|
||||
const uint32_t bit_offset = 6 - ((pixel_position % 4) * 2);
|
||||
const auto original = this->buffer_[byte_position];
|
||||
|
||||
this->buffer_[byte_position] = (original & (~(0b11 << bit_offset))) | // mask old 2bpp
|
||||
(pixel_bits << bit_offset); // add new 2bpp
|
||||
}
|
||||
|
||||
bool EPaperJD79660::reset() {
|
||||
// On entry state RESET set step, next state will be RESET_END
|
||||
if (this->state_ == EPaperState::RESET) {
|
||||
this->step_ = FSMState::RESET_STEP0_H;
|
||||
}
|
||||
|
||||
switch (this->step_) {
|
||||
case FSMState::RESET_STEP0_H:
|
||||
// Step #0: Reset H for some settle time.
|
||||
|
||||
ESP_LOGVV(TAG, "reset #0");
|
||||
this->reset_pin_->digital_write(true);
|
||||
|
||||
this->reset_duration_ = SLEEP_MS_RESET0;
|
||||
this->step_ = FSMState::RESET_STEP1_L;
|
||||
return false; // another loop: step #1 below
|
||||
|
||||
case FSMState::RESET_STEP1_L:
|
||||
// Step #1: Reset L pulse for slightly >1.5ms.
|
||||
// This is actual reset trigger.
|
||||
|
||||
ESP_LOGVV(TAG, "reset #1");
|
||||
|
||||
// As commented on SLEEP_MS_RESET1: Reset pulse must happen within time window.
|
||||
// So do not use FSM loop, and avoid other calls/logs during pulse below.
|
||||
this->reset_pin_->digital_write(false);
|
||||
delay(SLEEP_MS_RESET1);
|
||||
this->reset_pin_->digital_write(true);
|
||||
|
||||
this->reset_duration_ = SLEEP_MS_RESET2;
|
||||
this->step_ = FSMState::RESET_STEP2_IDLECHECK;
|
||||
return false; // another loop: step #2 below
|
||||
|
||||
case FSMState::RESET_STEP2_IDLECHECK:
|
||||
// Step #2: Basically finished. Check sanity, and move FSM to INITIALISE state
|
||||
ESP_LOGVV(TAG, "reset #2");
|
||||
|
||||
if (!this->is_idle_()) {
|
||||
// Expectation: Idle after reset + settle time.
|
||||
// Improperly connected/unexpected hardware?
|
||||
// Error path reproducable e.g. with disconnected VDD/... pins
|
||||
// (optimally while busy_pin configured with local pulldown).
|
||||
// -> Mark failed to avoid followup problems.
|
||||
this->mark_failed(LOG_STR("Busy after reset"));
|
||||
}
|
||||
break; // End state loop below
|
||||
|
||||
default:
|
||||
// Unexpected step = bug?
|
||||
this->mark_failed();
|
||||
}
|
||||
|
||||
this->step_ = FSMState::INIT_STEP0_REGULARINIT; // reset for initialize state
|
||||
return true;
|
||||
}
|
||||
|
||||
bool EPaperJD79660::initialise(bool partial) {
|
||||
switch (this->step_) {
|
||||
case FSMState::INIT_STEP0_REGULARINIT:
|
||||
// Step #0: Regular init sequence
|
||||
ESP_LOGVV(TAG, "init #0");
|
||||
if (!EPaperBase::initialise(partial)) { // Call parent impl
|
||||
return false; // If parent should request another loop, do so
|
||||
}
|
||||
|
||||
// Fast init requested + supported?
|
||||
if (partial && (this->fast_update_length_ > 0)) {
|
||||
this->step_ = FSMState::INIT_STEP1_FASTINIT;
|
||||
this->wait_for_idle_(true); // Must wait for idle before fastinit sequence in next loop
|
||||
return false; // another loop: step #1 below
|
||||
}
|
||||
|
||||
break; // End state loop below
|
||||
|
||||
case FSMState::INIT_STEP1_FASTINIT:
|
||||
// Step #1: Fast init sequence
|
||||
ESP_LOGVV(TAG, "init #1");
|
||||
this->write_fastinit_();
|
||||
break; // End state loop below
|
||||
|
||||
default:
|
||||
// Unexpected step = bug?
|
||||
this->mark_failed();
|
||||
}
|
||||
|
||||
this->step_ = FSMState::NONE;
|
||||
return true; // Finished: State transition waits for idle
|
||||
}
|
||||
|
||||
bool EPaperJD79660::transfer_buffer_chunks_() {
|
||||
size_t buf_idx = 0;
|
||||
uint8_t bytes_to_send[MAX_TRANSFER_SIZE];
|
||||
const uint32_t start_time = App.get_loop_component_start_time();
|
||||
const auto buffer_length = this->buffer_length_;
|
||||
while (this->current_data_index_ != buffer_length) {
|
||||
bytes_to_send[buf_idx++] = this->buffer_[this->current_data_index_++];
|
||||
|
||||
if (buf_idx == sizeof bytes_to_send) {
|
||||
this->start_data_();
|
||||
this->write_array(bytes_to_send, buf_idx);
|
||||
this->disable();
|
||||
ESP_LOGVV(TAG, "Wrote %zu bytes at %ums", buf_idx, (unsigned) millis());
|
||||
buf_idx = 0;
|
||||
|
||||
if (millis() - start_time > MAX_TRANSFER_TIME) {
|
||||
// Let the main loop run and come back next loop
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Finished the entire dataset
|
||||
if (buf_idx != 0) {
|
||||
this->start_data_();
|
||||
this->write_array(bytes_to_send, buf_idx);
|
||||
this->disable();
|
||||
ESP_LOGVV(TAG, "Wrote %zu bytes at %ums", buf_idx, (unsigned) millis());
|
||||
}
|
||||
// Cleanup for next transfer
|
||||
this->current_data_index_ = 0;
|
||||
|
||||
// Finished with all buffer chunks
|
||||
return true;
|
||||
}
|
||||
|
||||
void EPaperJD79660::write_fastinit_() {
|
||||
// Undocumented register sequence in vendor register range.
|
||||
// Related to Fast Init/Update.
|
||||
// Should likely happen after regular init seq and power on, but before refresh.
|
||||
// Might only work for some models with certain factory MTP.
|
||||
// Please do not change without knowledge to avoid breakage.
|
||||
|
||||
this->send_init_sequence_(this->fast_update_, this->fast_update_length_);
|
||||
}
|
||||
|
||||
bool EPaperJD79660::transfer_data() {
|
||||
// For now always send full frame buffer in chunks.
|
||||
// JD79660 might support partial window transfers. But sample code missing.
|
||||
// And likely minimal impact, solely on SPI transfer time into RAM.
|
||||
|
||||
if (this->current_data_index_ == 0) {
|
||||
this->command(CMD_TRANSFER);
|
||||
}
|
||||
|
||||
return this->transfer_buffer_chunks_();
|
||||
}
|
||||
|
||||
void EPaperJD79660::refresh_screen([[maybe_unused]] bool partial) {
|
||||
ESP_LOGV(TAG, "Refresh");
|
||||
this->cmd_data(CMD_REFRESH, {(uint8_t) 0x00});
|
||||
}
|
||||
|
||||
void EPaperJD79660::power_off() {
|
||||
ESP_LOGV(TAG, "Power off");
|
||||
this->cmd_data(CMD_POWEROFF, {(uint8_t) 0x00});
|
||||
}
|
||||
|
||||
void EPaperJD79660::deep_sleep() {
|
||||
ESP_LOGV(TAG, "Deep sleep");
|
||||
// "Deepsleep between update": Ensure EPD sleep to avoid early hardware wearout!
|
||||
this->cmd_data(CMD_DEEPSLEEP, {(uint8_t) 0xA5});
|
||||
|
||||
// Notes:
|
||||
// - VDD: Some boards (Waveshare) with "clever reset logic" would allow switching off
|
||||
// EPD VDD by pulling reset pin low for longer time.
|
||||
// However, a) not all boards have this, b) reliable sequence timing is difficult,
|
||||
// c) saving is not worth it after deepsleep command above.
|
||||
// If needed: Better option is to drive VDD via MOSFET with separate enable pin.
|
||||
//
|
||||
// - Possible safe shutdown:
|
||||
// EPaperBase::on_safe_shutdown() may also trigger deep_sleep() again.
|
||||
// Regularly, in IDLE state, this does not make sense for this "deepsleep between update" model,
|
||||
// but SPI sequence should simply be ignored by sleeping receiver.
|
||||
// But if triggering during lengthy update, this quick SPI sleep sequence may have benefit.
|
||||
// Optimally, EPDs should even be set all white for longer storage.
|
||||
// But full sequence (>15s) not possible w/o app logic.
|
||||
}
|
||||
|
||||
} // namespace esphome::epaper_spi
|
||||
@@ -1,145 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "epaper_spi.h"
|
||||
|
||||
namespace esphome::epaper_spi {
|
||||
|
||||
/**
|
||||
* JD7966x IC driver implementation
|
||||
*
|
||||
* Currently tested with:
|
||||
* - JD79660 (max res: 200x200)
|
||||
*
|
||||
* May also work for other JD7966x chipset family members with minimal adaptations.
|
||||
*
|
||||
* Capabilities:
|
||||
* - HW frame buffer layout:
|
||||
* 4 colors (gray0..3, commonly BWYR). Bytes consist of 4px/2bpp.
|
||||
* Width must be rounded to multiple of 4.
|
||||
* - Fast init/update (shorter wave forms): Yes. Controlled by CONF_FULL_UPDATE_EVERY.
|
||||
* Needs undocumented fastinit sequence, based on likely vendor specific MTP content.
|
||||
* - Partial transfer (transfer only changed window): No. Maybe possible by HW.
|
||||
* - Partial refresh (refresh only changed window): No. Likely HW limit.
|
||||
*
|
||||
* @internal \c final saves few bytes by devirtualization. Remove \c final when subclassing.
|
||||
*/
|
||||
class EPaperJD79660 final : public EPaperBase {
|
||||
public:
|
||||
EPaperJD79660(const char *name, uint16_t width, uint16_t height, const uint8_t *init_sequence,
|
||||
size_t init_sequence_length, const uint8_t *fast_update, uint16_t fast_update_length)
|
||||
: EPaperBase(name, width, height, init_sequence, init_sequence_length, DISPLAY_TYPE_COLOR),
|
||||
fast_update_(fast_update),
|
||||
fast_update_length_(fast_update_length) {
|
||||
this->row_width_ = (width + 3) / 4; // Fix base class calc (2bpp instead of 1bpp)
|
||||
this->buffer_length_ = this->row_width_ * height;
|
||||
}
|
||||
|
||||
void fill(Color color) override;
|
||||
|
||||
protected:
|
||||
/** Draw colored pixel into frame buffer */
|
||||
void draw_pixel_at(int x, int y, Color color) override;
|
||||
|
||||
/** Reset (multistep sequence)
|
||||
* @pre this->reset_pin_ != nullptr // cv.Required check
|
||||
* @post Should be idle on successful reset. Can mark failures.
|
||||
*/
|
||||
bool reset() override;
|
||||
|
||||
/** Initialise (multistep sequence) */
|
||||
bool initialise(bool partial) override;
|
||||
|
||||
/** Buffer transfer */
|
||||
bool transfer_data() override;
|
||||
|
||||
/** Power on: Already part of init sequence (likely needed there before transferring buffers).
|
||||
* So nothing to do in FSM state.
|
||||
*/
|
||||
void power_on() override {}
|
||||
|
||||
/** Refresh screen
|
||||
* @param partial Ignored: Needed earlier in \a ::initialize
|
||||
* @pre Must be idle.
|
||||
* @post Should return to idle later after processing.
|
||||
*/
|
||||
void refresh_screen([[maybe_unused]] bool partial) override;
|
||||
|
||||
/** Power off
|
||||
* @pre Must be idle.
|
||||
* @post Should return to idle later after processing.
|
||||
* (latter will take long period like ~15-20s on actual refresh!)
|
||||
*/
|
||||
void power_off() override;
|
||||
|
||||
/** Deepsleep: Must be used to avoid hardware wearout!
|
||||
* @pre Must be idle.
|
||||
* @post Will go busy, and not return idle till ::reset!
|
||||
*/
|
||||
void deep_sleep() override;
|
||||
|
||||
/** Internal: Send fast init sequence via undocumented vendor registers
|
||||
* @pre Must be directly after regular ::initialise sequence, before ::transfer_data
|
||||
* @pre Must be idle.
|
||||
* @post Should return to idle later after processing.
|
||||
*/
|
||||
void write_fastinit_();
|
||||
|
||||
/** Internal: Send raw buffer in chunks
|
||||
* \retval true Finished
|
||||
* \retval false Loop time elapsed. Need to call again next loop.
|
||||
*/
|
||||
bool transfer_buffer_chunks_();
|
||||
|
||||
/** @name IC commands @{ */
|
||||
static constexpr uint8_t CMD_POWEROFF = 0x02;
|
||||
static constexpr uint8_t CMD_DEEPSLEEP = 0x07;
|
||||
static constexpr uint8_t CMD_TRANSFER = 0x10;
|
||||
static constexpr uint8_t CMD_REFRESH = 0x12;
|
||||
/** @} */
|
||||
|
||||
/** State machine constants for \a step_ */
|
||||
enum class FSMState : uint8_t {
|
||||
NONE = 0, //!< Initial/default value: Unused
|
||||
|
||||
/* Reset state steps */
|
||||
RESET_STEP0_H,
|
||||
RESET_STEP1_L,
|
||||
RESET_STEP2_IDLECHECK,
|
||||
|
||||
/* Init state steps */
|
||||
INIT_STEP0_REGULARINIT,
|
||||
INIT_STEP1_FASTINIT,
|
||||
};
|
||||
|
||||
/** Wait time (millisec) for first reset phase: High
|
||||
*
|
||||
* Wait via FSM loop.
|
||||
*/
|
||||
static constexpr uint16_t SLEEP_MS_RESET0 = 200;
|
||||
|
||||
/** Wait time (millisec) for second reset phase: Low
|
||||
*
|
||||
* Holding Reset Low too long may trigger "clever reset" logic
|
||||
* of e.g. Waveshare Rev2 boards: VDD is shut down via MOSFET, and IC
|
||||
* will not report idle anymore!
|
||||
* FSM loop may spuriously increase delay, e.g. >16ms.
|
||||
* Therefore, sync wait below, as allowed (code rule "delays > 10ms not permitted"),
|
||||
* yet only slightly exceeding known IC min req of >1.5ms.
|
||||
*/
|
||||
static constexpr uint16_t SLEEP_MS_RESET1 = 2;
|
||||
|
||||
/** Wait time (millisec) for third reset phase: High
|
||||
*
|
||||
* Wait via FSM loop.
|
||||
*/
|
||||
static constexpr uint16_t SLEEP_MS_RESET2 = 200;
|
||||
|
||||
// properties initialised in the constructor
|
||||
const uint8_t *const fast_update_{};
|
||||
const uint16_t fast_update_length_{};
|
||||
|
||||
/** Counter for tracking substeps within FSM state */
|
||||
FSMState step_{FSMState::NONE};
|
||||
};
|
||||
|
||||
} // namespace esphome::epaper_spi
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
namespace esphome::epaper_spi {
|
||||
|
||||
class EPaperSpectraE6 final : public EPaperBase {
|
||||
class EPaperSpectraE6 : public EPaperBase {
|
||||
public:
|
||||
EPaperSpectraE6(const char *name, uint16_t width, uint16_t height, const uint8_t *init_sequence,
|
||||
size_t init_sequence_length)
|
||||
|
||||
@@ -6,7 +6,7 @@ namespace esphome::epaper_spi {
|
||||
/**
|
||||
* An epaper display that needs LUTs to be sent to it.
|
||||
*/
|
||||
class EpaperWaveshare final : public EPaperMono {
|
||||
class EpaperWaveshare : public EPaperMono {
|
||||
public:
|
||||
EpaperWaveshare(const char *name, uint16_t width, uint16_t height, const uint8_t *init_sequence,
|
||||
size_t init_sequence_length, const uint8_t *lut, size_t lut_length, const uint8_t *partial_lut,
|
||||
|
||||
@@ -1,86 +0,0 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components.mipi import flatten_sequence
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_BUSY_PIN, CONF_RESET_PIN
|
||||
from esphome.core import ID
|
||||
|
||||
from ..display import CONF_INIT_SEQUENCE_ID
|
||||
from . import EpaperModel
|
||||
|
||||
|
||||
class JD79660(EpaperModel):
|
||||
def __init__(self, name, class_name="EPaperJD79660", fast_update=None, **kwargs):
|
||||
super().__init__(name, class_name, **kwargs)
|
||||
self.fast_update = fast_update
|
||||
|
||||
def option(self, name, fallback=cv.UNDEFINED) -> cv.Optional | cv.Required:
|
||||
# Validate required pins, as C++ code will assume existence
|
||||
if name in (CONF_RESET_PIN, CONF_BUSY_PIN):
|
||||
return cv.Required(name)
|
||||
|
||||
# Delegate to parent
|
||||
return super().option(name, fallback)
|
||||
|
||||
def get_constructor_args(self, config) -> tuple:
|
||||
# Resembles init_sequence handling for fast_update config
|
||||
if self.fast_update is None:
|
||||
fast_update = cg.nullptr, 0
|
||||
else:
|
||||
flat_fast_update = flatten_sequence(self.fast_update)
|
||||
fast_update = (
|
||||
cg.static_const_array(
|
||||
ID(
|
||||
config[CONF_INIT_SEQUENCE_ID].id + "_fast_update", type=cg.uint8
|
||||
),
|
||||
flat_fast_update,
|
||||
),
|
||||
len(flat_fast_update),
|
||||
)
|
||||
return (*fast_update,)
|
||||
|
||||
|
||||
jd79660 = JD79660(
|
||||
"jd79660",
|
||||
# Specified refresh times are ~20s (full) or ~15s (fast) due to BWRY.
|
||||
# So disallow low update intervals (with safety margin), to avoid e.g. FSM update loops.
|
||||
# Even less frequent intervals (min/h) highly recommended to optimize lifetime!
|
||||
minimum_update_interval="30s",
|
||||
# SPI rate: From spec comparisons, IC should allow SCL write cycles up to 10MHz rate.
|
||||
# Existing code samples also prefer 10MHz. So justifies as default.
|
||||
# Decrease value further in user config if needed (e.g. poor cabling).
|
||||
data_rate="10MHz",
|
||||
# No need to set optional reset_duration:
|
||||
# Code requires multistep reset sequence with precise timings
|
||||
# according to data sheet or samples.
|
||||
)
|
||||
|
||||
# Waveshare 1.54-G
|
||||
#
|
||||
# Device may have specific factory provisioned MTP content to facilitate vendor register features like fast init.
|
||||
# Vendor specific init derived from vendor sample code
|
||||
# <https://github.com/waveshareteam/e-Paper/blob/master/E-paper_Separate_Program/1in54_e-Paper_G/ESP32/EPD_1in54g.cpp>
|
||||
# Compatible MIT license, see esphome/LICENSE file.
|
||||
#
|
||||
# fmt: off
|
||||
jd79660.extend(
|
||||
"Waveshare-1.54in-G",
|
||||
width=200,
|
||||
height=200,
|
||||
|
||||
initsequence=(
|
||||
(0x4D, 0x78,),
|
||||
(0x00, 0x0F, 0x29,),
|
||||
(0x06, 0x0d, 0x12, 0x30, 0x20, 0x19, 0x2a, 0x22,),
|
||||
(0x50, 0x37,),
|
||||
(0x61, 200 // 256, 200 % 256, 200 // 256, 200 % 256,), # RES: 200x200 fixed
|
||||
(0xE9, 0x01,),
|
||||
(0x30, 0x08,),
|
||||
# Power On (0x04): Must be early part of init seq = Disabled later!
|
||||
(0x04,),
|
||||
),
|
||||
fast_update=(
|
||||
(0xE0, 0x02,),
|
||||
(0xE6, 0x5D,),
|
||||
(0xA5, 0x00,),
|
||||
),
|
||||
)
|
||||
@@ -1435,10 +1435,6 @@ async def to_code(config):
|
||||
CORE.relative_internal_path(".espressif")
|
||||
)
|
||||
|
||||
# Set the uv cache inside the data dir so "Clean All" clears it.
|
||||
# Avoids persistent corrupted cache from mid-stream download failures.
|
||||
os.environ["UV_CACHE_DIR"] = str(CORE.relative_internal_path(".uv_cache"))
|
||||
|
||||
if conf[CONF_TYPE] == FRAMEWORK_ESP_IDF:
|
||||
cg.add_build_flag("-DUSE_ESP_IDF")
|
||||
cg.add_build_flag("-DUSE_ESP32_FRAMEWORK_ESP_IDF")
|
||||
|
||||
@@ -48,7 +48,7 @@ class ESPBTUUID {
|
||||
|
||||
// Remove before 2026.8.0
|
||||
ESPDEPRECATED("Use to_str() instead. Removed in 2026.8.0", "2026.2.0")
|
||||
std::string to_string() const; // NOLINT
|
||||
std::string to_string() const;
|
||||
const char *to_str(std::span<char, UUID_STR_LEN> output) const;
|
||||
|
||||
protected:
|
||||
|
||||
@@ -27,11 +27,6 @@ static const char *const TAG = "esp32_hosted.update";
|
||||
// Older coprocessor firmware versions have a 1500-byte limit per RPC call
|
||||
constexpr size_t CHUNK_SIZE = 1500;
|
||||
|
||||
#ifdef USE_ESP32_HOSTED_HTTP_UPDATE
|
||||
// Interval/timeout IDs (uint32_t to avoid string comparison)
|
||||
constexpr uint32_t INITIAL_CHECK_INTERVAL_ID = 0;
|
||||
#endif
|
||||
|
||||
// Compile-time version string from esp_hosted_host_fw_ver.h macros
|
||||
#define STRINGIFY_(x) #x
|
||||
#define STRINGIFY(x) STRINGIFY_(x)
|
||||
@@ -132,18 +127,15 @@ void Esp32HostedUpdate::setup() {
|
||||
this->status_clear_error();
|
||||
this->publish_state();
|
||||
#else
|
||||
// HTTP mode: check every 10s until network is ready (max 6 attempts)
|
||||
// HTTP mode: retry initial check every 10s until network is ready (max 6 attempts)
|
||||
// Only if update interval is > 1 minute to avoid redundant checks
|
||||
if (this->get_update_interval() > 60000) {
|
||||
this->initial_check_remaining_ = 6;
|
||||
this->set_interval(INITIAL_CHECK_INTERVAL_ID, 10000, [this]() {
|
||||
bool connected = network::is_connected();
|
||||
if (--this->initial_check_remaining_ == 0 || connected) {
|
||||
this->cancel_interval(INITIAL_CHECK_INTERVAL_ID);
|
||||
if (connected) {
|
||||
this->check();
|
||||
}
|
||||
this->set_retry("initial_check", 10000, 6, [this](uint8_t) {
|
||||
if (!network::is_connected()) {
|
||||
return RetryResult::RETRY;
|
||||
}
|
||||
this->check();
|
||||
return RetryResult::DONE;
|
||||
});
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -44,7 +44,6 @@ class Esp32HostedUpdate : public update::UpdateEntity, public PollingComponent {
|
||||
// HTTP mode helpers
|
||||
bool fetch_manifest_();
|
||||
bool stream_firmware_to_coprocessor_();
|
||||
uint8_t initial_check_remaining_{0};
|
||||
#else
|
||||
// Embedded mode members
|
||||
const uint8_t *firmware_data_{nullptr};
|
||||
|
||||
@@ -90,16 +90,14 @@ void HttpRequestUpdate::update_task(void *params) {
|
||||
UPDATE_RETURN;
|
||||
}
|
||||
size_t read_index = container->get_bytes_read();
|
||||
size_t content_length = container->content_length;
|
||||
|
||||
container->end();
|
||||
container.reset(); // Release ownership of the container's shared_ptr
|
||||
|
||||
bool valid = false;
|
||||
{ // Ensures the response string falls out of scope and deallocates before the task ends
|
||||
std::string response((char *) data, read_index);
|
||||
allocator.deallocate(data, container->content_length);
|
||||
|
||||
container->end();
|
||||
container.reset(); // Release ownership of the container's shared_ptr
|
||||
|
||||
valid = json::parse_json(response, [this_update](JsonObject root) -> bool {
|
||||
{ // Scope to ensure JsonDocument is destroyed before deallocating buffer
|
||||
valid = json::parse_json(data, read_index, [this_update](JsonObject root) -> bool {
|
||||
if (!root[ESPHOME_F("name")].is<const char *>() || !root[ESPHOME_F("version")].is<const char *>() ||
|
||||
!root[ESPHOME_F("builds")].is<JsonArray>()) {
|
||||
ESP_LOGE(TAG, "Manifest does not contain required fields");
|
||||
@@ -137,6 +135,7 @@ void HttpRequestUpdate::update_task(void *params) {
|
||||
return false;
|
||||
});
|
||||
}
|
||||
allocator.deallocate(data, content_length);
|
||||
|
||||
if (!valid) {
|
||||
ESP_LOGE(TAG, "Failed to parse JSON from %s", this_update->source_url_.c_str());
|
||||
@@ -157,17 +156,12 @@ void HttpRequestUpdate::update_task(void *params) {
|
||||
}
|
||||
}
|
||||
|
||||
{ // Ensures the current version string falls out of scope and deallocates before the task ends
|
||||
std::string current_version;
|
||||
#ifdef ESPHOME_PROJECT_VERSION
|
||||
current_version = ESPHOME_PROJECT_VERSION;
|
||||
this_update->update_info_.current_version = ESPHOME_PROJECT_VERSION;
|
||||
#else
|
||||
current_version = ESPHOME_VERSION;
|
||||
this_update->update_info_.current_version = ESPHOME_VERSION;
|
||||
#endif
|
||||
|
||||
this_update->update_info_.current_version = current_version;
|
||||
}
|
||||
|
||||
bool trigger_update_available = false;
|
||||
|
||||
if (this_update->update_info_.latest_version.empty() ||
|
||||
|
||||
@@ -134,23 +134,25 @@ ErrorCode ArduinoI2CBus::write_readv(uint8_t address, const uint8_t *write_buffe
|
||||
for (size_t j = 0; j != read_count; j++)
|
||||
read_buffer[j] = wire_->read();
|
||||
}
|
||||
// Avoid switch to prevent compiler-generated lookup table in RAM on ESP8266
|
||||
if (status == 0)
|
||||
return ERROR_OK;
|
||||
if (status == 1) {
|
||||
ESP_LOGVV(TAG, "TX failed: buffer not large enough");
|
||||
return ERROR_UNKNOWN;
|
||||
switch (status) {
|
||||
case 0:
|
||||
return ERROR_OK;
|
||||
case 1:
|
||||
// transmit buffer not large enough
|
||||
ESP_LOGVV(TAG, "TX failed: buffer not large enough");
|
||||
return ERROR_UNKNOWN;
|
||||
case 2:
|
||||
case 3:
|
||||
ESP_LOGVV(TAG, "TX failed: not acknowledged: %d", status);
|
||||
return ERROR_NOT_ACKNOWLEDGED;
|
||||
case 5:
|
||||
ESP_LOGVV(TAG, "TX failed: timeout");
|
||||
return ERROR_UNKNOWN;
|
||||
case 4:
|
||||
default:
|
||||
ESP_LOGVV(TAG, "TX failed: unknown error %u", status);
|
||||
return ERROR_UNKNOWN;
|
||||
}
|
||||
if (status == 2 || status == 3) {
|
||||
ESP_LOGVV(TAG, "TX failed: not acknowledged: %u", status);
|
||||
return ERROR_NOT_ACKNOWLEDGED;
|
||||
}
|
||||
if (status == 5) {
|
||||
ESP_LOGVV(TAG, "TX failed: timeout");
|
||||
return ERROR_UNKNOWN;
|
||||
}
|
||||
ESP_LOGVV(TAG, "TX failed: unknown error %u", status);
|
||||
return ERROR_UNKNOWN;
|
||||
}
|
||||
|
||||
/// Perform I2C bus recovery, see:
|
||||
|
||||
@@ -25,8 +25,13 @@ std::string build_json(const json_build_t &f) {
|
||||
}
|
||||
|
||||
bool parse_json(const std::string &data, const json_parse_t &f) {
|
||||
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
|
||||
return parse_json(reinterpret_cast<const uint8_t *>(data.c_str()), data.size(), f);
|
||||
}
|
||||
|
||||
bool parse_json(const uint8_t *data, size_t len, const json_parse_t &f) {
|
||||
// NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
|
||||
JsonDocument doc = parse_json(reinterpret_cast<const uint8_t *>(data.c_str()), data.size());
|
||||
JsonDocument doc = parse_json(data, len);
|
||||
if (doc.overflowed() || doc.isNull())
|
||||
return false;
|
||||
return f(doc.as<JsonObject>());
|
||||
|
||||
@@ -50,6 +50,8 @@ std::string build_json(const json_build_t &f);
|
||||
|
||||
/// Parse a JSON string and run the provided json parse function if it's valid.
|
||||
bool parse_json(const std::string &data, const json_parse_t &f);
|
||||
/// Parse JSON from raw bytes and run the provided json parse function if it's valid.
|
||||
bool parse_json(const uint8_t *data, size_t len, const json_parse_t &f);
|
||||
|
||||
/// Parse a JSON string and return the root JsonDocument (or an unbound object on error)
|
||||
JsonDocument parse_json(const uint8_t *data, size_t len);
|
||||
|
||||
@@ -275,19 +275,8 @@ void LD2410Component::restart_and_read_all_info() {
|
||||
}
|
||||
|
||||
void LD2410Component::loop() {
|
||||
// Read all available bytes in batches to reduce UART call overhead.
|
||||
int avail = this->available();
|
||||
uint8_t buf[MAX_LINE_LENGTH];
|
||||
while (avail > 0) {
|
||||
size_t to_read = std::min(static_cast<size_t>(avail), sizeof(buf));
|
||||
if (!this->read_array(buf, to_read)) {
|
||||
break;
|
||||
}
|
||||
avail -= to_read;
|
||||
|
||||
for (size_t i = 0; i < to_read; i++) {
|
||||
this->readline_(buf[i]);
|
||||
}
|
||||
while (this->available()) {
|
||||
this->readline_(this->read());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -310,19 +310,8 @@ void LD2412Component::restart_and_read_all_info() {
|
||||
}
|
||||
|
||||
void LD2412Component::loop() {
|
||||
// Read all available bytes in batches to reduce UART call overhead.
|
||||
int avail = this->available();
|
||||
uint8_t buf[MAX_LINE_LENGTH];
|
||||
while (avail > 0) {
|
||||
size_t to_read = std::min(static_cast<size_t>(avail), sizeof(buf));
|
||||
if (!this->read_array(buf, to_read)) {
|
||||
break;
|
||||
}
|
||||
avail -= to_read;
|
||||
|
||||
for (size_t i = 0; i < to_read; i++) {
|
||||
this->readline_(buf[i]);
|
||||
}
|
||||
while (this->available()) {
|
||||
this->readline_(this->read());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -276,19 +276,8 @@ void LD2450Component::dump_config() {
|
||||
}
|
||||
|
||||
void LD2450Component::loop() {
|
||||
// Read all available bytes in batches to reduce UART call overhead.
|
||||
int avail = this->available();
|
||||
uint8_t buf[MAX_LINE_LENGTH];
|
||||
while (avail > 0) {
|
||||
size_t to_read = std::min(static_cast<size_t>(avail), sizeof(buf));
|
||||
if (!this->read_array(buf, to_read)) {
|
||||
break;
|
||||
}
|
||||
avail -= to_read;
|
||||
|
||||
for (size_t i = 0; i < to_read; i++) {
|
||||
this->readline_(buf[i]);
|
||||
}
|
||||
while (this->available()) {
|
||||
this->readline_(this->read());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -68,7 +68,7 @@ void HOT Logger::write_msg_(const char *msg, uint16_t len) {
|
||||
#ifdef CONFIG_PRINTK
|
||||
// Requires the debug component and an active SWD connection.
|
||||
// It is used for pyocd rtt -t nrf52840
|
||||
printk("%.*s", static_cast<int>(len), msg);
|
||||
k_str_out(const_cast<char *>(msg), len);
|
||||
#endif
|
||||
if (this->uart_dev_ == nullptr) {
|
||||
return;
|
||||
|
||||
@@ -38,29 +38,22 @@ void LPS22Component::dump_config() {
|
||||
LOG_UPDATE_INTERVAL(this);
|
||||
}
|
||||
|
||||
static constexpr uint32_t INTERVAL_READ = 0;
|
||||
|
||||
void LPS22Component::update() {
|
||||
uint8_t value = 0x00;
|
||||
this->read_register(CTRL_REG2, &value, 1);
|
||||
value |= CTRL_REG2_ONE_SHOT_MASK;
|
||||
this->write_register(CTRL_REG2, &value, 1);
|
||||
this->read_attempts_remaining_ = READ_ATTEMPTS;
|
||||
this->set_interval(INTERVAL_READ, READ_INTERVAL, [this]() { this->try_read_(); });
|
||||
this->set_retry(READ_INTERVAL, READ_ATTEMPTS, [this](uint8_t _) { return this->try_read_(); });
|
||||
}
|
||||
|
||||
void LPS22Component::try_read_() {
|
||||
RetryResult LPS22Component::try_read_() {
|
||||
uint8_t value = 0x00;
|
||||
this->read_register(STATUS, &value, 1);
|
||||
const uint8_t expected_status_mask = STATUS_T_DA_MASK | STATUS_P_DA_MASK;
|
||||
if ((value & expected_status_mask) != expected_status_mask) {
|
||||
ESP_LOGD(TAG, "STATUS not ready: %x", value);
|
||||
if (--this->read_attempts_remaining_ == 0) {
|
||||
this->cancel_interval(INTERVAL_READ);
|
||||
}
|
||||
return;
|
||||
return RetryResult::RETRY;
|
||||
}
|
||||
this->cancel_interval(INTERVAL_READ);
|
||||
|
||||
if (this->temperature_sensor_ != nullptr) {
|
||||
uint8_t t_buf[2]{0};
|
||||
@@ -75,6 +68,7 @@ void LPS22Component::try_read_() {
|
||||
uint32_t p_lsb = encode_uint24(p_buf[2], p_buf[1], p_buf[0]);
|
||||
this->pressure_sensor_->publish_state(PRESSURE_SCALE * static_cast<float>(p_lsb));
|
||||
}
|
||||
return RetryResult::DONE;
|
||||
}
|
||||
|
||||
} // namespace lps22
|
||||
|
||||
@@ -17,11 +17,10 @@ class LPS22Component : public sensor::Sensor, public PollingComponent, public i2
|
||||
void dump_config() override;
|
||||
|
||||
protected:
|
||||
void try_read_();
|
||||
|
||||
sensor::Sensor *temperature_sensor_{nullptr};
|
||||
sensor::Sensor *pressure_sensor_{nullptr};
|
||||
uint8_t read_attempts_remaining_{0};
|
||||
|
||||
RetryResult try_read_();
|
||||
};
|
||||
|
||||
} // namespace lps22
|
||||
|
||||
@@ -436,7 +436,6 @@ def container_schema(widget_type: WidgetType, extras=None):
|
||||
schema = schema.extend(widget_type.schema)
|
||||
|
||||
def validator(value):
|
||||
value = value or {}
|
||||
return append_layout_schema(schema, value)(value)
|
||||
|
||||
return validator
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from esphome import automation
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import audio, esp32, socket, speaker
|
||||
from esphome.components import audio, esp32, speaker
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_BITS_PER_SAMPLE,
|
||||
@@ -61,7 +61,7 @@ def _set_stream_limits(config):
|
||||
def _validate_source_speaker(config):
|
||||
fconf = fv.full_config.get()
|
||||
|
||||
# Get ID for the output speaker and add it to the source speakers config to easily inherit properties
|
||||
# Get ID for the output speaker and add it to the source speakrs config to easily inherit properties
|
||||
path = fconf.get_path_for_id(config[CONF_ID])[:-3]
|
||||
path.append(CONF_OUTPUT_SPEAKER)
|
||||
output_speaker_id = fconf.get_config_for_path(path)
|
||||
@@ -111,9 +111,6 @@ FINAL_VALIDATE_SCHEMA = cv.All(
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
# Enable wake_loop_threadsafe for immediate command processing from other tasks
|
||||
socket.require_wake_loop_threadsafe()
|
||||
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
|
||||
@@ -130,9 +127,6 @@ async def to_code(config):
|
||||
"CONFIG_SPIRAM_ALLOW_STACK_EXTERNAL_MEMORY", True
|
||||
)
|
||||
|
||||
# Initialize FixedVector with exact count of source speakers
|
||||
cg.add(var.init_source_speakers(len(config[CONF_SOURCE_SPEAKERS])))
|
||||
|
||||
for speaker_config in config[CONF_SOURCE_SPEAKERS]:
|
||||
source_speaker = cg.new_Pvariable(speaker_config[CONF_ID])
|
||||
|
||||
|
||||
@@ -8,8 +8,8 @@
|
||||
namespace esphome {
|
||||
namespace mixer_speaker {
|
||||
template<typename... Ts> class DuckingApplyAction : public Action<Ts...>, public Parented<SourceSpeaker> {
|
||||
TEMPLATABLE_VALUE(uint8_t, decibel_reduction);
|
||||
TEMPLATABLE_VALUE(uint32_t, duration);
|
||||
TEMPLATABLE_VALUE(uint8_t, decibel_reduction)
|
||||
TEMPLATABLE_VALUE(uint32_t, duration)
|
||||
void play(const Ts &...x) override {
|
||||
this->parent_->apply_ducking(this->decibel_reduction_.value(x...), this->duration_.value(x...));
|
||||
}
|
||||
|
||||
@@ -2,13 +2,11 @@
|
||||
|
||||
#ifdef USE_ESP32
|
||||
|
||||
#include "esphome/core/application.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cstring>
|
||||
|
||||
namespace esphome {
|
||||
@@ -16,7 +14,6 @@ namespace mixer_speaker {
|
||||
|
||||
static const UBaseType_t MIXER_TASK_PRIORITY = 10;
|
||||
|
||||
static const uint32_t STOPPING_TIMEOUT_MS = 5000;
|
||||
static const uint32_t TRANSFER_BUFFER_DURATION_MS = 50;
|
||||
static const uint32_t TASK_DELAY_MS = 25;
|
||||
|
||||
@@ -30,53 +27,21 @@ static const char *const TAG = "speaker_mixer";
|
||||
// Gives the Q15 fixed point scaling factor to reduce by 0 dB, 1dB, ..., 50 dB
|
||||
// dB to PCM scaling factor formula: floating_point_scale_factor = 2^(-db/6.014)
|
||||
// float to Q15 fixed point formula: q15_scale_factor = floating_point_scale_factor * 2^(15)
|
||||
static const std::array<int16_t, 51> DECIBEL_REDUCTION_TABLE = {
|
||||
static const std::vector<int16_t> DECIBEL_REDUCTION_TABLE = {
|
||||
32767, 29201, 26022, 23189, 20665, 18415, 16410, 14624, 13032, 11613, 10349, 9222, 8218, 7324, 6527, 5816, 5183,
|
||||
4619, 4116, 3668, 3269, 2913, 2596, 2313, 2061, 1837, 1637, 1459, 1300, 1158, 1032, 920, 820, 731,
|
||||
651, 580, 517, 461, 411, 366, 326, 291, 259, 231, 206, 183, 163, 146, 130, 116, 103};
|
||||
|
||||
// Event bits for SourceSpeaker command processing
|
||||
enum SourceSpeakerEventBits : uint32_t {
|
||||
SOURCE_SPEAKER_COMMAND_START = (1 << 0),
|
||||
SOURCE_SPEAKER_COMMAND_STOP = (1 << 1),
|
||||
SOURCE_SPEAKER_COMMAND_FINISH = (1 << 2),
|
||||
enum MixerEventGroupBits : uint32_t {
|
||||
COMMAND_STOP = (1 << 0), // stops the mixer task
|
||||
STATE_STARTING = (1 << 10),
|
||||
STATE_RUNNING = (1 << 11),
|
||||
STATE_STOPPING = (1 << 12),
|
||||
STATE_STOPPED = (1 << 13),
|
||||
ERR_ESP_NO_MEM = (1 << 19),
|
||||
ALL_BITS = 0x00FFFFFF, // All valid FreeRTOS event group bits
|
||||
};
|
||||
|
||||
// Event bits for mixer task control and state
|
||||
enum MixerTaskEventBits : uint32_t {
|
||||
MIXER_TASK_COMMAND_START = (1 << 0),
|
||||
MIXER_TASK_COMMAND_STOP = (1 << 1),
|
||||
MIXER_TASK_STATE_STARTING = (1 << 10),
|
||||
MIXER_TASK_STATE_RUNNING = (1 << 11),
|
||||
MIXER_TASK_STATE_STOPPING = (1 << 12),
|
||||
MIXER_TASK_STATE_STOPPED = (1 << 13),
|
||||
MIXER_TASK_ERR_ESP_NO_MEM = (1 << 19),
|
||||
MIXER_TASK_ALL_BITS = 0x00FFFFFF, // All valid FreeRTOS event group bits
|
||||
};
|
||||
|
||||
static inline uint32_t atomic_subtract_clamped(std::atomic<uint32_t> &var, uint32_t amount) {
|
||||
uint32_t current = var.load(std::memory_order_acquire);
|
||||
uint32_t subtracted = 0;
|
||||
if (current > 0) {
|
||||
uint32_t new_value;
|
||||
do {
|
||||
subtracted = std::min(amount, current);
|
||||
new_value = current - subtracted;
|
||||
} while (!var.compare_exchange_weak(current, new_value, std::memory_order_release, std::memory_order_acquire));
|
||||
}
|
||||
return subtracted;
|
||||
}
|
||||
|
||||
static bool create_event_group(EventGroupHandle_t &event_group, Component *component) {
|
||||
event_group = xEventGroupCreate();
|
||||
if (event_group == nullptr) {
|
||||
ESP_LOGE(TAG, "Failed to create event group");
|
||||
component->mark_failed();
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void SourceSpeaker::dump_config() {
|
||||
ESP_LOGCONFIG(TAG,
|
||||
"Mixer Source Speaker\n"
|
||||
@@ -90,70 +55,22 @@ void SourceSpeaker::dump_config() {
|
||||
}
|
||||
|
||||
void SourceSpeaker::setup() {
|
||||
if (!create_event_group(this->event_group_, this)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Start with loop disabled since we begin in STATE_STOPPED with no pending commands
|
||||
this->disable_loop();
|
||||
|
||||
this->parent_->get_output_speaker()->add_audio_output_callback([this](uint32_t new_frames, int64_t write_timestamp) {
|
||||
// First, drain the playback delay (frames in pipeline before this source started contributing)
|
||||
uint32_t delay_to_drain = atomic_subtract_clamped(this->playback_delay_frames_, new_frames);
|
||||
uint32_t remaining_frames = new_frames - delay_to_drain;
|
||||
// The SourceSpeaker may not have included any audio in the mixed output, so verify there were pending frames
|
||||
uint32_t speakers_playback_frames = std::min(new_frames, this->pending_playback_frames_);
|
||||
this->pending_playback_frames_ -= speakers_playback_frames;
|
||||
|
||||
// Then, count towards this source's pending playback frames
|
||||
if (remaining_frames > 0) {
|
||||
uint32_t speakers_playback_frames = atomic_subtract_clamped(this->pending_playback_frames_, remaining_frames);
|
||||
if (speakers_playback_frames > 0) {
|
||||
this->audio_output_callback_(speakers_playback_frames, write_timestamp);
|
||||
}
|
||||
if (speakers_playback_frames > 0) {
|
||||
this->audio_output_callback_(speakers_playback_frames, write_timestamp);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void SourceSpeaker::loop() {
|
||||
uint32_t event_bits = xEventGroupGetBits(this->event_group_);
|
||||
|
||||
// Process commands with priority: STOP > FINISH > START
|
||||
// This ensures stop commands take precedence over conflicting start commands
|
||||
if (event_bits & SOURCE_SPEAKER_COMMAND_STOP) {
|
||||
if (this->state_ == speaker::STATE_RUNNING) {
|
||||
// Clear both STOP and START bits - stop takes precedence
|
||||
xEventGroupClearBits(this->event_group_, SOURCE_SPEAKER_COMMAND_STOP | SOURCE_SPEAKER_COMMAND_START);
|
||||
this->enter_stopping_state_();
|
||||
} else if (this->state_ == speaker::STATE_STOPPED) {
|
||||
// Already stopped, just clear the command bits
|
||||
xEventGroupClearBits(this->event_group_, SOURCE_SPEAKER_COMMAND_STOP | SOURCE_SPEAKER_COMMAND_START);
|
||||
}
|
||||
// Leave bits set if transitioning states (STARTING/STOPPING) - will be processed once state allows
|
||||
} else if (event_bits & SOURCE_SPEAKER_COMMAND_FINISH) {
|
||||
if (this->state_ == speaker::STATE_RUNNING) {
|
||||
xEventGroupClearBits(this->event_group_, SOURCE_SPEAKER_COMMAND_FINISH);
|
||||
this->stop_gracefully_ = true;
|
||||
} else if (this->state_ == speaker::STATE_STOPPED) {
|
||||
// Already stopped, just clear the command bit
|
||||
xEventGroupClearBits(this->event_group_, SOURCE_SPEAKER_COMMAND_FINISH);
|
||||
}
|
||||
// Leave bit set if transitioning states - will be processed once state allows
|
||||
} else if (event_bits & SOURCE_SPEAKER_COMMAND_START) {
|
||||
if (this->state_ == speaker::STATE_STOPPED) {
|
||||
xEventGroupClearBits(this->event_group_, SOURCE_SPEAKER_COMMAND_START);
|
||||
this->state_ = speaker::STATE_STARTING;
|
||||
} else if (this->state_ == speaker::STATE_RUNNING) {
|
||||
// Already running, just clear the command bit
|
||||
xEventGroupClearBits(this->event_group_, SOURCE_SPEAKER_COMMAND_START);
|
||||
}
|
||||
// Leave bit set if transitioning states - will be processed once state allows
|
||||
}
|
||||
// Process state machine
|
||||
switch (this->state_) {
|
||||
case speaker::STATE_STARTING: {
|
||||
esp_err_t err = this->start_();
|
||||
if (err == ESP_OK) {
|
||||
this->pending_playback_frames_.store(0, std::memory_order_release); // reset pending playback frames
|
||||
this->playback_delay_frames_.store(0, std::memory_order_release); // reset playback delay
|
||||
this->has_contributed_.store(false, std::memory_order_release); // reset contribution tracking
|
||||
this->state_ = speaker::STATE_RUNNING;
|
||||
this->stop_gracefully_ = false;
|
||||
this->last_seen_data_ms_ = millis();
|
||||
@@ -161,62 +78,41 @@ void SourceSpeaker::loop() {
|
||||
} else {
|
||||
switch (err) {
|
||||
case ESP_ERR_NO_MEM:
|
||||
this->status_set_error(LOG_STR("Not enough memory"));
|
||||
this->status_set_error(LOG_STR("Failed to start mixer: not enough memory"));
|
||||
break;
|
||||
case ESP_ERR_NOT_SUPPORTED:
|
||||
this->status_set_error(LOG_STR("Unsupported bit depth"));
|
||||
this->status_set_error(LOG_STR("Failed to start mixer: unsupported bits per sample"));
|
||||
break;
|
||||
case ESP_ERR_INVALID_ARG:
|
||||
this->status_set_error(LOG_STR("Incompatible audio streams"));
|
||||
this->status_set_error(
|
||||
LOG_STR("Failed to start mixer: audio stream isn't compatible with the other audio stream."));
|
||||
break;
|
||||
case ESP_ERR_INVALID_STATE:
|
||||
this->status_set_error(LOG_STR("Task failed"));
|
||||
this->status_set_error(LOG_STR("Failed to start mixer: mixer task failed to start"));
|
||||
break;
|
||||
default:
|
||||
this->status_set_error(LOG_STR("Failed"));
|
||||
this->status_set_error(LOG_STR("Failed to start mixer"));
|
||||
break;
|
||||
}
|
||||
|
||||
this->enter_stopping_state_();
|
||||
this->state_ = speaker::STATE_STOPPING;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case speaker::STATE_RUNNING:
|
||||
if (!this->transfer_buffer_->has_buffered_data() &&
|
||||
(this->pending_playback_frames_.load(std::memory_order_acquire) == 0)) {
|
||||
// No audio data in buffer waiting to get mixed and no frames are pending playback
|
||||
if (!this->transfer_buffer_->has_buffered_data()) {
|
||||
if ((this->timeout_ms_.has_value() && ((millis() - this->last_seen_data_ms_) > this->timeout_ms_.value())) ||
|
||||
this->stop_gracefully_) {
|
||||
// Timeout exceeded or graceful stop requested
|
||||
this->enter_stopping_state_();
|
||||
this->state_ = speaker::STATE_STOPPING;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case speaker::STATE_STOPPING: {
|
||||
if ((this->parent_->get_output_speaker()->get_pause_state()) ||
|
||||
((millis() - this->stopping_start_ms_) > STOPPING_TIMEOUT_MS)) {
|
||||
// If parent speaker is paused or if the stopping timeout is exceeded, force stop the output speaker
|
||||
this->parent_->get_output_speaker()->stop();
|
||||
}
|
||||
|
||||
if (this->parent_->get_output_speaker()->is_stopped() ||
|
||||
(this->pending_playback_frames_.load(std::memory_order_acquire) == 0)) {
|
||||
// Output speaker is stopped OR all pending playback frames have played
|
||||
this->pending_playback_frames_.store(0, std::memory_order_release);
|
||||
this->stop_gracefully_ = false;
|
||||
|
||||
this->state_ = speaker::STATE_STOPPED;
|
||||
}
|
||||
case speaker::STATE_STOPPING:
|
||||
this->stop_();
|
||||
this->stop_gracefully_ = false;
|
||||
this->state_ = speaker::STATE_STOPPED;
|
||||
break;
|
||||
}
|
||||
case speaker::STATE_STOPPED:
|
||||
// Re-check event bits for any new commands that may have arrived
|
||||
event_bits = xEventGroupGetBits(this->event_group_);
|
||||
if (!(event_bits &
|
||||
(SOURCE_SPEAKER_COMMAND_START | SOURCE_SPEAKER_COMMAND_STOP | SOURCE_SPEAKER_COMMAND_FINISH))) {
|
||||
// No pending commands, disable loop to save CPU cycles
|
||||
this->disable_loop();
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -226,34 +122,17 @@ size_t SourceSpeaker::play(const uint8_t *data, size_t length, TickType_t ticks_
|
||||
this->start();
|
||||
}
|
||||
size_t bytes_written = 0;
|
||||
std::shared_ptr<RingBuffer> temp_ring_buffer = this->ring_buffer_.lock();
|
||||
if (temp_ring_buffer.use_count() > 0) {
|
||||
// Only write to the ring buffer if the reference is valid
|
||||
if (this->ring_buffer_.use_count() == 1) {
|
||||
std::shared_ptr<RingBuffer> temp_ring_buffer = this->ring_buffer_.lock();
|
||||
bytes_written = temp_ring_buffer->write_without_replacement(data, length, ticks_to_wait);
|
||||
if (bytes_written > 0) {
|
||||
this->last_seen_data_ms_ = millis();
|
||||
}
|
||||
} else {
|
||||
// Delay to avoid repeatedly hammering while waiting for the speaker to start
|
||||
vTaskDelay(ticks_to_wait);
|
||||
}
|
||||
return bytes_written;
|
||||
}
|
||||
|
||||
void SourceSpeaker::send_command_(uint32_t command_bit, bool wake_loop) {
|
||||
this->enable_loop_soon_any_context();
|
||||
uint32_t event_bits = xEventGroupGetBits(this->event_group_);
|
||||
if (!(event_bits & command_bit)) {
|
||||
xEventGroupSetBits(this->event_group_, command_bit);
|
||||
#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE)
|
||||
if (wake_loop) {
|
||||
App.wake_loop_threadsafe();
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
void SourceSpeaker::start() { this->send_command_(SOURCE_SPEAKER_COMMAND_START, true); }
|
||||
void SourceSpeaker::start() { this->state_ = speaker::STATE_STARTING; }
|
||||
|
||||
esp_err_t SourceSpeaker::start_() {
|
||||
const size_t ring_buffer_size = this->audio_stream_info_.ms_to_bytes(this->buffer_duration_ms_);
|
||||
@@ -264,26 +143,35 @@ esp_err_t SourceSpeaker::start_() {
|
||||
if (this->transfer_buffer_ == nullptr) {
|
||||
return ESP_ERR_NO_MEM;
|
||||
}
|
||||
std::shared_ptr<RingBuffer> temp_ring_buffer;
|
||||
|
||||
std::shared_ptr<RingBuffer> temp_ring_buffer = this->ring_buffer_.lock();
|
||||
if (!temp_ring_buffer) {
|
||||
if (!this->ring_buffer_.use_count()) {
|
||||
temp_ring_buffer = RingBuffer::create(ring_buffer_size);
|
||||
this->ring_buffer_ = temp_ring_buffer;
|
||||
}
|
||||
|
||||
if (!temp_ring_buffer) {
|
||||
if (!this->ring_buffer_.use_count()) {
|
||||
return ESP_ERR_NO_MEM;
|
||||
} else {
|
||||
this->transfer_buffer_->set_source(temp_ring_buffer);
|
||||
}
|
||||
}
|
||||
|
||||
this->pending_playback_frames_ = 0; // reset
|
||||
return this->parent_->start(this->audio_stream_info_);
|
||||
}
|
||||
|
||||
void SourceSpeaker::stop() { this->send_command_(SOURCE_SPEAKER_COMMAND_STOP); }
|
||||
void SourceSpeaker::stop() {
|
||||
if (this->state_ != speaker::STATE_STOPPED) {
|
||||
this->state_ = speaker::STATE_STOPPING;
|
||||
}
|
||||
}
|
||||
|
||||
void SourceSpeaker::finish() { this->send_command_(SOURCE_SPEAKER_COMMAND_FINISH); }
|
||||
void SourceSpeaker::stop_() {
|
||||
this->transfer_buffer_.reset(); // deallocates the transfer buffer
|
||||
}
|
||||
|
||||
void SourceSpeaker::finish() { this->stop_gracefully_ = true; }
|
||||
|
||||
bool SourceSpeaker::has_buffered_data() const {
|
||||
return ((this->transfer_buffer_.use_count() > 0) && this->transfer_buffer_->has_buffered_data());
|
||||
@@ -303,16 +191,19 @@ void SourceSpeaker::set_volume(float volume) {
|
||||
|
||||
float SourceSpeaker::get_volume() { return this->parent_->get_output_speaker()->get_volume(); }
|
||||
|
||||
size_t SourceSpeaker::process_data_from_source(std::shared_ptr<audio::AudioSourceTransferBuffer> &transfer_buffer,
|
||||
TickType_t ticks_to_wait) {
|
||||
// Store current offset, as these samples are already ducked
|
||||
const size_t current_length = transfer_buffer->available();
|
||||
size_t SourceSpeaker::process_data_from_source(TickType_t ticks_to_wait) {
|
||||
if (!this->transfer_buffer_.use_count()) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
size_t bytes_read = transfer_buffer->transfer_data_from_source(ticks_to_wait);
|
||||
// Store current offset, as these samples are already ducked
|
||||
const size_t current_length = this->transfer_buffer_->available();
|
||||
|
||||
size_t bytes_read = this->transfer_buffer_->transfer_data_from_source(ticks_to_wait);
|
||||
|
||||
uint32_t samples_to_duck = this->audio_stream_info_.bytes_to_samples(bytes_read);
|
||||
if (samples_to_duck > 0) {
|
||||
int16_t *current_buffer = reinterpret_cast<int16_t *>(transfer_buffer->get_buffer_start() + current_length);
|
||||
int16_t *current_buffer = reinterpret_cast<int16_t *>(this->transfer_buffer_->get_buffer_start() + current_length);
|
||||
|
||||
duck_samples(current_buffer, samples_to_duck, &this->current_ducking_db_reduction_,
|
||||
&this->ducking_transition_samples_remaining_, this->samples_per_ducking_step_,
|
||||
@@ -324,13 +215,10 @@ size_t SourceSpeaker::process_data_from_source(std::shared_ptr<audio::AudioSourc
|
||||
|
||||
void SourceSpeaker::apply_ducking(uint8_t decibel_reduction, uint32_t duration) {
|
||||
if (this->target_ducking_db_reduction_ != decibel_reduction) {
|
||||
// Start transition from the previous target (which becomes the new current level)
|
||||
this->current_ducking_db_reduction_ = this->target_ducking_db_reduction_;
|
||||
|
||||
this->target_ducking_db_reduction_ = decibel_reduction;
|
||||
|
||||
// Calculate the number of intermediate dB steps for the transition timing.
|
||||
// Subtract 1 because the first step is taken immediately after this calculation.
|
||||
uint8_t total_ducking_steps = 0;
|
||||
if (this->target_ducking_db_reduction_ > this->current_ducking_db_reduction_) {
|
||||
// The dB reduction level is increasing (which results in quieter audio)
|
||||
@@ -346,7 +234,7 @@ void SourceSpeaker::apply_ducking(uint8_t decibel_reduction, uint32_t duration)
|
||||
|
||||
this->samples_per_ducking_step_ = this->ducking_transition_samples_remaining_ / total_ducking_steps;
|
||||
this->ducking_transition_samples_remaining_ =
|
||||
this->samples_per_ducking_step_ * total_ducking_steps; // adjust for integer division rounding
|
||||
this->samples_per_ducking_step_ * total_ducking_steps; // Adjust for integer division rounding
|
||||
|
||||
this->current_ducking_db_reduction_ += this->db_change_per_ducking_step_;
|
||||
} else {
|
||||
@@ -405,12 +293,6 @@ void SourceSpeaker::duck_samples(int16_t *input_buffer, uint32_t input_samples_t
|
||||
}
|
||||
}
|
||||
|
||||
void SourceSpeaker::enter_stopping_state_() {
|
||||
this->state_ = speaker::STATE_STOPPING;
|
||||
this->stopping_start_ms_ = millis();
|
||||
this->transfer_buffer_.reset();
|
||||
}
|
||||
|
||||
void MixerSpeaker::dump_config() {
|
||||
ESP_LOGCONFIG(TAG,
|
||||
"Speaker Mixer:\n"
|
||||
@@ -419,74 +301,42 @@ void MixerSpeaker::dump_config() {
|
||||
}
|
||||
|
||||
void MixerSpeaker::setup() {
|
||||
if (!create_event_group(this->event_group_, this)) {
|
||||
this->event_group_ = xEventGroupCreate();
|
||||
|
||||
if (this->event_group_ == nullptr) {
|
||||
ESP_LOGE(TAG, "Failed to create event group");
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
|
||||
// Register callback to track frames in the output pipeline
|
||||
this->output_speaker_->add_audio_output_callback([this](uint32_t new_frames, int64_t write_timestamp) {
|
||||
atomic_subtract_clamped(this->frames_in_pipeline_, new_frames);
|
||||
});
|
||||
|
||||
// Start with loop disabled since no task is running and no commands are pending
|
||||
this->disable_loop();
|
||||
}
|
||||
|
||||
void MixerSpeaker::loop() {
|
||||
uint32_t event_group_bits = xEventGroupGetBits(this->event_group_);
|
||||
|
||||
// Handle pending start request
|
||||
if (event_group_bits & MIXER_TASK_COMMAND_START) {
|
||||
// Only start the task if it's fully stopped and cleaned up
|
||||
if (!this->status_has_error() && (this->task_handle_ == nullptr) && (this->task_stack_buffer_ == nullptr)) {
|
||||
esp_err_t err = this->start_task_();
|
||||
switch (err) {
|
||||
case ESP_OK:
|
||||
xEventGroupClearBits(this->event_group_, MIXER_TASK_COMMAND_START);
|
||||
break;
|
||||
case ESP_ERR_NO_MEM:
|
||||
ESP_LOGE(TAG, "Failed to start; retrying in 1 second");
|
||||
this->status_momentary_error("memory-failure", 1000);
|
||||
return;
|
||||
case ESP_ERR_INVALID_STATE:
|
||||
ESP_LOGE(TAG, "Failed to start; retrying in 1 second");
|
||||
this->status_momentary_error("task-failure", 1000);
|
||||
return;
|
||||
default:
|
||||
ESP_LOGE(TAG, "Failed to start; retrying in 1 second");
|
||||
this->status_momentary_error("failure", 1000);
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (event_group_bits & MixerEventGroupBits::STATE_STARTING) {
|
||||
ESP_LOGD(TAG, "Starting speaker mixer");
|
||||
xEventGroupClearBits(this->event_group_, MixerEventGroupBits::STATE_STARTING);
|
||||
}
|
||||
|
||||
if (event_group_bits & MIXER_TASK_STATE_STARTING) {
|
||||
ESP_LOGD(TAG, "Starting");
|
||||
xEventGroupClearBits(this->event_group_, MIXER_TASK_STATE_STARTING);
|
||||
if (event_group_bits & MixerEventGroupBits::ERR_ESP_NO_MEM) {
|
||||
this->status_set_error(LOG_STR("Failed to allocate the mixer's internal buffer"));
|
||||
xEventGroupClearBits(this->event_group_, MixerEventGroupBits::ERR_ESP_NO_MEM);
|
||||
}
|
||||
if (event_group_bits & MIXER_TASK_ERR_ESP_NO_MEM) {
|
||||
this->status_set_error(LOG_STR("Not enough memory"));
|
||||
xEventGroupClearBits(this->event_group_, MIXER_TASK_ERR_ESP_NO_MEM);
|
||||
}
|
||||
if (event_group_bits & MIXER_TASK_STATE_RUNNING) {
|
||||
ESP_LOGV(TAG, "Started");
|
||||
if (event_group_bits & MixerEventGroupBits::STATE_RUNNING) {
|
||||
ESP_LOGD(TAG, "Started speaker mixer");
|
||||
this->status_clear_error();
|
||||
xEventGroupClearBits(this->event_group_, MIXER_TASK_STATE_RUNNING);
|
||||
xEventGroupClearBits(this->event_group_, MixerEventGroupBits::STATE_RUNNING);
|
||||
}
|
||||
if (event_group_bits & MIXER_TASK_STATE_STOPPING) {
|
||||
ESP_LOGV(TAG, "Stopping");
|
||||
xEventGroupClearBits(this->event_group_, MIXER_TASK_STATE_STOPPING);
|
||||
if (event_group_bits & MixerEventGroupBits::STATE_STOPPING) {
|
||||
ESP_LOGD(TAG, "Stopping speaker mixer");
|
||||
xEventGroupClearBits(this->event_group_, MixerEventGroupBits::STATE_STOPPING);
|
||||
}
|
||||
if (event_group_bits & MIXER_TASK_STATE_STOPPED) {
|
||||
if (event_group_bits & MixerEventGroupBits::STATE_STOPPED) {
|
||||
if (this->delete_task_() == ESP_OK) {
|
||||
ESP_LOGD(TAG, "Stopped");
|
||||
xEventGroupClearBits(this->event_group_, MIXER_TASK_ALL_BITS);
|
||||
xEventGroupClearBits(this->event_group_, MixerEventGroupBits::ALL_BITS);
|
||||
}
|
||||
}
|
||||
|
||||
if (this->task_handle_ != nullptr) {
|
||||
// If the mixer task is running, check if all source speakers are stopped
|
||||
|
||||
bool all_stopped = true;
|
||||
|
||||
for (auto &speaker : this->source_speakers_) {
|
||||
@@ -494,15 +344,7 @@ void MixerSpeaker::loop() {
|
||||
}
|
||||
|
||||
if (all_stopped) {
|
||||
// Send stop command signal to the mixer task since no source speakers are active
|
||||
xEventGroupSetBits(this->event_group_, MIXER_TASK_COMMAND_STOP);
|
||||
}
|
||||
} else if (this->task_stack_buffer_ == nullptr) {
|
||||
// Task is fully stopped and cleaned up, check if we can disable loop
|
||||
event_group_bits = xEventGroupGetBits(this->event_group_);
|
||||
if (event_group_bits == 0) {
|
||||
// No pending events, disable loop to save CPU cycles
|
||||
this->disable_loop();
|
||||
this->stop();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -524,18 +366,7 @@ esp_err_t MixerSpeaker::start(audio::AudioStreamInfo &stream_info) {
|
||||
}
|
||||
}
|
||||
|
||||
this->enable_loop_soon_any_context(); // ensure loop processes command
|
||||
|
||||
uint32_t event_bits = xEventGroupGetBits(this->event_group_);
|
||||
if (!(event_bits & MIXER_TASK_COMMAND_START)) {
|
||||
// Set MIXER_TASK_COMMAND_START bit if not already set, and then immediately wake for low latency
|
||||
xEventGroupSetBits(this->event_group_, MIXER_TASK_COMMAND_START);
|
||||
#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE)
|
||||
App.wake_loop_threadsafe();
|
||||
#endif
|
||||
}
|
||||
|
||||
return ESP_OK;
|
||||
return this->start_task_();
|
||||
}
|
||||
|
||||
esp_err_t MixerSpeaker::start_task_() {
|
||||
@@ -566,32 +397,29 @@ esp_err_t MixerSpeaker::start_task_() {
|
||||
}
|
||||
|
||||
esp_err_t MixerSpeaker::delete_task_() {
|
||||
if (this->task_handle_ != nullptr) {
|
||||
// Delete the task
|
||||
vTaskDelete(this->task_handle_);
|
||||
if (!this->task_created_) {
|
||||
this->task_handle_ = nullptr;
|
||||
}
|
||||
|
||||
if ((this->task_handle_ == nullptr) && (this->task_stack_buffer_ != nullptr)) {
|
||||
// Deallocate the task stack buffer
|
||||
if (this->task_stack_in_psram_) {
|
||||
RAMAllocator<StackType_t> stack_allocator(RAMAllocator<StackType_t>::ALLOC_EXTERNAL);
|
||||
stack_allocator.deallocate(this->task_stack_buffer_, TASK_STACK_SIZE);
|
||||
} else {
|
||||
RAMAllocator<StackType_t> stack_allocator(RAMAllocator<StackType_t>::ALLOC_INTERNAL);
|
||||
stack_allocator.deallocate(this->task_stack_buffer_, TASK_STACK_SIZE);
|
||||
if (this->task_stack_buffer_ != nullptr) {
|
||||
if (this->task_stack_in_psram_) {
|
||||
RAMAllocator<StackType_t> stack_allocator(RAMAllocator<StackType_t>::ALLOC_EXTERNAL);
|
||||
stack_allocator.deallocate(this->task_stack_buffer_, TASK_STACK_SIZE);
|
||||
} else {
|
||||
RAMAllocator<StackType_t> stack_allocator(RAMAllocator<StackType_t>::ALLOC_INTERNAL);
|
||||
stack_allocator.deallocate(this->task_stack_buffer_, TASK_STACK_SIZE);
|
||||
}
|
||||
|
||||
this->task_stack_buffer_ = nullptr;
|
||||
}
|
||||
|
||||
this->task_stack_buffer_ = nullptr;
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
if ((this->task_handle_ != nullptr) || (this->task_stack_buffer_ != nullptr)) {
|
||||
return ESP_ERR_INVALID_STATE;
|
||||
}
|
||||
|
||||
return ESP_OK;
|
||||
return ESP_ERR_INVALID_STATE;
|
||||
}
|
||||
|
||||
void MixerSpeaker::stop() { xEventGroupSetBits(this->event_group_, MixerEventGroupBits::COMMAND_STOP); }
|
||||
|
||||
void MixerSpeaker::copy_frames(const int16_t *input_buffer, audio::AudioStreamInfo input_stream_info,
|
||||
int16_t *output_buffer, audio::AudioStreamInfo output_stream_info,
|
||||
uint32_t frames_to_transfer) {
|
||||
@@ -644,34 +472,32 @@ void MixerSpeaker::mix_audio_samples(const int16_t *primary_buffer, audio::Audio
|
||||
}
|
||||
|
||||
void MixerSpeaker::audio_mixer_task(void *params) {
|
||||
MixerSpeaker *this_mixer = static_cast<MixerSpeaker *>(params);
|
||||
MixerSpeaker *this_mixer = (MixerSpeaker *) params;
|
||||
|
||||
xEventGroupSetBits(this_mixer->event_group_, MIXER_TASK_STATE_STARTING);
|
||||
xEventGroupSetBits(this_mixer->event_group_, MixerEventGroupBits::STATE_STARTING);
|
||||
|
||||
this_mixer->task_created_ = true;
|
||||
|
||||
std::unique_ptr<audio::AudioSinkTransferBuffer> output_transfer_buffer = audio::AudioSinkTransferBuffer::create(
|
||||
this_mixer->audio_stream_info_.value().ms_to_bytes(TRANSFER_BUFFER_DURATION_MS));
|
||||
|
||||
if (output_transfer_buffer == nullptr) {
|
||||
xEventGroupSetBits(this_mixer->event_group_, MIXER_TASK_STATE_STOPPED | MIXER_TASK_ERR_ESP_NO_MEM);
|
||||
xEventGroupSetBits(this_mixer->event_group_,
|
||||
MixerEventGroupBits::STATE_STOPPED | MixerEventGroupBits::ERR_ESP_NO_MEM);
|
||||
|
||||
vTaskSuspend(nullptr); // Suspend this task indefinitely until the loop method deletes it
|
||||
this_mixer->task_created_ = false;
|
||||
vTaskDelete(nullptr);
|
||||
}
|
||||
|
||||
output_transfer_buffer->set_sink(this_mixer->output_speaker_);
|
||||
|
||||
xEventGroupSetBits(this_mixer->event_group_, MIXER_TASK_STATE_RUNNING);
|
||||
xEventGroupSetBits(this_mixer->event_group_, MixerEventGroupBits::STATE_RUNNING);
|
||||
|
||||
bool sent_finished = false;
|
||||
|
||||
// Pre-allocate vectors to avoid heap allocation in the loop (max 8 source speakers per schema)
|
||||
FixedVector<SourceSpeaker *> speakers_with_data;
|
||||
FixedVector<std::shared_ptr<audio::AudioSourceTransferBuffer>> transfer_buffers_with_data;
|
||||
speakers_with_data.init(this_mixer->source_speakers_.size());
|
||||
transfer_buffers_with_data.init(this_mixer->source_speakers_.size());
|
||||
|
||||
while (true) {
|
||||
uint32_t event_group_bits = xEventGroupGetBits(this_mixer->event_group_);
|
||||
if (event_group_bits & MIXER_TASK_COMMAND_STOP) {
|
||||
if (event_group_bits & MixerEventGroupBits::COMMAND_STOP) {
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -681,20 +507,15 @@ void MixerSpeaker::audio_mixer_task(void *params) {
|
||||
const uint32_t output_frames_free =
|
||||
this_mixer->audio_stream_info_.value().bytes_to_frames(output_transfer_buffer->free());
|
||||
|
||||
speakers_with_data.clear();
|
||||
transfer_buffers_with_data.clear();
|
||||
std::vector<SourceSpeaker *> speakers_with_data;
|
||||
std::vector<std::shared_ptr<audio::AudioSourceTransferBuffer>> transfer_buffers_with_data;
|
||||
|
||||
for (auto &speaker : this_mixer->source_speakers_) {
|
||||
if (speaker->is_running() && !speaker->get_pause_state()) {
|
||||
// Speaker is running and not paused, so it possibly can provide audio data
|
||||
if (speaker->get_transfer_buffer().use_count() > 0) {
|
||||
std::shared_ptr<audio::AudioSourceTransferBuffer> transfer_buffer = speaker->get_transfer_buffer().lock();
|
||||
if (transfer_buffer.use_count() == 0) {
|
||||
// No transfer buffer allocated, so skip processing this speaker
|
||||
continue;
|
||||
}
|
||||
speaker->process_data_from_source(transfer_buffer, 0); // Transfers and ducks audio from source ring buffers
|
||||
speaker->process_data_from_source(0); // Transfers and ducks audio from source ring buffers
|
||||
|
||||
if (transfer_buffer->available() > 0) {
|
||||
if ((transfer_buffer->available() > 0) && !speaker->get_pause_state()) {
|
||||
// Store the locked transfer buffers in their own vector to avoid releasing ownership until after the loop
|
||||
transfer_buffers_with_data.push_back(transfer_buffer);
|
||||
speakers_with_data.push_back(speaker);
|
||||
@@ -726,21 +547,13 @@ void MixerSpeaker::audio_mixer_task(void *params) {
|
||||
reinterpret_cast<int16_t *>(output_transfer_buffer->get_buffer_end()),
|
||||
this_mixer->audio_stream_info_.value(), frames_to_mix);
|
||||
|
||||
// Set playback delay for newly contributing source
|
||||
if (!speakers_with_data[0]->has_contributed_.load(std::memory_order_acquire)) {
|
||||
speakers_with_data[0]->playback_delay_frames_.store(
|
||||
this_mixer->frames_in_pipeline_.load(std::memory_order_acquire), std::memory_order_release);
|
||||
speakers_with_data[0]->has_contributed_.store(true, std::memory_order_release);
|
||||
}
|
||||
|
||||
// Update source speaker pending frames
|
||||
speakers_with_data[0]->pending_playback_frames_.fetch_add(frames_to_mix, std::memory_order_release);
|
||||
// Update source speaker buffer length
|
||||
transfer_buffers_with_data[0]->decrease_buffer_length(active_stream_info.frames_to_bytes(frames_to_mix));
|
||||
speakers_with_data[0]->pending_playback_frames_ += frames_to_mix;
|
||||
|
||||
// Update output transfer buffer length and pipeline frame count
|
||||
// Update output transfer buffer length
|
||||
output_transfer_buffer->increase_buffer_length(
|
||||
this_mixer->audio_stream_info_.value().frames_to_bytes(frames_to_mix));
|
||||
this_mixer->frames_in_pipeline_.fetch_add(frames_to_mix, std::memory_order_release);
|
||||
} else {
|
||||
// Speaker's stream info doesn't match the output speaker's, so it's a new source speaker
|
||||
if (!this_mixer->output_speaker_->is_stopped()) {
|
||||
@@ -755,8 +568,6 @@ void MixerSpeaker::audio_mixer_task(void *params) {
|
||||
active_stream_info.get_sample_rate());
|
||||
this_mixer->output_speaker_->set_audio_stream_info(this_mixer->audio_stream_info_.value());
|
||||
this_mixer->output_speaker_->start();
|
||||
// Reset pipeline frame count since we're starting fresh with a new sample rate
|
||||
this_mixer->frames_in_pipeline_.store(0, std::memory_order_release);
|
||||
sent_finished = false;
|
||||
}
|
||||
}
|
||||
@@ -785,39 +596,26 @@ void MixerSpeaker::audio_mixer_task(void *params) {
|
||||
}
|
||||
}
|
||||
|
||||
// Get current pipeline depth for delay calculation (before incrementing)
|
||||
uint32_t current_pipeline_frames = this_mixer->frames_in_pipeline_.load(std::memory_order_acquire);
|
||||
|
||||
// Update source transfer buffer lengths and add new audio durations to the source speaker pending playbacks
|
||||
for (size_t i = 0; i < transfer_buffers_with_data.size(); ++i) {
|
||||
// Set playback delay for newly contributing sources
|
||||
if (!speakers_with_data[i]->has_contributed_.load(std::memory_order_acquire)) {
|
||||
speakers_with_data[i]->playback_delay_frames_.store(current_pipeline_frames, std::memory_order_release);
|
||||
speakers_with_data[i]->has_contributed_.store(true, std::memory_order_release);
|
||||
}
|
||||
|
||||
speakers_with_data[i]->pending_playback_frames_.fetch_add(frames_to_mix, std::memory_order_release);
|
||||
transfer_buffers_with_data[i]->decrease_buffer_length(
|
||||
speakers_with_data[i]->get_audio_stream_info().frames_to_bytes(frames_to_mix));
|
||||
speakers_with_data[i]->pending_playback_frames_ += frames_to_mix;
|
||||
}
|
||||
|
||||
// Update output transfer buffer length and pipeline frame count (once, not per source)
|
||||
// Update output transfer buffer length
|
||||
output_transfer_buffer->increase_buffer_length(
|
||||
this_mixer->audio_stream_info_.value().frames_to_bytes(frames_to_mix));
|
||||
this_mixer->frames_in_pipeline_.fetch_add(frames_to_mix, std::memory_order_release);
|
||||
}
|
||||
}
|
||||
|
||||
xEventGroupSetBits(this_mixer->event_group_, MIXER_TASK_STATE_STOPPING);
|
||||
|
||||
// Reset pipeline frame count since the task is stopping
|
||||
this_mixer->frames_in_pipeline_.store(0, std::memory_order_release);
|
||||
xEventGroupSetBits(this_mixer->event_group_, MixerEventGroupBits::STATE_STOPPING);
|
||||
|
||||
output_transfer_buffer.reset();
|
||||
|
||||
xEventGroupSetBits(this_mixer->event_group_, MIXER_TASK_STATE_STOPPED);
|
||||
|
||||
vTaskSuspend(nullptr); // Suspend this task indefinitely until the loop method deletes it
|
||||
xEventGroupSetBits(this_mixer->event_group_, MixerEventGroupBits::STATE_STOPPED);
|
||||
this_mixer->task_created_ = false;
|
||||
vTaskDelete(nullptr);
|
||||
}
|
||||
|
||||
} // namespace mixer_speaker
|
||||
|
||||
@@ -7,31 +7,26 @@
|
||||
#include "esphome/components/speaker/speaker.h"
|
||||
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
|
||||
#include <freertos/FreeRTOS.h>
|
||||
#include <freertos/event_groups.h>
|
||||
|
||||
#include <atomic>
|
||||
#include <freertos/FreeRTOS.h>
|
||||
|
||||
namespace esphome {
|
||||
namespace mixer_speaker {
|
||||
|
||||
/* Classes for mixing several source speaker audio streams and writing it to another speaker component.
|
||||
* - Volume controls are passed through to the output speaker
|
||||
* - Source speaker commands are signaled via event group bits and processed in its loop function to ensure thread
|
||||
* safety
|
||||
* - Directly handles pausing at the SourceSpeaker level; pause state is not passed through to the output speaker.
|
||||
* - Audio sent to the SourceSpeaker must have 16 bits per sample.
|
||||
* - Audio sent to the SourceSpeaker's must have 16 bits per sample.
|
||||
* - Audio sent to the SourceSpeaker can have any number of channels. They are duplicated or ignored as needed to match
|
||||
* the number of channels required for the output speaker.
|
||||
* - In queue mode, the audio sent to the SourceSpeakers can have different sample rates.
|
||||
* - In queue mode, the audio sent to the SoureSpeakers can have different sample rates.
|
||||
* - In non-queue mode, the audio sent to the SourceSpeakers must have the same sample rates.
|
||||
* - SourceSpeaker has an internal ring buffer. It also allocates a shared_ptr for an AudioTranserBuffer object.
|
||||
* - Audio Data Flow:
|
||||
* - Audio data played on a SourceSpeaker first writes to its internal ring buffer.
|
||||
* - MixerSpeaker task temporarily takes shared ownership of each SourceSpeaker's AudioTransferBuffer.
|
||||
* - MixerSpeaker calls SourceSpeaker's `process_data_from_source`, which transfers audio from the SourceSpeaker's
|
||||
* - MixerSpeaker calls SourceSpeaker's `process_data_from_source`, which tranfers audio from the SourceSpeaker's
|
||||
* ring buffer to its AudioTransferBuffer. Audio ducking is applied at this step.
|
||||
* - In queue mode, MixerSpeaker prioritizes the earliest configured SourceSpeaker with audio data. Audio data is
|
||||
* sent to the output speaker.
|
||||
@@ -68,15 +63,13 @@ class SourceSpeaker : public speaker::Speaker, public Component {
|
||||
bool get_pause_state() const override { return this->pause_state_; }
|
||||
|
||||
/// @brief Transfers audio from the ring buffer into the transfer buffer. Ducks audio while transferring.
|
||||
/// @param transfer_buffer Locked shared_ptr to the transfer buffer (must be valid, not null)
|
||||
/// @param ticks_to_wait FreeRTOS ticks to wait while waiting to read from the ring buffer.
|
||||
/// @return Number of bytes transferred from the ring buffer.
|
||||
size_t process_data_from_source(std::shared_ptr<audio::AudioSourceTransferBuffer> &transfer_buffer,
|
||||
TickType_t ticks_to_wait);
|
||||
size_t process_data_from_source(TickType_t ticks_to_wait);
|
||||
|
||||
/// @brief Sets the ducking level for the source speaker.
|
||||
/// @param decibel_reduction The dB reduction level. For example, 0 is no change, 10 is a reduction by 10 dB
|
||||
/// @param duration The number of milliseconds to transition from the current level to the new level
|
||||
/// @param decibel_reduction (uint8_t) The dB reduction level. For example, 0 is no change, 10 is a reduction by 10 dB
|
||||
/// @param duration (uint32_t) The number of milliseconds to transition from the current level to the new level
|
||||
void apply_ducking(uint8_t decibel_reduction, uint32_t duration);
|
||||
|
||||
void set_buffer_duration(uint32_t buffer_duration_ms) { this->buffer_duration_ms_ = buffer_duration_ms; }
|
||||
@@ -88,15 +81,14 @@ class SourceSpeaker : public speaker::Speaker, public Component {
|
||||
protected:
|
||||
friend class MixerSpeaker;
|
||||
esp_err_t start_();
|
||||
void enter_stopping_state_();
|
||||
void send_command_(uint32_t command_bit, bool wake_loop = false);
|
||||
void stop_();
|
||||
|
||||
/// @brief Ducks audio samples by a specified amount. When changing the ducking amount, it can transition gradually
|
||||
/// over a specified amount of samples.
|
||||
/// @param input_buffer buffer with audio samples to be ducked in place
|
||||
/// @param input_samples_to_duck number of samples to process in ``input_buffer``
|
||||
/// @param current_ducking_db_reduction pointer to the current dB reduction
|
||||
/// @param ducking_transition_samples_remaining pointer to the total number of samples left before the
|
||||
/// @param ducking_transition_samples_remaining pointer to the total number of samples left before the the
|
||||
/// transition is finished
|
||||
/// @param samples_per_ducking_step total number of samples per ducking step for the transition
|
||||
/// @param db_change_per_ducking_step the change in dB reduction per step
|
||||
@@ -122,12 +114,7 @@ class SourceSpeaker : public speaker::Speaker, public Component {
|
||||
uint32_t ducking_transition_samples_remaining_{0};
|
||||
uint32_t samples_per_ducking_step_{0};
|
||||
|
||||
std::atomic<uint32_t> pending_playback_frames_{0};
|
||||
std::atomic<uint32_t> playback_delay_frames_{0}; // Frames in output pipeline when this source started contributing
|
||||
std::atomic<bool> has_contributed_{false}; // Tracks if source has contributed during this session
|
||||
|
||||
EventGroupHandle_t event_group_{nullptr};
|
||||
uint32_t stopping_start_ms_{0};
|
||||
uint32_t pending_playback_frames_{0};
|
||||
};
|
||||
|
||||
class MixerSpeaker : public Component {
|
||||
@@ -136,11 +123,10 @@ class MixerSpeaker : public Component {
|
||||
void setup() override;
|
||||
void loop() override;
|
||||
|
||||
void init_source_speakers(size_t count) { this->source_speakers_.init(count); }
|
||||
void add_source_speaker(SourceSpeaker *source_speaker) { this->source_speakers_.push_back(source_speaker); }
|
||||
|
||||
/// @brief Starts the mixer task. Called by a source speaker giving the current audio stream information
|
||||
/// @param stream_info The calling source speaker's audio stream information
|
||||
/// @param stream_info The calling source speakers audio stream information
|
||||
/// @return ESP_ERR_NOT_SUPPORTED if the incoming stream is incompatible due to unsupported bits per sample
|
||||
/// ESP_ERR_INVALID_ARG if the incoming stream is incompatible to be mixed with the other input audio stream
|
||||
/// ESP_ERR_NO_MEM if there isn't enough memory for the task's stack
|
||||
@@ -148,6 +134,8 @@ class MixerSpeaker : public Component {
|
||||
/// ESP_OK if the incoming stream is compatible and the mixer task starts
|
||||
esp_err_t start(audio::AudioStreamInfo &stream_info);
|
||||
|
||||
void stop();
|
||||
|
||||
void set_output_channels(uint8_t output_channels) { this->output_channels_ = output_channels; }
|
||||
void set_output_speaker(speaker::Speaker *speaker) { this->output_speaker_ = speaker; }
|
||||
void set_queue_mode(bool queue_mode) { this->queue_mode_ = queue_mode; }
|
||||
@@ -155,9 +143,6 @@ class MixerSpeaker : public Component {
|
||||
|
||||
speaker::Speaker *get_output_speaker() const { return this->output_speaker_; }
|
||||
|
||||
/// @brief Returns the current number of frames in the output pipeline (written but not yet played)
|
||||
uint32_t get_frames_in_pipeline() const { return this->frames_in_pipeline_.load(std::memory_order_acquire); }
|
||||
|
||||
protected:
|
||||
/// @brief Copies audio frames from the input buffer to the output buffer taking into account the number of channels
|
||||
/// in each stream. If the output stream has more channels, the input samples are duplicated. If the output stream has
|
||||
@@ -174,11 +159,11 @@ class MixerSpeaker : public Component {
|
||||
/// and secondary samples are duplicated or dropped as necessary to ensure the output stream has the configured number
|
||||
/// of channels. Output samples are clamped to the corresponding int16 min or max values if the mixed sample
|
||||
/// overflows.
|
||||
/// @param primary_buffer samples buffer for the primary stream
|
||||
/// @param primary_buffer (int16_t *) samples buffer for the primary stream
|
||||
/// @param primary_stream_info stream info for the primary stream
|
||||
/// @param secondary_buffer samples buffer for secondary stream
|
||||
/// @param secondary_buffer (int16_t *) samples buffer for secondary stream
|
||||
/// @param secondary_stream_info stream info for the secondary stream
|
||||
/// @param output_buffer buffer for the mixed samples
|
||||
/// @param output_buffer (int16_t *) buffer for the mixed samples
|
||||
/// @param output_stream_info stream info for the output buffer
|
||||
/// @param frames_to_mix number of frames in the primary and secondary buffers to mix together
|
||||
static void mix_audio_samples(const int16_t *primary_buffer, audio::AudioStreamInfo primary_stream_info,
|
||||
@@ -200,20 +185,20 @@ class MixerSpeaker : public Component {
|
||||
|
||||
EventGroupHandle_t event_group_{nullptr};
|
||||
|
||||
FixedVector<SourceSpeaker *> source_speakers_;
|
||||
std::vector<SourceSpeaker *> source_speakers_;
|
||||
speaker::Speaker *output_speaker_{nullptr};
|
||||
|
||||
uint8_t output_channels_;
|
||||
bool queue_mode_;
|
||||
bool task_stack_in_psram_{false};
|
||||
|
||||
bool task_created_{false};
|
||||
|
||||
TaskHandle_t task_handle_{nullptr};
|
||||
StaticTask_t task_stack_;
|
||||
StackType_t *task_stack_buffer_{nullptr};
|
||||
|
||||
optional<audio::AudioStreamInfo> audio_stream_info_;
|
||||
|
||||
std::atomic<uint32_t> frames_in_pipeline_{0}; // Frames written to output but not yet played
|
||||
};
|
||||
|
||||
} // namespace mixer_speaker
|
||||
|
||||
@@ -19,25 +19,16 @@ void Modbus::setup() {
|
||||
void Modbus::loop() {
|
||||
const uint32_t now = App.get_loop_component_start_time();
|
||||
|
||||
// Read all available bytes in batches to reduce UART call overhead.
|
||||
int avail = this->available();
|
||||
uint8_t buf[64];
|
||||
while (avail > 0) {
|
||||
size_t to_read = std::min(static_cast<size_t>(avail), sizeof(buf));
|
||||
if (!this->read_array(buf, to_read)) {
|
||||
break;
|
||||
}
|
||||
avail -= to_read;
|
||||
|
||||
for (size_t i = 0; i < to_read; i++) {
|
||||
if (this->parse_modbus_byte_(buf[i])) {
|
||||
this->last_modbus_byte_ = now;
|
||||
} else {
|
||||
size_t at = this->rx_buffer_.size();
|
||||
if (at > 0) {
|
||||
ESP_LOGV(TAG, "Clearing buffer of %d bytes - parse failed", at);
|
||||
this->rx_buffer_.clear();
|
||||
}
|
||||
while (this->available()) {
|
||||
uint8_t byte;
|
||||
this->read_byte(&byte);
|
||||
if (this->parse_modbus_byte_(byte)) {
|
||||
this->last_modbus_byte_ = now;
|
||||
} else {
|
||||
size_t at = this->rx_buffer_.size();
|
||||
if (at > 0) {
|
||||
ESP_LOGV(TAG, "Clearing buffer of %d bytes - parse failed", at);
|
||||
this->rx_buffer_.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -228,50 +219,39 @@ void Modbus::send(uint8_t address, uint8_t function_code, uint16_t start_address
|
||||
return;
|
||||
}
|
||||
|
||||
static constexpr size_t ADDR_SIZE = 1;
|
||||
static constexpr size_t FC_SIZE = 1;
|
||||
static constexpr size_t START_ADDR_SIZE = 2;
|
||||
static constexpr size_t NUM_ENTITIES_SIZE = 2;
|
||||
static constexpr size_t BYTE_COUNT_SIZE = 1;
|
||||
static constexpr size_t MAX_PAYLOAD_SIZE = std::numeric_limits<uint8_t>::max();
|
||||
static constexpr size_t CRC_SIZE = 2;
|
||||
static constexpr size_t MAX_FRAME_SIZE =
|
||||
ADDR_SIZE + FC_SIZE + START_ADDR_SIZE + NUM_ENTITIES_SIZE + BYTE_COUNT_SIZE + MAX_PAYLOAD_SIZE + CRC_SIZE;
|
||||
uint8_t data[MAX_FRAME_SIZE];
|
||||
size_t pos = 0;
|
||||
|
||||
data[pos++] = address;
|
||||
data[pos++] = function_code;
|
||||
std::vector<uint8_t> data;
|
||||
data.push_back(address);
|
||||
data.push_back(function_code);
|
||||
if (this->role == ModbusRole::CLIENT) {
|
||||
data[pos++] = start_address >> 8;
|
||||
data[pos++] = start_address >> 0;
|
||||
data.push_back(start_address >> 8);
|
||||
data.push_back(start_address >> 0);
|
||||
if (function_code != ModbusFunctionCode::WRITE_SINGLE_COIL &&
|
||||
function_code != ModbusFunctionCode::WRITE_SINGLE_REGISTER) {
|
||||
data[pos++] = number_of_entities >> 8;
|
||||
data[pos++] = number_of_entities >> 0;
|
||||
data.push_back(number_of_entities >> 8);
|
||||
data.push_back(number_of_entities >> 0);
|
||||
}
|
||||
}
|
||||
|
||||
if (payload != nullptr) {
|
||||
if (this->role == ModbusRole::SERVER || function_code == ModbusFunctionCode::WRITE_MULTIPLE_COILS ||
|
||||
function_code == ModbusFunctionCode::WRITE_MULTIPLE_REGISTERS) { // Write multiple
|
||||
data[pos++] = payload_len; // Byte count is required for write
|
||||
data.push_back(payload_len); // Byte count is required for write
|
||||
} else {
|
||||
payload_len = 2; // Write single register or coil
|
||||
}
|
||||
for (int i = 0; i < payload_len; i++) {
|
||||
data[pos++] = payload[i];
|
||||
data.push_back(payload[i]);
|
||||
}
|
||||
}
|
||||
|
||||
auto crc = crc16(data, pos);
|
||||
data[pos++] = crc >> 0;
|
||||
data[pos++] = crc >> 8;
|
||||
auto crc = crc16(data.data(), data.size());
|
||||
data.push_back(crc >> 0);
|
||||
data.push_back(crc >> 8);
|
||||
|
||||
if (this->flow_control_pin_ != nullptr)
|
||||
this->flow_control_pin_->digital_write(true);
|
||||
|
||||
this->write_array(data, pos);
|
||||
this->write_array(data);
|
||||
this->flush();
|
||||
|
||||
if (this->flow_control_pin_ != nullptr)
|
||||
@@ -281,7 +261,7 @@ void Modbus::send(uint8_t address, uint8_t function_code, uint16_t start_address
|
||||
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
|
||||
char hex_buf[format_hex_pretty_size(MODBUS_MAX_LOG_BYTES)];
|
||||
#endif
|
||||
ESP_LOGV(TAG, "Modbus write: %s", format_hex_pretty_to(hex_buf, data, pos));
|
||||
ESP_LOGV(TAG, "Modbus write: %s", format_hex_pretty_to(hex_buf, data.data(), data.size()));
|
||||
}
|
||||
|
||||
// Helper function for lambdas
|
||||
|
||||
@@ -13,12 +13,31 @@ static const char *const TAG = "mqtt.alarm_control_panel";
|
||||
|
||||
using namespace esphome::alarm_control_panel;
|
||||
|
||||
// Alarm state MQTT strings indexed by AlarmControlPanelState enum (0-9)
|
||||
PROGMEM_STRING_TABLE(AlarmMqttStateStrings, "disarmed", "armed_home", "armed_away", "armed_night", "armed_vacation",
|
||||
"armed_custom_bypass", "pending", "arming", "disarming", "triggered", "unknown");
|
||||
|
||||
static ProgmemStr alarm_state_to_mqtt_str(AlarmControlPanelState state) {
|
||||
return AlarmMqttStateStrings::get_progmem_str(static_cast<uint8_t>(state), AlarmMqttStateStrings::LAST_INDEX);
|
||||
switch (state) {
|
||||
case ACP_STATE_DISARMED:
|
||||
return ESPHOME_F("disarmed");
|
||||
case ACP_STATE_ARMED_HOME:
|
||||
return ESPHOME_F("armed_home");
|
||||
case ACP_STATE_ARMED_AWAY:
|
||||
return ESPHOME_F("armed_away");
|
||||
case ACP_STATE_ARMED_NIGHT:
|
||||
return ESPHOME_F("armed_night");
|
||||
case ACP_STATE_ARMED_VACATION:
|
||||
return ESPHOME_F("armed_vacation");
|
||||
case ACP_STATE_ARMED_CUSTOM_BYPASS:
|
||||
return ESPHOME_F("armed_custom_bypass");
|
||||
case ACP_STATE_PENDING:
|
||||
return ESPHOME_F("pending");
|
||||
case ACP_STATE_ARMING:
|
||||
return ESPHOME_F("arming");
|
||||
case ACP_STATE_DISARMING:
|
||||
return ESPHOME_F("disarming");
|
||||
case ACP_STATE_TRIGGERED:
|
||||
return ESPHOME_F("triggered");
|
||||
default:
|
||||
return ESPHOME_F("unknown");
|
||||
}
|
||||
}
|
||||
|
||||
MQTTAlarmControlPanelComponent::MQTTAlarmControlPanelComponent(AlarmControlPanel *alarm_control_panel)
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
#include "esphome/core/entity_base.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/progmem.h"
|
||||
#include "esphome/core/version.h"
|
||||
#ifdef USE_LOGGER
|
||||
#include "esphome/components/logger/logger.h"
|
||||
@@ -28,11 +27,6 @@ namespace esphome::mqtt {
|
||||
|
||||
static const char *const TAG = "mqtt";
|
||||
|
||||
// Disconnect reason strings indexed by MQTTClientDisconnectReason enum (0-8)
|
||||
PROGMEM_STRING_TABLE(MQTTDisconnectReasonStrings, "TCP disconnected", "Unacceptable Protocol Version",
|
||||
"Identifier Rejected", "Server Unavailable", "Malformed Credentials", "Not Authorized",
|
||||
"Not Enough Space", "TLS Bad Fingerprint", "DNS Resolve Error", "Unknown");
|
||||
|
||||
MQTTClientComponent::MQTTClientComponent() {
|
||||
global_mqtt_client = this;
|
||||
char mac_addr[MAC_ADDRESS_BUFFER_SIZE];
|
||||
@@ -354,8 +348,36 @@ void MQTTClientComponent::loop() {
|
||||
mqtt_backend_.loop();
|
||||
|
||||
if (this->disconnect_reason_.has_value()) {
|
||||
const LogString *reason_s = MQTTDisconnectReasonStrings::get_log_str(
|
||||
static_cast<uint8_t>(*this->disconnect_reason_), MQTTDisconnectReasonStrings::LAST_INDEX);
|
||||
const LogString *reason_s;
|
||||
switch (*this->disconnect_reason_) {
|
||||
case MQTTClientDisconnectReason::TCP_DISCONNECTED:
|
||||
reason_s = LOG_STR("TCP disconnected");
|
||||
break;
|
||||
case MQTTClientDisconnectReason::MQTT_UNACCEPTABLE_PROTOCOL_VERSION:
|
||||
reason_s = LOG_STR("Unacceptable Protocol Version");
|
||||
break;
|
||||
case MQTTClientDisconnectReason::MQTT_IDENTIFIER_REJECTED:
|
||||
reason_s = LOG_STR("Identifier Rejected");
|
||||
break;
|
||||
case MQTTClientDisconnectReason::MQTT_SERVER_UNAVAILABLE:
|
||||
reason_s = LOG_STR("Server Unavailable");
|
||||
break;
|
||||
case MQTTClientDisconnectReason::MQTT_MALFORMED_CREDENTIALS:
|
||||
reason_s = LOG_STR("Malformed Credentials");
|
||||
break;
|
||||
case MQTTClientDisconnectReason::MQTT_NOT_AUTHORIZED:
|
||||
reason_s = LOG_STR("Not Authorized");
|
||||
break;
|
||||
case MQTTClientDisconnectReason::ESP8266_NOT_ENOUGH_SPACE:
|
||||
reason_s = LOG_STR("Not Enough Space");
|
||||
break;
|
||||
case MQTTClientDisconnectReason::TLS_BAD_FINGERPRINT:
|
||||
reason_s = LOG_STR("TLS Bad Fingerprint");
|
||||
break;
|
||||
default:
|
||||
reason_s = LOG_STR("Unknown");
|
||||
break;
|
||||
}
|
||||
if (!network::is_connected()) {
|
||||
reason_s = LOG_STR("WiFi disconnected");
|
||||
}
|
||||
|
||||
@@ -13,44 +13,109 @@ static const char *const TAG = "mqtt.climate";
|
||||
|
||||
using namespace esphome::climate;
|
||||
|
||||
// Climate mode MQTT strings indexed by ClimateMode enum (0-6): OFF, HEAT_COOL, COOL, HEAT, FAN_ONLY, DRY, AUTO
|
||||
PROGMEM_STRING_TABLE(ClimateMqttModeStrings, "off", "heat_cool", "cool", "heat", "fan_only", "dry", "auto", "unknown");
|
||||
|
||||
static ProgmemStr climate_mode_to_mqtt_str(ClimateMode mode) {
|
||||
return ClimateMqttModeStrings::get_progmem_str(static_cast<uint8_t>(mode), ClimateMqttModeStrings::LAST_INDEX);
|
||||
switch (mode) {
|
||||
case CLIMATE_MODE_OFF:
|
||||
return ESPHOME_F("off");
|
||||
case CLIMATE_MODE_HEAT_COOL:
|
||||
return ESPHOME_F("heat_cool");
|
||||
case CLIMATE_MODE_AUTO:
|
||||
return ESPHOME_F("auto");
|
||||
case CLIMATE_MODE_COOL:
|
||||
return ESPHOME_F("cool");
|
||||
case CLIMATE_MODE_HEAT:
|
||||
return ESPHOME_F("heat");
|
||||
case CLIMATE_MODE_FAN_ONLY:
|
||||
return ESPHOME_F("fan_only");
|
||||
case CLIMATE_MODE_DRY:
|
||||
return ESPHOME_F("dry");
|
||||
default:
|
||||
return ESPHOME_F("unknown");
|
||||
}
|
||||
}
|
||||
|
||||
// Climate action MQTT strings indexed by ClimateAction enum (0,2-6): OFF, (gap), COOLING, HEATING, IDLE, DRYING, FAN
|
||||
PROGMEM_STRING_TABLE(ClimateMqttActionStrings, "off", "unknown", "cooling", "heating", "idle", "drying", "fan",
|
||||
"unknown");
|
||||
|
||||
static ProgmemStr climate_action_to_mqtt_str(ClimateAction action) {
|
||||
return ClimateMqttActionStrings::get_progmem_str(static_cast<uint8_t>(action), ClimateMqttActionStrings::LAST_INDEX);
|
||||
switch (action) {
|
||||
case CLIMATE_ACTION_OFF:
|
||||
return ESPHOME_F("off");
|
||||
case CLIMATE_ACTION_COOLING:
|
||||
return ESPHOME_F("cooling");
|
||||
case CLIMATE_ACTION_HEATING:
|
||||
return ESPHOME_F("heating");
|
||||
case CLIMATE_ACTION_IDLE:
|
||||
return ESPHOME_F("idle");
|
||||
case CLIMATE_ACTION_DRYING:
|
||||
return ESPHOME_F("drying");
|
||||
case CLIMATE_ACTION_FAN:
|
||||
return ESPHOME_F("fan");
|
||||
default:
|
||||
return ESPHOME_F("unknown");
|
||||
}
|
||||
}
|
||||
|
||||
// Climate fan mode MQTT strings indexed by ClimateFanMode enum (0-9)
|
||||
PROGMEM_STRING_TABLE(ClimateMqttFanModeStrings, "on", "off", "auto", "low", "medium", "high", "middle", "focus",
|
||||
"diffuse", "quiet", "unknown");
|
||||
|
||||
static ProgmemStr climate_fan_mode_to_mqtt_str(ClimateFanMode fan_mode) {
|
||||
return ClimateMqttFanModeStrings::get_progmem_str(static_cast<uint8_t>(fan_mode),
|
||||
ClimateMqttFanModeStrings::LAST_INDEX);
|
||||
switch (fan_mode) {
|
||||
case CLIMATE_FAN_ON:
|
||||
return ESPHOME_F("on");
|
||||
case CLIMATE_FAN_OFF:
|
||||
return ESPHOME_F("off");
|
||||
case CLIMATE_FAN_AUTO:
|
||||
return ESPHOME_F("auto");
|
||||
case CLIMATE_FAN_LOW:
|
||||
return ESPHOME_F("low");
|
||||
case CLIMATE_FAN_MEDIUM:
|
||||
return ESPHOME_F("medium");
|
||||
case CLIMATE_FAN_HIGH:
|
||||
return ESPHOME_F("high");
|
||||
case CLIMATE_FAN_MIDDLE:
|
||||
return ESPHOME_F("middle");
|
||||
case CLIMATE_FAN_FOCUS:
|
||||
return ESPHOME_F("focus");
|
||||
case CLIMATE_FAN_DIFFUSE:
|
||||
return ESPHOME_F("diffuse");
|
||||
case CLIMATE_FAN_QUIET:
|
||||
return ESPHOME_F("quiet");
|
||||
default:
|
||||
return ESPHOME_F("unknown");
|
||||
}
|
||||
}
|
||||
|
||||
// Climate swing mode MQTT strings indexed by ClimateSwingMode enum (0-3): OFF, BOTH, VERTICAL, HORIZONTAL
|
||||
PROGMEM_STRING_TABLE(ClimateMqttSwingModeStrings, "off", "both", "vertical", "horizontal", "unknown");
|
||||
|
||||
static ProgmemStr climate_swing_mode_to_mqtt_str(ClimateSwingMode swing_mode) {
|
||||
return ClimateMqttSwingModeStrings::get_progmem_str(static_cast<uint8_t>(swing_mode),
|
||||
ClimateMqttSwingModeStrings::LAST_INDEX);
|
||||
switch (swing_mode) {
|
||||
case CLIMATE_SWING_OFF:
|
||||
return ESPHOME_F("off");
|
||||
case CLIMATE_SWING_BOTH:
|
||||
return ESPHOME_F("both");
|
||||
case CLIMATE_SWING_VERTICAL:
|
||||
return ESPHOME_F("vertical");
|
||||
case CLIMATE_SWING_HORIZONTAL:
|
||||
return ESPHOME_F("horizontal");
|
||||
default:
|
||||
return ESPHOME_F("unknown");
|
||||
}
|
||||
}
|
||||
|
||||
// Climate preset MQTT strings indexed by ClimatePreset enum (0-7)
|
||||
PROGMEM_STRING_TABLE(ClimateMqttPresetStrings, "none", "home", "away", "boost", "comfort", "eco", "sleep", "activity",
|
||||
"unknown");
|
||||
|
||||
static ProgmemStr climate_preset_to_mqtt_str(ClimatePreset preset) {
|
||||
return ClimateMqttPresetStrings::get_progmem_str(static_cast<uint8_t>(preset), ClimateMqttPresetStrings::LAST_INDEX);
|
||||
switch (preset) {
|
||||
case CLIMATE_PRESET_NONE:
|
||||
return ESPHOME_F("none");
|
||||
case CLIMATE_PRESET_HOME:
|
||||
return ESPHOME_F("home");
|
||||
case CLIMATE_PRESET_ECO:
|
||||
return ESPHOME_F("eco");
|
||||
case CLIMATE_PRESET_AWAY:
|
||||
return ESPHOME_F("away");
|
||||
case CLIMATE_PRESET_BOOST:
|
||||
return ESPHOME_F("boost");
|
||||
case CLIMATE_PRESET_COMFORT:
|
||||
return ESPHOME_F("comfort");
|
||||
case CLIMATE_PRESET_SLEEP:
|
||||
return ESPHOME_F("sleep");
|
||||
case CLIMATE_PRESET_ACTIVITY:
|
||||
return ESPHOME_F("activity");
|
||||
default:
|
||||
return ESPHOME_F("unknown");
|
||||
}
|
||||
}
|
||||
|
||||
void MQTTClimateComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) {
|
||||
|
||||
@@ -14,9 +14,6 @@ namespace esphome::mqtt {
|
||||
|
||||
static const char *const TAG = "mqtt.component";
|
||||
|
||||
// Entity category MQTT strings indexed by EntityCategory enum: NONE(0) is skipped, CONFIG(1), DIAGNOSTIC(2)
|
||||
PROGMEM_STRING_TABLE(EntityCategoryMqttStrings, "", "config", "diagnostic");
|
||||
|
||||
// Helper functions for building topic strings on stack
|
||||
inline char *append_str(char *p, const char *s, size_t len) {
|
||||
memcpy(p, s, len);
|
||||
@@ -34,7 +31,10 @@ inline char *append_char(char *p, char c) {
|
||||
// MQTT_COMPONENT_TYPE_MAX_LEN, MQTT_SUFFIX_MAX_LEN, and MQTT_DEFAULT_TOPIC_MAX_LEN are in mqtt_component.h.
|
||||
// ESPHOME_DEVICE_NAME_MAX_LEN and OBJECT_ID_MAX_LEN are defined in entity_base.h.
|
||||
// This ensures the stack buffers below are always large enough.
|
||||
// MQTT_DISCOVERY_PREFIX_MAX_LEN and MQTT_DISCOVERY_TOPIC_MAX_LEN are defined in mqtt_component.h
|
||||
static constexpr size_t DISCOVERY_PREFIX_MAX_LEN = 64; // Validated in Python: cv.Length(max=64)
|
||||
// Format: prefix + "/" + type + "/" + name + "/" + object_id + "/config" + null
|
||||
static constexpr size_t DISCOVERY_TOPIC_MAX_LEN = DISCOVERY_PREFIX_MAX_LEN + 1 + MQTT_COMPONENT_TYPE_MAX_LEN + 1 +
|
||||
ESPHOME_DEVICE_NAME_MAX_LEN + 1 + OBJECT_ID_MAX_LEN + 7 + 1;
|
||||
|
||||
// Function implementation of LOG_MQTT_COMPONENT macro to reduce code size
|
||||
void log_mqtt_component(const char *tag, MQTTComponent *obj, bool state_topic, bool command_topic) {
|
||||
@@ -51,15 +51,15 @@ void MQTTComponent::set_subscribe_qos(uint8_t qos) { this->subscribe_qos_ = qos;
|
||||
|
||||
void MQTTComponent::set_retain(bool retain) { this->retain_ = retain; }
|
||||
|
||||
StringRef MQTTComponent::get_discovery_topic_to_(std::span<char, MQTT_DISCOVERY_TOPIC_MAX_LEN> buf,
|
||||
const MQTTDiscoveryInfo &discovery_info) const {
|
||||
std::string MQTTComponent::get_discovery_topic_(const MQTTDiscoveryInfo &discovery_info) const {
|
||||
char sanitized_name[ESPHOME_DEVICE_NAME_MAX_LEN + 1];
|
||||
str_sanitize_to(sanitized_name, App.get_name().c_str());
|
||||
const char *comp_type = this->component_type();
|
||||
char object_id_buf[OBJECT_ID_MAX_LEN];
|
||||
StringRef object_id = this->get_default_object_id_to_(object_id_buf);
|
||||
|
||||
char *p = buf.data();
|
||||
char buf[DISCOVERY_TOPIC_MAX_LEN];
|
||||
char *p = buf;
|
||||
|
||||
p = append_str(p, discovery_info.prefix.data(), discovery_info.prefix.size());
|
||||
p = append_char(p, '/');
|
||||
@@ -69,9 +69,8 @@ StringRef MQTTComponent::get_discovery_topic_to_(std::span<char, MQTT_DISCOVERY_
|
||||
p = append_char(p, '/');
|
||||
p = append_str(p, object_id.c_str(), object_id.size());
|
||||
p = append_str(p, "/config", 7);
|
||||
*p = '\0';
|
||||
|
||||
return StringRef(buf.data(), p - buf.data());
|
||||
return std::string(buf, p - buf);
|
||||
}
|
||||
|
||||
StringRef MQTTComponent::get_default_topic_for_to_(std::span<char, MQTT_DEFAULT_TOPIC_MAX_LEN> buf, const char *suffix,
|
||||
@@ -180,19 +179,16 @@ bool MQTTComponent::publish_json(const char *topic, const json::json_build_t &f)
|
||||
bool MQTTComponent::send_discovery_() {
|
||||
const MQTTDiscoveryInfo &discovery_info = global_mqtt_client->get_discovery_info();
|
||||
|
||||
char discovery_topic_buf[MQTT_DISCOVERY_TOPIC_MAX_LEN];
|
||||
StringRef discovery_topic = this->get_discovery_topic_to_(discovery_topic_buf, discovery_info);
|
||||
|
||||
if (discovery_info.clean) {
|
||||
ESP_LOGV(TAG, "'%s': Cleaning discovery", this->friendly_name_().c_str());
|
||||
return global_mqtt_client->publish(discovery_topic.c_str(), "", 0, this->qos_, true);
|
||||
return global_mqtt_client->publish(this->get_discovery_topic_(discovery_info), "", 0, this->qos_, true);
|
||||
}
|
||||
|
||||
ESP_LOGV(TAG, "'%s': Sending discovery", this->friendly_name_().c_str());
|
||||
|
||||
// NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
|
||||
return global_mqtt_client->publish_json(
|
||||
discovery_topic.c_str(),
|
||||
this->get_discovery_topic_(discovery_info),
|
||||
[this](JsonObject root) {
|
||||
SendDiscoveryConfig config;
|
||||
config.state_topic = true;
|
||||
@@ -205,7 +201,7 @@ bool MQTTComponent::send_discovery_() {
|
||||
}
|
||||
|
||||
// Fields from EntityBase
|
||||
root[MQTT_NAME] = this->get_entity()->has_own_name() ? this->friendly_name_() : StringRef();
|
||||
root[MQTT_NAME] = this->get_entity()->has_own_name() ? this->friendly_name_() : "";
|
||||
|
||||
if (this->is_disabled_by_default_())
|
||||
root[MQTT_ENABLED_BY_DEFAULT] = false;
|
||||
@@ -217,9 +213,13 @@ bool MQTTComponent::send_discovery_() {
|
||||
// NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks)
|
||||
|
||||
const auto entity_category = this->get_entity()->get_entity_category();
|
||||
if (entity_category != ENTITY_CATEGORY_NONE) {
|
||||
root[MQTT_ENTITY_CATEGORY] = EntityCategoryMqttStrings::get_progmem_str(
|
||||
static_cast<uint8_t>(entity_category), static_cast<uint8_t>(ENTITY_CATEGORY_CONFIG));
|
||||
switch (entity_category) {
|
||||
case ENTITY_CATEGORY_NONE:
|
||||
break;
|
||||
case ENTITY_CATEGORY_CONFIG:
|
||||
case ENTITY_CATEGORY_DIAGNOSTIC:
|
||||
root[MQTT_ENTITY_CATEGORY] = entity_category == ENTITY_CATEGORY_CONFIG ? "config" : "diagnostic";
|
||||
break;
|
||||
}
|
||||
|
||||
if (config.state_topic) {
|
||||
@@ -249,7 +249,7 @@ bool MQTTComponent::send_discovery_() {
|
||||
if (discovery_info.unique_id_generator == MQTT_MAC_ADDRESS_UNIQUE_ID_GENERATOR) {
|
||||
char friendly_name_hash[9];
|
||||
buf_append_printf(friendly_name_hash, sizeof(friendly_name_hash), 0, "%08" PRIx32,
|
||||
fnv1_hash(this->friendly_name_().c_str()));
|
||||
fnv1_hash(this->friendly_name_()));
|
||||
// Format: mac-component_type-hash (e.g. "aabbccddeeff-sensor-12345678")
|
||||
// MAC (12) + "-" (1) + domain (max 20) + "-" (1) + hash (8) + null (1) = 43
|
||||
char unique_id[MAC_ADDRESS_BUFFER_SIZE + ESPHOME_DOMAIN_MAX_LEN + 11];
|
||||
@@ -415,7 +415,7 @@ void MQTTComponent::schedule_resend_state() { this->resend_state_ = true; }
|
||||
bool MQTTComponent::is_connected_() const { return global_mqtt_client->is_connected(); }
|
||||
|
||||
// Pull these properties from EntityBase if not overridden
|
||||
const StringRef &MQTTComponent::friendly_name_() const { return this->get_entity()->get_name(); }
|
||||
std::string MQTTComponent::friendly_name_() const { return this->get_entity()->get_name(); }
|
||||
StringRef MQTTComponent::get_default_object_id_to_(std::span<char, OBJECT_ID_MAX_LEN> buf) const {
|
||||
return this->get_entity()->get_object_id_to(buf);
|
||||
}
|
||||
|
||||
@@ -32,10 +32,6 @@ static constexpr size_t MQTT_TOPIC_PREFIX_MAX_LEN = 64; // Validated in Python:
|
||||
// Format: prefix + "/" + type + "/" + object_id + "/" + suffix + null
|
||||
static constexpr size_t MQTT_DEFAULT_TOPIC_MAX_LEN =
|
||||
MQTT_TOPIC_PREFIX_MAX_LEN + 1 + MQTT_COMPONENT_TYPE_MAX_LEN + 1 + OBJECT_ID_MAX_LEN + 1 + MQTT_SUFFIX_MAX_LEN + 1;
|
||||
static constexpr size_t MQTT_DISCOVERY_PREFIX_MAX_LEN = 64; // Validated in Python: cv.Length(max=64)
|
||||
// Format: prefix + "/" + type + "/" + name + "/" + object_id + "/config" + null
|
||||
static constexpr size_t MQTT_DISCOVERY_TOPIC_MAX_LEN = MQTT_DISCOVERY_PREFIX_MAX_LEN + 1 + MQTT_COMPONENT_TYPE_MAX_LEN +
|
||||
1 + ESPHOME_DEVICE_NAME_MAX_LEN + 1 + OBJECT_ID_MAX_LEN + 7 + 1;
|
||||
|
||||
class MQTTComponent; // Forward declaration
|
||||
void log_mqtt_component(const char *tag, MQTTComponent *obj, bool state_topic, bool command_topic);
|
||||
@@ -267,9 +263,8 @@ class MQTTComponent : public Component {
|
||||
void subscribe_json(const std::string &topic, const mqtt_json_callback_t &callback, uint8_t qos = 0);
|
||||
|
||||
protected:
|
||||
/// Helper method to get the discovery topic for this component into a buffer.
|
||||
StringRef get_discovery_topic_to_(std::span<char, MQTT_DISCOVERY_TOPIC_MAX_LEN> buf,
|
||||
const MQTTDiscoveryInfo &discovery_info) const;
|
||||
/// Helper method to get the discovery topic for this component.
|
||||
std::string get_discovery_topic_(const MQTTDiscoveryInfo &discovery_info) const;
|
||||
|
||||
/** Get this components state/command/... topic into a buffer.
|
||||
*
|
||||
@@ -293,7 +288,7 @@ class MQTTComponent : public Component {
|
||||
virtual const EntityBase *get_entity() const = 0;
|
||||
|
||||
/// Get the friendly name of this MQTT component.
|
||||
const StringRef &friendly_name_() const;
|
||||
std::string friendly_name_() const;
|
||||
|
||||
/// Get the icon field of this component as StringRef
|
||||
StringRef get_icon_ref_() const;
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
#include "mqtt_number.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/progmem.h"
|
||||
|
||||
#include "mqtt_const.h"
|
||||
|
||||
@@ -13,9 +12,6 @@ static const char *const TAG = "mqtt.number";
|
||||
|
||||
using namespace esphome::number;
|
||||
|
||||
// Number mode MQTT strings indexed by NumberMode enum: AUTO(0) is skipped, BOX(1), SLIDER(2)
|
||||
PROGMEM_STRING_TABLE(NumberMqttModeStrings, "", "box", "slider");
|
||||
|
||||
MQTTNumberComponent::MQTTNumberComponent(Number *number) : number_(number) {}
|
||||
|
||||
void MQTTNumberComponent::setup() {
|
||||
@@ -52,10 +48,15 @@ void MQTTNumberComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryCon
|
||||
if (!unit_of_measurement.empty()) {
|
||||
root[MQTT_UNIT_OF_MEASUREMENT] = unit_of_measurement;
|
||||
}
|
||||
const auto mode = this->number_->traits.get_mode();
|
||||
if (mode != NUMBER_MODE_AUTO) {
|
||||
root[MQTT_MODE] =
|
||||
NumberMqttModeStrings::get_progmem_str(static_cast<uint8_t>(mode), static_cast<uint8_t>(NUMBER_MODE_BOX));
|
||||
switch (this->number_->traits.get_mode()) {
|
||||
case NUMBER_MODE_AUTO:
|
||||
break;
|
||||
case NUMBER_MODE_BOX:
|
||||
root[MQTT_MODE] = "box";
|
||||
break;
|
||||
case NUMBER_MODE_SLIDER:
|
||||
root[MQTT_MODE] = "slider";
|
||||
break;
|
||||
}
|
||||
const auto device_class = this->number_->traits.get_device_class_ref();
|
||||
if (!device_class.empty()) {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
#include "mqtt_text.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/progmem.h"
|
||||
|
||||
#include "mqtt_const.h"
|
||||
|
||||
@@ -13,9 +12,6 @@ static const char *const TAG = "mqtt.text";
|
||||
|
||||
using namespace esphome::text;
|
||||
|
||||
// Text mode MQTT strings indexed by TextMode enum (0-1): TEXT, PASSWORD
|
||||
PROGMEM_STRING_TABLE(TextMqttModeStrings, "text", "password");
|
||||
|
||||
MQTTTextComponent::MQTTTextComponent(Text *text) : text_(text) {}
|
||||
|
||||
void MQTTTextComponent::setup() {
|
||||
@@ -38,8 +34,14 @@ const EntityBase *MQTTTextComponent::get_entity() const { return this->text_; }
|
||||
|
||||
void MQTTTextComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) {
|
||||
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
|
||||
root[MQTT_MODE] = TextMqttModeStrings::get_progmem_str(static_cast<uint8_t>(this->text_->traits.get_mode()),
|
||||
static_cast<uint8_t>(TEXT_MODE_TEXT));
|
||||
switch (this->text_->traits.get_mode()) {
|
||||
case TEXT_MODE_TEXT:
|
||||
root[MQTT_MODE] = "text";
|
||||
break;
|
||||
case TEXT_MODE_PASSWORD:
|
||||
root[MQTT_MODE] = "password";
|
||||
break;
|
||||
}
|
||||
|
||||
config.command_topic = true;
|
||||
}
|
||||
|
||||
@@ -72,55 +72,53 @@ void MS8607Component::setup() {
|
||||
|
||||
// I do not know why the device sometimes NACKs the reset command, but
|
||||
// try 3 times in case it's a transitory issue on this boot
|
||||
// Backoff: executes at now, +5ms, +30ms
|
||||
this->reset_attempts_remaining_ = 3;
|
||||
this->reset_interval_ = 5;
|
||||
this->try_reset_();
|
||||
}
|
||||
this->set_retry(
|
||||
"reset", 5, 3,
|
||||
[this](const uint8_t remaining_setup_attempts) {
|
||||
ESP_LOGD(TAG, "Resetting both I2C addresses: 0x%02X, 0x%02X", this->address_,
|
||||
this->humidity_device_->get_address());
|
||||
// I believe sending the reset command to both addresses is preferable to
|
||||
// skipping humidity if PT fails for some reason.
|
||||
// However, only consider the reset successful if they both ACK
|
||||
bool const pt_successful = this->write_bytes(MS8607_PT_CMD_RESET, nullptr, 0);
|
||||
bool const h_successful = this->humidity_device_->write_bytes(MS8607_CMD_H_RESET, nullptr, 0);
|
||||
|
||||
void MS8607Component::try_reset_() {
|
||||
ESP_LOGD(TAG, "Resetting both I2C addresses: 0x%02X, 0x%02X", this->address_, this->humidity_device_->get_address());
|
||||
// I believe sending the reset command to both addresses is preferable to
|
||||
// skipping humidity if PT fails for some reason.
|
||||
// However, only consider the reset successful if they both ACK
|
||||
bool const pt_successful = this->write_bytes(MS8607_PT_CMD_RESET, nullptr, 0);
|
||||
bool const h_successful = this->humidity_device_->write_bytes(MS8607_CMD_H_RESET, nullptr, 0);
|
||||
if (!(pt_successful && h_successful)) {
|
||||
ESP_LOGE(TAG, "Resetting I2C devices failed");
|
||||
if (!pt_successful && !h_successful) {
|
||||
this->error_code_ = ErrorCode::PTH_RESET_FAILED;
|
||||
} else if (!pt_successful) {
|
||||
this->error_code_ = ErrorCode::PT_RESET_FAILED;
|
||||
} else {
|
||||
this->error_code_ = ErrorCode::H_RESET_FAILED;
|
||||
}
|
||||
|
||||
if (!(pt_successful && h_successful)) {
|
||||
ESP_LOGE(TAG, "Resetting I2C devices failed");
|
||||
if (!pt_successful && !h_successful) {
|
||||
this->error_code_ = ErrorCode::PTH_RESET_FAILED;
|
||||
} else if (!pt_successful) {
|
||||
this->error_code_ = ErrorCode::PT_RESET_FAILED;
|
||||
} else {
|
||||
this->error_code_ = ErrorCode::H_RESET_FAILED;
|
||||
}
|
||||
if (remaining_setup_attempts > 0) {
|
||||
this->status_set_error();
|
||||
} else {
|
||||
this->mark_failed();
|
||||
}
|
||||
return RetryResult::RETRY;
|
||||
}
|
||||
|
||||
if (--this->reset_attempts_remaining_ > 0) {
|
||||
uint32_t delay = this->reset_interval_;
|
||||
this->reset_interval_ *= 5;
|
||||
this->set_timeout("reset", delay, [this]() { this->try_reset_(); });
|
||||
this->status_set_error();
|
||||
} else {
|
||||
this->mark_failed();
|
||||
}
|
||||
return;
|
||||
}
|
||||
this->setup_status_ = SetupStatus::NEEDS_PROM_READ;
|
||||
this->error_code_ = ErrorCode::NONE;
|
||||
this->status_clear_error();
|
||||
|
||||
this->setup_status_ = SetupStatus::NEEDS_PROM_READ;
|
||||
this->error_code_ = ErrorCode::NONE;
|
||||
this->status_clear_error();
|
||||
// 15ms delay matches datasheet, Adafruit_MS8607 & SparkFun_PHT_MS8607_Arduino_Library
|
||||
this->set_timeout("prom-read", 15, [this]() {
|
||||
if (this->read_calibration_values_from_prom_()) {
|
||||
this->setup_status_ = SetupStatus::SUCCESSFUL;
|
||||
this->status_clear_error();
|
||||
} else {
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
// 15ms delay matches datasheet, Adafruit_MS8607 & SparkFun_PHT_MS8607_Arduino_Library
|
||||
this->set_timeout("prom-read", 15, [this]() {
|
||||
if (this->read_calibration_values_from_prom_()) {
|
||||
this->setup_status_ = SetupStatus::SUCCESSFUL;
|
||||
this->status_clear_error();
|
||||
} else {
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
});
|
||||
return RetryResult::DONE;
|
||||
},
|
||||
5.0f); // executes at now, +5ms, +25ms
|
||||
}
|
||||
|
||||
void MS8607Component::update() {
|
||||
|
||||
@@ -44,8 +44,6 @@ class MS8607Component : public PollingComponent, public i2c::I2CDevice {
|
||||
void set_humidity_device(MS8607HumidityDevice *humidity_device) { humidity_device_ = humidity_device; }
|
||||
|
||||
protected:
|
||||
/// Attempt to reset both I2C devices, retrying with backoff on failure
|
||||
void try_reset_();
|
||||
/**
|
||||
Read and store the Pressure & Temperature calibration settings from the PROM.
|
||||
Intended to be called during setup(), this will set the `failure_reason_`
|
||||
@@ -104,8 +102,6 @@ class MS8607Component : public PollingComponent, public i2c::I2CDevice {
|
||||
enum class SetupStatus;
|
||||
/// Current step in the multi-step & possibly delayed setup() process
|
||||
SetupStatus setup_status_;
|
||||
uint32_t reset_interval_{5};
|
||||
uint8_t reset_attempts_remaining_{0};
|
||||
};
|
||||
|
||||
} // namespace ms8607
|
||||
|
||||
@@ -397,17 +397,11 @@ bool Nextion::remove_from_q_(bool report_empty) {
|
||||
}
|
||||
|
||||
void Nextion::process_serial_() {
|
||||
// Read all available bytes in batches to reduce UART call overhead.
|
||||
int avail = this->available();
|
||||
uint8_t buf[64];
|
||||
while (avail > 0) {
|
||||
size_t to_read = std::min(static_cast<size_t>(avail), sizeof(buf));
|
||||
if (!this->read_array(buf, to_read)) {
|
||||
break;
|
||||
}
|
||||
avail -= to_read;
|
||||
uint8_t d;
|
||||
|
||||
this->command_data_.append(reinterpret_cast<const char *>(buf), to_read);
|
||||
while (this->available()) {
|
||||
read_byte(&d);
|
||||
this->command_data_ += d;
|
||||
}
|
||||
}
|
||||
// nextion.tech/instruction-set/
|
||||
|
||||
@@ -396,9 +396,9 @@ static bool process_rolling_code(Provider &provider, PacketDecoder &decoder) {
|
||||
/**
|
||||
* Process a received packet
|
||||
*/
|
||||
void PacketTransport::process_(std::span<const uint8_t> data) {
|
||||
void PacketTransport::process_(const std::vector<uint8_t> &data) {
|
||||
auto ping_key_seen = !this->ping_pong_enable_;
|
||||
PacketDecoder decoder(data.data(), data.size());
|
||||
PacketDecoder decoder((data.data()), data.size());
|
||||
char namebuf[256]{};
|
||||
uint8_t byte;
|
||||
FuData rdata{};
|
||||
|
||||
@@ -9,9 +9,8 @@
|
||||
#include "esphome/components/binary_sensor/binary_sensor.h"
|
||||
#endif
|
||||
|
||||
#include <map>
|
||||
#include <span>
|
||||
#include <vector>
|
||||
#include <map>
|
||||
|
||||
/**
|
||||
* Providing packet encoding functions for exchanging data with a remote host.
|
||||
@@ -114,7 +113,7 @@ class PacketTransport : public PollingComponent {
|
||||
virtual bool should_send() { return true; }
|
||||
|
||||
// to be called by child classes when a data packet is received.
|
||||
void process_(std::span<const uint8_t> data);
|
||||
void process_(const std::vector<uint8_t> &data);
|
||||
void send_data_(bool all);
|
||||
void flush_();
|
||||
void add_data_(uint8_t key, const char *id, float data);
|
||||
|
||||
@@ -13,12 +13,9 @@ void Pipsolar::setup() {
|
||||
}
|
||||
|
||||
void Pipsolar::empty_uart_buffer_() {
|
||||
uint8_t buf[64];
|
||||
int avail;
|
||||
while ((avail = this->available()) > 0) {
|
||||
if (!this->read_array(buf, std::min(static_cast<size_t>(avail), sizeof(buf)))) {
|
||||
break;
|
||||
}
|
||||
uint8_t byte;
|
||||
while (this->available()) {
|
||||
this->read_byte(&byte);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -97,47 +94,32 @@ void Pipsolar::loop() {
|
||||
}
|
||||
|
||||
if (this->state_ == STATE_COMMAND || this->state_ == STATE_POLL) {
|
||||
int avail = this->available();
|
||||
while (avail > 0) {
|
||||
uint8_t buf[64];
|
||||
size_t to_read = std::min(static_cast<size_t>(avail), sizeof(buf));
|
||||
if (!this->read_array(buf, to_read)) {
|
||||
while (this->available()) {
|
||||
uint8_t byte;
|
||||
this->read_byte(&byte);
|
||||
|
||||
// make sure data and null terminator fit in buffer
|
||||
if (this->read_pos_ >= PIPSOLAR_READ_BUFFER_LENGTH - 1) {
|
||||
this->read_pos_ = 0;
|
||||
this->empty_uart_buffer_();
|
||||
ESP_LOGW(TAG, "response data too long, discarding.");
|
||||
break;
|
||||
}
|
||||
avail -= to_read;
|
||||
bool done = false;
|
||||
for (size_t i = 0; i < to_read; i++) {
|
||||
uint8_t byte = buf[i];
|
||||
this->read_buffer_[this->read_pos_] = byte;
|
||||
this->read_pos_++;
|
||||
|
||||
// make sure data and null terminator fit in buffer
|
||||
if (this->read_pos_ >= PIPSOLAR_READ_BUFFER_LENGTH - 1) {
|
||||
this->read_pos_ = 0;
|
||||
this->empty_uart_buffer_();
|
||||
ESP_LOGW(TAG, "response data too long, discarding.");
|
||||
done = true;
|
||||
break;
|
||||
// end of answer
|
||||
if (byte == 0x0D) {
|
||||
this->read_buffer_[this->read_pos_] = 0;
|
||||
this->empty_uart_buffer_();
|
||||
if (this->state_ == STATE_POLL) {
|
||||
this->state_ = STATE_POLL_COMPLETE;
|
||||
}
|
||||
this->read_buffer_[this->read_pos_] = byte;
|
||||
this->read_pos_++;
|
||||
|
||||
// end of answer
|
||||
if (byte == 0x0D) {
|
||||
this->read_buffer_[this->read_pos_] = 0;
|
||||
this->empty_uart_buffer_();
|
||||
if (this->state_ == STATE_POLL) {
|
||||
this->state_ = STATE_POLL_COMPLETE;
|
||||
}
|
||||
if (this->state_ == STATE_COMMAND) {
|
||||
this->state_ = STATE_COMMAND_COMPLETE;
|
||||
}
|
||||
done = true;
|
||||
break;
|
||||
if (this->state_ == STATE_COMMAND) {
|
||||
this->state_ = STATE_COMMAND_COMPLETE;
|
||||
}
|
||||
}
|
||||
if (done) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
} // available
|
||||
}
|
||||
if (this->state_ == STATE_COMMAND) {
|
||||
if (millis() - this->command_start_millis_ > esphome::pipsolar::Pipsolar::COMMAND_TIMEOUT) {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
#include "rd03d.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include <cmath>
|
||||
|
||||
@@ -81,47 +80,37 @@ void RD03DComponent::dump_config() {
|
||||
}
|
||||
|
||||
void RD03DComponent::loop() {
|
||||
// Read all available bytes in batches to reduce UART call overhead.
|
||||
int avail = this->available();
|
||||
uint8_t buf[64];
|
||||
while (avail > 0) {
|
||||
size_t to_read = std::min(static_cast<size_t>(avail), sizeof(buf));
|
||||
if (!this->read_array(buf, to_read)) {
|
||||
break;
|
||||
}
|
||||
avail -= to_read;
|
||||
for (size_t i = 0; i < to_read; i++) {
|
||||
uint8_t byte = buf[i];
|
||||
ESP_LOGVV(TAG, "Received byte: 0x%02X, buffer_pos: %d", byte, this->buffer_pos_);
|
||||
while (this->available()) {
|
||||
uint8_t byte = this->read();
|
||||
ESP_LOGVV(TAG, "Received byte: 0x%02X, buffer_pos: %d", byte, this->buffer_pos_);
|
||||
|
||||
// Check if we're looking for frame header
|
||||
if (this->buffer_pos_ < FRAME_HEADER_SIZE) {
|
||||
if (byte == FRAME_HEADER[this->buffer_pos_]) {
|
||||
this->buffer_[this->buffer_pos_++] = byte;
|
||||
} else if (byte == FRAME_HEADER[0]) {
|
||||
// Start over if we see a potential new header
|
||||
this->buffer_[0] = byte;
|
||||
this->buffer_pos_ = 1;
|
||||
} else {
|
||||
this->buffer_pos_ = 0;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Accumulate data bytes
|
||||
this->buffer_[this->buffer_pos_++] = byte;
|
||||
|
||||
// Check if we have a complete frame
|
||||
if (this->buffer_pos_ == FRAME_SIZE) {
|
||||
// Validate footer
|
||||
if (this->buffer_[FRAME_SIZE - 2] == FRAME_FOOTER[0] && this->buffer_[FRAME_SIZE - 1] == FRAME_FOOTER[1]) {
|
||||
this->process_frame_();
|
||||
} else {
|
||||
ESP_LOGW(TAG, "Invalid frame footer: 0x%02X 0x%02X (expected 0x55 0xCC)", this->buffer_[FRAME_SIZE - 2],
|
||||
this->buffer_[FRAME_SIZE - 1]);
|
||||
}
|
||||
// Check if we're looking for frame header
|
||||
if (this->buffer_pos_ < FRAME_HEADER_SIZE) {
|
||||
if (byte == FRAME_HEADER[this->buffer_pos_]) {
|
||||
this->buffer_[this->buffer_pos_++] = byte;
|
||||
} else if (byte == FRAME_HEADER[0]) {
|
||||
// Start over if we see a potential new header
|
||||
this->buffer_[0] = byte;
|
||||
this->buffer_pos_ = 1;
|
||||
} else {
|
||||
this->buffer_pos_ = 0;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Accumulate data bytes
|
||||
this->buffer_[this->buffer_pos_++] = byte;
|
||||
|
||||
// Check if we have a complete frame
|
||||
if (this->buffer_pos_ == FRAME_SIZE) {
|
||||
// Validate footer
|
||||
if (this->buffer_[FRAME_SIZE - 2] == FRAME_FOOTER[0] && this->buffer_[FRAME_SIZE - 1] == FRAME_FOOTER[1]) {
|
||||
this->process_frame_();
|
||||
} else {
|
||||
ESP_LOGW(TAG, "Invalid frame footer: 0x%02X 0x%02X (expected 0x55 0xCC)", this->buffer_[FRAME_SIZE - 2],
|
||||
this->buffer_[FRAME_SIZE - 1]);
|
||||
}
|
||||
this->buffer_pos_ = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -136,21 +136,14 @@ void RFBridgeComponent::loop() {
|
||||
this->last_bridge_byte_ = now;
|
||||
}
|
||||
|
||||
int avail = this->available();
|
||||
while (avail > 0) {
|
||||
uint8_t buf[64];
|
||||
size_t to_read = std::min(static_cast<size_t>(avail), sizeof(buf));
|
||||
if (!this->read_array(buf, to_read)) {
|
||||
break;
|
||||
}
|
||||
avail -= to_read;
|
||||
for (size_t i = 0; i < to_read; i++) {
|
||||
if (this->parse_bridge_byte_(buf[i])) {
|
||||
ESP_LOGVV(TAG, "Parsed: 0x%02X", buf[i]);
|
||||
this->last_bridge_byte_ = now;
|
||||
} else {
|
||||
this->rx_buffer_.clear();
|
||||
}
|
||||
while (this->available()) {
|
||||
uint8_t byte;
|
||||
this->read_byte(&byte);
|
||||
if (this->parse_bridge_byte_(byte)) {
|
||||
ESP_LOGVV(TAG, "Parsed: 0x%02X", byte);
|
||||
this->last_bridge_byte_ = now;
|
||||
} else {
|
||||
this->rx_buffer_.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -106,19 +106,12 @@ void MR24HPC1Component::update_() {
|
||||
|
||||
// main loop
|
||||
void MR24HPC1Component::loop() {
|
||||
// Read all available bytes in batches to reduce UART call overhead.
|
||||
int avail = this->available();
|
||||
uint8_t buf[64];
|
||||
while (avail > 0) {
|
||||
size_t to_read = std::min(static_cast<size_t>(avail), sizeof(buf));
|
||||
if (!this->read_array(buf, to_read)) {
|
||||
break;
|
||||
}
|
||||
avail -= to_read;
|
||||
uint8_t byte;
|
||||
|
||||
for (size_t i = 0; i < to_read; i++) {
|
||||
this->r24_split_data_frame_(buf[i]); // split data frame
|
||||
}
|
||||
// Is there data on the serial port
|
||||
while (this->available()) {
|
||||
this->read_byte(&byte);
|
||||
this->r24_split_data_frame_(byte); // split data frame
|
||||
}
|
||||
|
||||
if ((this->s_output_info_switch_flag_ == OUTPUT_SWTICH_OFF) &&
|
||||
|
||||
@@ -30,21 +30,14 @@ void MR60BHA2Component::dump_config() {
|
||||
|
||||
// main loop
|
||||
void MR60BHA2Component::loop() {
|
||||
// Read all available bytes in batches to reduce UART call overhead.
|
||||
int avail = this->available();
|
||||
uint8_t buf[64];
|
||||
while (avail > 0) {
|
||||
size_t to_read = std::min(static_cast<size_t>(avail), sizeof(buf));
|
||||
if (!this->read_array(buf, to_read)) {
|
||||
break;
|
||||
}
|
||||
avail -= to_read;
|
||||
uint8_t byte;
|
||||
|
||||
for (size_t i = 0; i < to_read; i++) {
|
||||
this->rx_message_.push_back(buf[i]);
|
||||
if (!this->validate_message_()) {
|
||||
this->rx_message_.clear();
|
||||
}
|
||||
// Is there data on the serial port
|
||||
while (this->available()) {
|
||||
this->read_byte(&byte);
|
||||
this->rx_message_.push_back(byte);
|
||||
if (!this->validate_message_()) {
|
||||
this->rx_message_.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,19 +49,12 @@ void MR60FDA2Component::setup() {
|
||||
|
||||
// main loop
|
||||
void MR60FDA2Component::loop() {
|
||||
// Read all available bytes in batches to reduce UART call overhead.
|
||||
int avail = this->available();
|
||||
uint8_t buf[64];
|
||||
while (avail > 0) {
|
||||
size_t to_read = std::min(static_cast<size_t>(avail), sizeof(buf));
|
||||
if (!this->read_array(buf, to_read)) {
|
||||
break;
|
||||
}
|
||||
avail -= to_read;
|
||||
uint8_t byte;
|
||||
|
||||
for (size_t i = 0; i < to_read; i++) {
|
||||
this->split_frame_(buf[i]); // split data frame
|
||||
}
|
||||
// Is there data on the serial port
|
||||
while (this->available()) {
|
||||
this->read_byte(&byte);
|
||||
this->split_frame_(byte); // split data frame
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -9,11 +9,6 @@ namespace esphome::sensor {
|
||||
|
||||
static const char *const TAG = "sensor.filter";
|
||||
|
||||
// Filter scheduler IDs.
|
||||
// Each filter is its own Component instance, so the scheduler scopes
|
||||
// IDs by component pointer — no risk of collisions between instances.
|
||||
constexpr uint32_t FILTER_ID = 0;
|
||||
|
||||
// Filter
|
||||
void Filter::input(float value) {
|
||||
ESP_LOGVV(TAG, "Filter(%p)::input(%f)", this, value);
|
||||
@@ -196,7 +191,7 @@ optional<float> ThrottleAverageFilter::new_value(float value) {
|
||||
return {};
|
||||
}
|
||||
void ThrottleAverageFilter::setup() {
|
||||
this->set_interval(FILTER_ID, this->time_period_, [this]() {
|
||||
this->set_interval("throttle_average", this->time_period_, [this]() {
|
||||
ESP_LOGVV(TAG, "ThrottleAverageFilter(%p)::interval(sum=%f, n=%i)", this, this->sum_, this->n_);
|
||||
if (this->n_ == 0) {
|
||||
if (this->have_nan_)
|
||||
@@ -388,7 +383,7 @@ optional<float> TimeoutFilterConfigured::new_value(float value) {
|
||||
|
||||
// DebounceFilter
|
||||
optional<float> DebounceFilter::new_value(float value) {
|
||||
this->set_timeout(FILTER_ID, this->time_period_, [this, value]() { this->output(value); });
|
||||
this->set_timeout("debounce", this->time_period_, [this, value]() { this->output(value); });
|
||||
|
||||
return {};
|
||||
}
|
||||
@@ -411,7 +406,7 @@ optional<float> HeartbeatFilter::new_value(float value) {
|
||||
}
|
||||
|
||||
void HeartbeatFilter::setup() {
|
||||
this->set_interval(FILTER_ID, this->time_period_, [this]() {
|
||||
this->set_interval("heartbeat", this->time_period_, [this]() {
|
||||
ESP_LOGVV(TAG, "HeartbeatFilter(%p)::interval(has_value=%s, last_input=%f)", this, YESNO(this->has_value_),
|
||||
this->last_input_);
|
||||
if (!this->has_value_)
|
||||
|
||||
@@ -103,20 +103,6 @@ void SpeakerMediaPlayer::set_playlist_delay_ms(AudioPipelineType pipeline_type,
|
||||
}
|
||||
}
|
||||
|
||||
void SpeakerMediaPlayer::stop_and_unpause_media_() {
|
||||
this->media_pipeline_->stop();
|
||||
this->unpause_media_remaining_ = 3;
|
||||
this->set_interval("unpause_med", 50, [this]() {
|
||||
if (this->media_pipeline_state_ == AudioPipelineState::STOPPED) {
|
||||
this->cancel_interval("unpause_med");
|
||||
this->media_pipeline_->set_pause_state(false);
|
||||
this->is_paused_ = false;
|
||||
} else if (--this->unpause_media_remaining_ == 0) {
|
||||
this->cancel_interval("unpause_med");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void SpeakerMediaPlayer::watch_media_commands_() {
|
||||
if (!this->is_ready()) {
|
||||
return;
|
||||
@@ -158,7 +144,15 @@ void SpeakerMediaPlayer::watch_media_commands_() {
|
||||
if (this->is_paused_) {
|
||||
// If paused, stop the media pipeline and unpause it after confirming its stopped. This avoids playing a
|
||||
// short segment of the paused file before starting the new one.
|
||||
this->stop_and_unpause_media_();
|
||||
this->media_pipeline_->stop();
|
||||
this->set_retry("unpause_med", 50, 3, [this](const uint8_t remaining_attempts) {
|
||||
if (this->media_pipeline_state_ == AudioPipelineState::STOPPED) {
|
||||
this->media_pipeline_->set_pause_state(false);
|
||||
this->is_paused_ = false;
|
||||
return RetryResult::DONE;
|
||||
}
|
||||
return RetryResult::RETRY;
|
||||
});
|
||||
} else {
|
||||
// Not paused, just directly start the file
|
||||
if (media_command.file.has_value()) {
|
||||
@@ -203,21 +197,27 @@ void SpeakerMediaPlayer::watch_media_commands_() {
|
||||
this->cancel_timeout("next_ann");
|
||||
this->announcement_playlist_.clear();
|
||||
this->announcement_pipeline_->stop();
|
||||
this->unpause_announcement_remaining_ = 3;
|
||||
this->set_interval("unpause_ann", 50, [this]() {
|
||||
this->set_retry("unpause_ann", 50, 3, [this](const uint8_t remaining_attempts) {
|
||||
if (this->announcement_pipeline_state_ == AudioPipelineState::STOPPED) {
|
||||
this->cancel_interval("unpause_ann");
|
||||
this->announcement_pipeline_->set_pause_state(false);
|
||||
} else if (--this->unpause_announcement_remaining_ == 0) {
|
||||
this->cancel_interval("unpause_ann");
|
||||
return RetryResult::DONE;
|
||||
}
|
||||
return RetryResult::RETRY;
|
||||
});
|
||||
}
|
||||
} else {
|
||||
if (this->media_pipeline_ != nullptr) {
|
||||
this->cancel_timeout("next_media");
|
||||
this->media_playlist_.clear();
|
||||
this->stop_and_unpause_media_();
|
||||
this->media_pipeline_->stop();
|
||||
this->set_retry("unpause_med", 50, 3, [this](const uint8_t remaining_attempts) {
|
||||
if (this->media_pipeline_state_ == AudioPipelineState::STOPPED) {
|
||||
this->media_pipeline_->set_pause_state(false);
|
||||
this->is_paused_ = false;
|
||||
return RetryResult::DONE;
|
||||
}
|
||||
return RetryResult::RETRY;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -112,9 +112,6 @@ class SpeakerMediaPlayer : public Component,
|
||||
/// media pipelines are defined.
|
||||
inline bool single_pipeline_() { return (this->media_speaker_ == nullptr); }
|
||||
|
||||
/// Stops the media pipeline and polls until stopped to unpause it, avoiding an audible glitch.
|
||||
void stop_and_unpause_media_();
|
||||
|
||||
// Processes commands from media_control_command_queue_.
|
||||
void watch_media_commands_();
|
||||
|
||||
@@ -144,8 +141,6 @@ class SpeakerMediaPlayer : public Component,
|
||||
|
||||
bool is_paused_{false};
|
||||
bool is_muted_{false};
|
||||
uint8_t unpause_media_remaining_{0};
|
||||
uint8_t unpause_announcement_remaining_{0};
|
||||
|
||||
// The amount to change the volume on volume up/down commands
|
||||
float volume_increment_;
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
#include "esphome/core/application.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/progmem.h"
|
||||
|
||||
namespace esphome::template_ {
|
||||
|
||||
@@ -29,11 +28,18 @@ void TemplateAlarmControlPanel::add_sensor(binary_sensor::BinarySensor *sensor,
|
||||
this->sensor_data_.push_back(sd);
|
||||
};
|
||||
|
||||
// Alarm sensor type strings indexed by AlarmSensorType enum (0-3): DELAYED, INSTANT, DELAYED_FOLLOWER, INSTANT_ALWAYS
|
||||
PROGMEM_STRING_TABLE(AlarmSensorTypeStrings, "delayed", "instant", "delayed_follower", "instant_always");
|
||||
|
||||
static const LogString *sensor_type_to_string(AlarmSensorType type) {
|
||||
return AlarmSensorTypeStrings::get_log_str(static_cast<uint8_t>(type), 0);
|
||||
switch (type) {
|
||||
case ALARM_SENSOR_TYPE_INSTANT:
|
||||
return LOG_STR("instant");
|
||||
case ALARM_SENSOR_TYPE_DELAYED_FOLLOWER:
|
||||
return LOG_STR("delayed_follower");
|
||||
case ALARM_SENSOR_TYPE_INSTANT_ALWAYS:
|
||||
return LOG_STR("instant_always");
|
||||
case ALARM_SENSOR_TYPE_DELAYED:
|
||||
default:
|
||||
return LOG_STR("delayed");
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ enum BinarySensorFlags : uint16_t {
|
||||
BINARY_SENSOR_MODE_BYPASS_AUTO = 1 << 4,
|
||||
};
|
||||
|
||||
enum AlarmSensorType : uint8_t {
|
||||
enum AlarmSensorType : uint16_t {
|
||||
ALARM_SENSOR_TYPE_DELAYED = 0,
|
||||
ALARM_SENSOR_TYPE_INSTANT,
|
||||
ALARM_SENSOR_TYPE_DELAYED_FOLLOWER,
|
||||
|
||||
@@ -46,7 +46,6 @@ CONFIG_SCHEMA = (
|
||||
RESTORE_MODES, upper=True
|
||||
),
|
||||
cv.Optional(CONF_CURRENT_TEMPERATURE): cv.returning_lambda,
|
||||
cv.Optional(CONF_TARGET_TEMPERATURE): cv.returning_lambda,
|
||||
cv.Optional(CONF_MODE): cv.returning_lambda,
|
||||
cv.Optional(CONF_SUPPORTED_MODES): cv.ensure_list(
|
||||
water_heater.validate_water_heater_mode
|
||||
@@ -79,14 +78,6 @@ async def to_code(config: ConfigType) -> None:
|
||||
)
|
||||
cg.add(var.set_current_temperature_lambda(template_))
|
||||
|
||||
if CONF_TARGET_TEMPERATURE in config:
|
||||
template_ = await cg.process_lambda(
|
||||
config[CONF_TARGET_TEMPERATURE],
|
||||
[],
|
||||
return_type=cg.optional.template(cg.float_),
|
||||
)
|
||||
cg.add(var.set_target_temperature_lambda(template_))
|
||||
|
||||
if CONF_MODE in config:
|
||||
template_ = await cg.process_lambda(
|
||||
config[CONF_MODE],
|
||||
|
||||
@@ -16,8 +16,7 @@ void TemplateWaterHeater::setup() {
|
||||
restore->perform();
|
||||
}
|
||||
}
|
||||
if (!this->current_temperature_f_.has_value() && !this->target_temperature_f_.has_value() &&
|
||||
!this->mode_f_.has_value())
|
||||
if (!this->current_temperature_f_.has_value() && !this->mode_f_.has_value())
|
||||
this->disable_loop();
|
||||
}
|
||||
|
||||
@@ -29,9 +28,6 @@ water_heater::WaterHeaterTraits TemplateWaterHeater::traits() {
|
||||
}
|
||||
|
||||
traits.set_supports_current_temperature(true);
|
||||
if (this->target_temperature_f_.has_value()) {
|
||||
traits.add_feature_flags(water_heater::WATER_HEATER_SUPPORTS_TARGET_TEMPERATURE);
|
||||
}
|
||||
return traits;
|
||||
}
|
||||
|
||||
@@ -46,14 +42,6 @@ void TemplateWaterHeater::loop() {
|
||||
}
|
||||
}
|
||||
|
||||
auto target_temp = this->target_temperature_f_.call();
|
||||
if (target_temp.has_value()) {
|
||||
if (*target_temp != this->target_temperature_) {
|
||||
this->target_temperature_ = *target_temp;
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
auto new_mode = this->mode_f_.call();
|
||||
if (new_mode.has_value()) {
|
||||
if (*new_mode != this->mode_) {
|
||||
|
||||
@@ -20,9 +20,6 @@ class TemplateWaterHeater : public Component, public water_heater::WaterHeater {
|
||||
template<typename F> void set_current_temperature_lambda(F &&f) {
|
||||
this->current_temperature_f_.set(std::forward<F>(f));
|
||||
}
|
||||
template<typename F> void set_target_temperature_lambda(F &&f) {
|
||||
this->target_temperature_f_.set(std::forward<F>(f));
|
||||
}
|
||||
template<typename F> void set_mode_lambda(F &&f) { this->mode_f_.set(std::forward<F>(f)); }
|
||||
|
||||
void set_optimistic(bool optimistic) { this->optimistic_ = optimistic; }
|
||||
@@ -47,7 +44,6 @@ class TemplateWaterHeater : public Component, public water_heater::WaterHeater {
|
||||
// Ordered to minimize padding on 32-bit: 4-byte members first, then smaller
|
||||
Trigger<> set_trigger_;
|
||||
TemplateLambda<float> current_temperature_f_;
|
||||
TemplateLambda<float> target_temperature_f_;
|
||||
TemplateLambda<water_heater::WaterHeaterMode> mode_f_;
|
||||
TemplateWaterHeaterRestoreMode restore_mode_{WATER_HEATER_NO_RESTORE};
|
||||
water_heater::WaterHeaterModeMask supported_modes_;
|
||||
|
||||
@@ -31,19 +31,10 @@ void Tuya::setup() {
|
||||
}
|
||||
|
||||
void Tuya::loop() {
|
||||
// Read all available bytes in batches to reduce UART call overhead.
|
||||
int avail = this->available();
|
||||
uint8_t buf[64];
|
||||
while (avail > 0) {
|
||||
size_t to_read = std::min(static_cast<size_t>(avail), sizeof(buf));
|
||||
if (!this->read_array(buf, to_read)) {
|
||||
break;
|
||||
}
|
||||
avail -= to_read;
|
||||
|
||||
for (size_t i = 0; i < to_read; i++) {
|
||||
this->handle_char_(buf[i]);
|
||||
}
|
||||
while (this->available()) {
|
||||
uint8_t c;
|
||||
this->read_byte(&c);
|
||||
this->handle_char_(c);
|
||||
}
|
||||
process_command_queue_();
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ from esphome.components.packet_transport import (
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_DATA, CONF_ID, CONF_PORT, CONF_TRIGGER_ID
|
||||
from esphome.core import ID
|
||||
from esphome.cpp_generator import MockObj
|
||||
from esphome.cpp_generator import literal
|
||||
|
||||
CODEOWNERS = ["@clydebarrow"]
|
||||
DEPENDENCIES = ["network"]
|
||||
@@ -23,12 +23,8 @@ MULTI_CONF = True
|
||||
udp_ns = cg.esphome_ns.namespace("udp")
|
||||
UDPComponent = udp_ns.class_("UDPComponent", cg.Component)
|
||||
UDPWriteAction = udp_ns.class_("UDPWriteAction", automation.Action)
|
||||
trigger_argname = "data"
|
||||
# Listener callback type (non-owning span from UDP component)
|
||||
listener_args = cg.std_span.template(cg.uint8.operator("const"))
|
||||
listener_argtype = [(listener_args, trigger_argname)]
|
||||
# Automation/trigger type (owned vector, safe for deferred actions like delay)
|
||||
trigger_args = cg.std_vector.template(cg.uint8)
|
||||
trigger_argname = "data"
|
||||
trigger_argtype = [(trigger_args, trigger_argname)]
|
||||
|
||||
CONF_ADDRESSES = "addresses"
|
||||
@@ -122,13 +118,7 @@ async def to_code(config):
|
||||
trigger_id, trigger_argtype, on_receive
|
||||
)
|
||||
trigger_lambda = await cg.process_lambda(
|
||||
trigger.trigger(
|
||||
cg.std_vector.template(cg.uint8)(
|
||||
MockObj(trigger_argname).begin(),
|
||||
MockObj(trigger_argname).end(),
|
||||
)
|
||||
),
|
||||
listener_argtype,
|
||||
trigger.trigger(literal(trigger_argname)), trigger_argtype
|
||||
)
|
||||
cg.add(var.add_listener(trigger_lambda))
|
||||
cg.add(var.set_should_listen())
|
||||
|
||||
@@ -12,7 +12,7 @@ bool UDPTransport::should_send() { return network::is_connected(); }
|
||||
void UDPTransport::setup() {
|
||||
PacketTransport::setup();
|
||||
if (!this->providers_.empty() || this->is_encrypted_()) {
|
||||
this->parent_->add_listener([this](std::span<const uint8_t> data) { this->process_(data); });
|
||||
this->parent_->add_listener([this](std::vector<uint8_t> &buf) { this->process_(buf); });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -103,8 +103,8 @@ void UDPComponent::setup() {
|
||||
}
|
||||
|
||||
void UDPComponent::loop() {
|
||||
auto buf = std::vector<uint8_t>(MAX_PACKET_SIZE);
|
||||
if (this->should_listen_) {
|
||||
std::array<uint8_t, MAX_PACKET_SIZE> buf;
|
||||
for (;;) {
|
||||
#if defined(USE_SOCKET_IMPL_BSD_SOCKETS) || defined(USE_SOCKET_IMPL_LWIP_SOCKETS)
|
||||
auto len = this->listen_socket_->read(buf.data(), buf.size());
|
||||
@@ -116,9 +116,9 @@ void UDPComponent::loop() {
|
||||
#endif
|
||||
if (len <= 0)
|
||||
break;
|
||||
size_t packet_len = static_cast<size_t>(len);
|
||||
ESP_LOGV(TAG, "Received packet of length %zu", packet_len);
|
||||
this->packet_listeners_.call(std::span<const uint8_t>(buf.data(), packet_len));
|
||||
buf.resize(len);
|
||||
ESP_LOGV(TAG, "Received packet of length %zu", len);
|
||||
this->packet_listeners_.call(buf);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,9 +10,7 @@
|
||||
#ifdef USE_SOCKET_IMPL_LWIP_TCP
|
||||
#include <WiFiUdp.h>
|
||||
#endif
|
||||
#include <array>
|
||||
#include <initializer_list>
|
||||
#include <span>
|
||||
#include <vector>
|
||||
|
||||
namespace esphome::udp {
|
||||
@@ -28,7 +26,7 @@ class UDPComponent : public Component {
|
||||
void set_broadcast_port(uint16_t port) { this->broadcast_port_ = port; }
|
||||
void set_should_broadcast() { this->should_broadcast_ = true; }
|
||||
void set_should_listen() { this->should_listen_ = true; }
|
||||
void add_listener(std::function<void(std::span<const uint8_t>)> &&listener) {
|
||||
void add_listener(std::function<void(std::vector<uint8_t> &)> &&listener) {
|
||||
this->packet_listeners_.add(std::move(listener));
|
||||
}
|
||||
void setup() override;
|
||||
@@ -43,7 +41,7 @@ class UDPComponent : public Component {
|
||||
uint16_t broadcast_port_{};
|
||||
bool should_broadcast_{};
|
||||
bool should_listen_{};
|
||||
CallbackManager<void(std::span<const uint8_t>)> packet_listeners_{};
|
||||
CallbackManager<void(std::vector<uint8_t> &)> packet_listeners_{};
|
||||
|
||||
#if defined(USE_SOCKET_IMPL_BSD_SOCKETS) || defined(USE_SOCKET_IMPL_LWIP_SOCKETS)
|
||||
std::unique_ptr<socket::Socket> broadcast_socket_ = nullptr;
|
||||
|
||||
@@ -83,7 +83,7 @@ struct Timer {
|
||||
}
|
||||
// Remove before 2026.8.0
|
||||
ESPDEPRECATED("Use to_str() instead. Removed in 2026.8.0", "2026.2.0")
|
||||
std::string to_string() const { // NOLINT
|
||||
std::string to_string() const {
|
||||
char buffer[TO_STR_BUFFER_SIZE];
|
||||
return this->to_str(buffer);
|
||||
}
|
||||
|
||||
@@ -65,7 +65,6 @@ WaterHeaterCall &WaterHeaterCall::set_away(bool away) {
|
||||
} else {
|
||||
this->state_ &= ~WATER_HEATER_STATE_AWAY;
|
||||
}
|
||||
this->state_mask_ |= WATER_HEATER_STATE_AWAY;
|
||||
return *this;
|
||||
}
|
||||
|
||||
@@ -75,7 +74,6 @@ WaterHeaterCall &WaterHeaterCall::set_on(bool on) {
|
||||
} else {
|
||||
this->state_ &= ~WATER_HEATER_STATE_ON;
|
||||
}
|
||||
this->state_mask_ |= WATER_HEATER_STATE_ON;
|
||||
return *this;
|
||||
}
|
||||
|
||||
@@ -94,11 +92,11 @@ void WaterHeaterCall::perform() {
|
||||
if (!std::isnan(this->target_temperature_high_)) {
|
||||
ESP_LOGD(TAG, " Target Temperature High: %.2f", this->target_temperature_high_);
|
||||
}
|
||||
if (this->state_mask_ & WATER_HEATER_STATE_AWAY) {
|
||||
ESP_LOGD(TAG, " Away: %s", (this->state_ & WATER_HEATER_STATE_AWAY) ? "YES" : "NO");
|
||||
if (this->state_ & WATER_HEATER_STATE_AWAY) {
|
||||
ESP_LOGD(TAG, " Away: YES");
|
||||
}
|
||||
if (this->state_mask_ & WATER_HEATER_STATE_ON) {
|
||||
ESP_LOGD(TAG, " On: %s", (this->state_ & WATER_HEATER_STATE_ON) ? "YES" : "NO");
|
||||
if (this->state_ & WATER_HEATER_STATE_ON) {
|
||||
ESP_LOGD(TAG, " On: YES");
|
||||
}
|
||||
this->parent_->control(*this);
|
||||
}
|
||||
@@ -139,17 +137,13 @@ void WaterHeaterCall::validate_() {
|
||||
this->target_temperature_high_ = NAN;
|
||||
}
|
||||
}
|
||||
if (!traits.get_supports_away_mode()) {
|
||||
if (this->state_ & WATER_HEATER_STATE_AWAY) {
|
||||
ESP_LOGW(TAG, "'%s' - Away mode not supported", this->parent_->get_name().c_str());
|
||||
}
|
||||
if ((this->state_ & WATER_HEATER_STATE_AWAY) && !traits.get_supports_away_mode()) {
|
||||
ESP_LOGW(TAG, "'%s' - Away mode not supported", this->parent_->get_name().c_str());
|
||||
this->state_ &= ~WATER_HEATER_STATE_AWAY;
|
||||
this->state_mask_ &= ~WATER_HEATER_STATE_AWAY;
|
||||
}
|
||||
// If ON/OFF not supported, device is always on - clear the flag silently
|
||||
if (!traits.has_feature_flags(WATER_HEATER_SUPPORTS_ON_OFF)) {
|
||||
this->state_ &= ~WATER_HEATER_STATE_ON;
|
||||
this->state_mask_ &= ~WATER_HEATER_STATE_ON;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -91,8 +91,6 @@ class WaterHeaterCall {
|
||||
float get_target_temperature_high() const { return this->target_temperature_high_; }
|
||||
/// Get state flags value
|
||||
uint32_t get_state() const { return this->state_; }
|
||||
/// Get mask of state flags that are being changed
|
||||
uint32_t get_state_mask() const { return this->state_mask_; }
|
||||
|
||||
protected:
|
||||
void validate_();
|
||||
@@ -102,7 +100,6 @@ class WaterHeaterCall {
|
||||
float target_temperature_low_{NAN};
|
||||
float target_temperature_high_{NAN};
|
||||
uint32_t state_{0};
|
||||
uint32_t state_mask_{0};
|
||||
};
|
||||
|
||||
struct WaterHeaterCallInternal : public WaterHeaterCall {
|
||||
@@ -114,7 +111,6 @@ struct WaterHeaterCallInternal : public WaterHeaterCall {
|
||||
this->target_temperature_low_ = restore.target_temperature_low_;
|
||||
this->target_temperature_high_ = restore.target_temperature_high_;
|
||||
this->state_ = restore.state_;
|
||||
this->state_mask_ = restore.state_mask_;
|
||||
return *this;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
from pathlib import Path
|
||||
|
||||
import esphome.codegen as cg
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ID
|
||||
from esphome.core import CORE, coroutine_with_priority
|
||||
from esphome.coroutine import CoroPriority
|
||||
from esphome.helpers import copy_file_if_changed
|
||||
|
||||
CODEOWNERS = ["@esphome/core"]
|
||||
DEPENDENCIES = ["network"]
|
||||
@@ -52,15 +49,5 @@ async def to_code(config):
|
||||
CORE.add_platformio_option(
|
||||
"lib_ignore", ["ESPAsyncTCP", "AsyncTCP", "AsyncTCP_RP2040W"]
|
||||
)
|
||||
# ESPAsyncWebServer uses Hash library for sha1() on RP2040
|
||||
cg.add_library("Hash", None)
|
||||
# Fix Hash.h include conflict: Crypto-no-arduino (used by dsmr)
|
||||
# provides a Hash.h that shadows the framework's Hash library.
|
||||
# Prepend the framework Hash path so it's found first.
|
||||
copy_file_if_changed(
|
||||
Path(__file__).parent / "fix_rp2040_hash.py.script",
|
||||
CORE.relative_build_path("fix_rp2040_hash.py"),
|
||||
)
|
||||
cg.add_platformio_option("extra_scripts", ["pre:fix_rp2040_hash.py"])
|
||||
# https://github.com/ESP32Async/ESPAsyncWebServer/blob/main/library.json
|
||||
cg.add_library("ESP32Async/ESPAsyncWebServer", "3.9.6")
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
# ESPAsyncWebServer includes <Hash.h> expecting the Arduino-Pico framework's Hash
|
||||
# library (which provides sha1() functions). However, the Crypto-no-arduino library
|
||||
# (used by dsmr) also provides a Hash.h that can shadow the framework version when
|
||||
# PlatformIO's chain+ LDF mode auto-discovers it as a dependency.
|
||||
# Prepend the framework Hash path to CXXFLAGS so it is found first.
|
||||
import os
|
||||
|
||||
Import("env")
|
||||
framework_dir = env.PioPlatform().get_package_dir("framework-arduinopico")
|
||||
hash_src = os.path.join(framework_dir, "libraries", "Hash", "src")
|
||||
env.Prepend(CXXFLAGS=["-I" + hash_src])
|
||||
@@ -6,7 +6,8 @@
|
||||
#include <cstring>
|
||||
#include "multipart_parser.h"
|
||||
|
||||
namespace esphome::web_server_idf {
|
||||
namespace esphome {
|
||||
namespace web_server_idf {
|
||||
|
||||
static const char *const TAG = "multipart";
|
||||
|
||||
@@ -248,5 +249,6 @@ std::string str_trim(const std::string &str) {
|
||||
return str.substr(start, end - start + 1);
|
||||
}
|
||||
|
||||
} // namespace esphome::web_server_idf
|
||||
} // namespace web_server_idf
|
||||
} // namespace esphome
|
||||
#endif // defined(USE_ESP32) && defined(USE_WEBSERVER_OTA)
|
||||
|
||||
@@ -10,7 +10,8 @@
|
||||
#include <string>
|
||||
#include <utility>
|
||||
|
||||
namespace esphome::web_server_idf {
|
||||
namespace esphome {
|
||||
namespace web_server_idf {
|
||||
|
||||
// Wrapper around zorxx/multipart-parser for ESP-IDF OTA uploads
|
||||
class MultipartReader {
|
||||
@@ -80,5 +81,6 @@ bool parse_multipart_boundary(const char *content_type, const char **boundary_st
|
||||
// Trim whitespace from both ends of a string
|
||||
std::string str_trim(const std::string &str);
|
||||
|
||||
} // namespace esphome::web_server_idf
|
||||
} // namespace web_server_idf
|
||||
} // namespace esphome
|
||||
#endif // defined(USE_ESP32) && defined(USE_WEBSERVER_OTA)
|
||||
|
||||
@@ -8,7 +8,8 @@
|
||||
|
||||
#include "utils.h"
|
||||
|
||||
namespace esphome::web_server_idf {
|
||||
namespace esphome {
|
||||
namespace web_server_idf {
|
||||
|
||||
static const char *const TAG = "web_server_idf_utils";
|
||||
|
||||
@@ -118,5 +119,6 @@ const char *stristr(const char *haystack, const char *needle) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
} // namespace esphome::web_server_idf
|
||||
} // namespace web_server_idf
|
||||
} // namespace esphome
|
||||
#endif // USE_ESP32
|
||||
|
||||
@@ -5,7 +5,8 @@
|
||||
#include <string>
|
||||
#include "esphome/core/helpers.h"
|
||||
|
||||
namespace esphome::web_server_idf {
|
||||
namespace esphome {
|
||||
namespace web_server_idf {
|
||||
|
||||
/// Decode URL-encoded string in-place (e.g., %20 -> space, + -> space)
|
||||
/// Returns the new length of the decoded string
|
||||
@@ -28,5 +29,6 @@ bool str_ncmp_ci(const char *s1, const char *s2, size_t n);
|
||||
// Case-insensitive string search (like strstr but case-insensitive)
|
||||
const char *stristr(const char *haystack, const char *needle);
|
||||
|
||||
} // namespace esphome::web_server_idf
|
||||
} // namespace web_server_idf
|
||||
} // namespace esphome
|
||||
#endif // USE_ESP32
|
||||
|
||||
@@ -30,7 +30,8 @@
|
||||
#include <cerrno>
|
||||
#include <sys/socket.h>
|
||||
|
||||
namespace esphome::web_server_idf {
|
||||
namespace esphome {
|
||||
namespace web_server_idf {
|
||||
|
||||
#ifndef HTTPD_409
|
||||
#define HTTPD_409 "409 Conflict"
|
||||
@@ -257,6 +258,8 @@ StringRef AsyncWebServerRequest::url_to(std::span<char, URL_BUF_SIZE> buffer) co
|
||||
return StringRef(buffer.data(), decoded_len);
|
||||
}
|
||||
|
||||
std::string AsyncWebServerRequest::host() const { return this->get_header("Host").value(); }
|
||||
|
||||
void AsyncWebServerRequest::send(AsyncWebServerResponse *response) {
|
||||
httpd_resp_send(*this, response->get_content_data(), response->get_content_size());
|
||||
}
|
||||
@@ -894,6 +897,7 @@ esp_err_t AsyncWebServer::handle_multipart_upload_(httpd_req_t *r, const char *c
|
||||
}
|
||||
#endif // USE_WEBSERVER_OTA
|
||||
|
||||
} // namespace esphome::web_server_idf
|
||||
} // namespace web_server_idf
|
||||
} // namespace esphome
|
||||
|
||||
#endif // !defined(USE_ESP32)
|
||||
|
||||
@@ -121,6 +121,7 @@ class AsyncWebServerRequest {
|
||||
char buffer[URL_BUF_SIZE];
|
||||
return std::string(this->url_to(buffer));
|
||||
}
|
||||
std::string host() const;
|
||||
// NOLINTNEXTLINE(readability-identifier-naming)
|
||||
size_t contentLength() const { return this->req_->content_len; }
|
||||
|
||||
|
||||
@@ -71,11 +71,9 @@ def _validate_load_certificate(value):
|
||||
|
||||
|
||||
def validate_certificate(value):
|
||||
# _validate_load_certificate already calls cv.file_() internally,
|
||||
# but returns the parsed certificate object. We re-call cv.file_()
|
||||
# to get the resolved path string that the bundle walker can discover.
|
||||
_validate_load_certificate(value)
|
||||
return str(cv.file_(value))
|
||||
# Validation result should be the path, not the loaded certificate
|
||||
return value
|
||||
|
||||
|
||||
def _validate_load_private_key(key, cert_pw):
|
||||
|
||||
@@ -191,17 +191,15 @@ template<typename... Ts> class DelayAction : public Action<Ts...>, public Compon
|
||||
// instead of std::bind to avoid bind overhead (~16 bytes heap + faster execution)
|
||||
if constexpr (sizeof...(Ts) == 0) {
|
||||
App.scheduler.set_timer_common_(
|
||||
this, Scheduler::SchedulerItem::TIMEOUT, Scheduler::NameType::NUMERIC_ID_INTERNAL, nullptr,
|
||||
static_cast<uint32_t>(InternalSchedulerID::DELAY_ACTION), this->delay_.value(),
|
||||
this, Scheduler::SchedulerItem::TIMEOUT, Scheduler::NameType::STATIC_STRING, "delay", 0, this->delay_.value(),
|
||||
[this]() { this->play_next_(); },
|
||||
/* is_retry= */ false, /* skip_cancel= */ this->num_running_ > 1);
|
||||
} else {
|
||||
// For delays with arguments, use std::bind to preserve argument values
|
||||
// Arguments must be copied because original references may be invalid after delay
|
||||
auto f = std::bind(&DelayAction<Ts...>::play_next_, this, x...);
|
||||
App.scheduler.set_timer_common_(this, Scheduler::SchedulerItem::TIMEOUT, Scheduler::NameType::NUMERIC_ID_INTERNAL,
|
||||
nullptr, static_cast<uint32_t>(InternalSchedulerID::DELAY_ACTION),
|
||||
this->delay_.value(x...), std::move(f),
|
||||
App.scheduler.set_timer_common_(this, Scheduler::SchedulerItem::TIMEOUT, Scheduler::NameType::STATIC_STRING,
|
||||
"delay", 0, this->delay_.value(x...), std::move(f),
|
||||
/* is_retry= */ false, /* skip_cancel= */ this->num_running_ > 1);
|
||||
}
|
||||
}
|
||||
@@ -210,7 +208,7 @@ template<typename... Ts> class DelayAction : public Action<Ts...>, public Compon
|
||||
void play(const Ts &...x) override { /* ignore - see play_complex */
|
||||
}
|
||||
|
||||
void stop() override { this->cancel_timeout(InternalSchedulerID::DELAY_ACTION); }
|
||||
void stop() override { this->cancel_timeout("delay"); }
|
||||
};
|
||||
|
||||
template<typename... Ts> class LambdaAction : public Action<Ts...> {
|
||||
|
||||
@@ -152,10 +152,7 @@ void Component::set_retry(const std::string &name, uint32_t initial_wait_time, u
|
||||
|
||||
void Component::set_retry(const char *name, uint32_t initial_wait_time, uint8_t max_attempts,
|
||||
std::function<RetryResult(uint8_t)> &&f, float backoff_increase_factor) { // NOLINT
|
||||
#pragma GCC diagnostic push
|
||||
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
|
||||
App.scheduler.set_retry(this, name, initial_wait_time, max_attempts, std::move(f), backoff_increase_factor);
|
||||
#pragma GCC diagnostic pop
|
||||
}
|
||||
|
||||
bool Component::cancel_retry(const std::string &name) { // NOLINT
|
||||
@@ -166,10 +163,7 @@ bool Component::cancel_retry(const std::string &name) { // NOLINT
|
||||
}
|
||||
|
||||
bool Component::cancel_retry(const char *name) { // NOLINT
|
||||
#pragma GCC diagnostic push
|
||||
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
|
||||
return App.scheduler.cancel_retry(this, name);
|
||||
#pragma GCC diagnostic pop
|
||||
}
|
||||
|
||||
void Component::set_timeout(const std::string &name, uint32_t timeout, std::function<void()> &&f) { // NOLINT
|
||||
@@ -201,38 +195,18 @@ void Component::set_timeout(uint32_t id, uint32_t timeout, std::function<void()>
|
||||
|
||||
bool Component::cancel_timeout(uint32_t id) { return App.scheduler.cancel_timeout(this, id); }
|
||||
|
||||
void Component::set_timeout(InternalSchedulerID id, uint32_t timeout, std::function<void()> &&f) { // NOLINT
|
||||
App.scheduler.set_timeout(this, id, timeout, std::move(f));
|
||||
}
|
||||
|
||||
bool Component::cancel_timeout(InternalSchedulerID id) { return App.scheduler.cancel_timeout(this, id); }
|
||||
|
||||
void Component::set_interval(uint32_t id, uint32_t interval, std::function<void()> &&f) { // NOLINT
|
||||
App.scheduler.set_interval(this, id, interval, std::move(f));
|
||||
}
|
||||
|
||||
bool Component::cancel_interval(uint32_t id) { return App.scheduler.cancel_interval(this, id); }
|
||||
|
||||
void Component::set_interval(InternalSchedulerID id, uint32_t interval, std::function<void()> &&f) { // NOLINT
|
||||
App.scheduler.set_interval(this, id, interval, std::move(f));
|
||||
}
|
||||
|
||||
bool Component::cancel_interval(InternalSchedulerID id) { return App.scheduler.cancel_interval(this, id); }
|
||||
|
||||
void Component::set_retry(uint32_t id, uint32_t initial_wait_time, uint8_t max_attempts,
|
||||
std::function<RetryResult(uint8_t)> &&f, float backoff_increase_factor) { // NOLINT
|
||||
#pragma GCC diagnostic push
|
||||
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
|
||||
App.scheduler.set_retry(this, id, initial_wait_time, max_attempts, std::move(f), backoff_increase_factor);
|
||||
#pragma GCC diagnostic pop
|
||||
}
|
||||
|
||||
bool Component::cancel_retry(uint32_t id) {
|
||||
#pragma GCC diagnostic push
|
||||
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
|
||||
return App.scheduler.cancel_retry(this, id);
|
||||
#pragma GCC diagnostic pop
|
||||
}
|
||||
bool Component::cancel_retry(uint32_t id) { return App.scheduler.cancel_retry(this, id); }
|
||||
|
||||
void Component::call_loop() { this->loop(); }
|
||||
void Component::call_setup() { this->setup(); }
|
||||
@@ -397,10 +371,7 @@ void Component::set_interval(uint32_t interval, std::function<void()> &&f) { //
|
||||
}
|
||||
void Component::set_retry(uint32_t initial_wait_time, uint8_t max_attempts, std::function<RetryResult(uint8_t)> &&f,
|
||||
float backoff_increase_factor) { // NOLINT
|
||||
#pragma GCC diagnostic push
|
||||
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
|
||||
App.scheduler.set_retry(this, "", initial_wait_time, max_attempts, std::move(f), backoff_increase_factor);
|
||||
#pragma GCC diagnostic pop
|
||||
}
|
||||
bool Component::is_failed() const { return (this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_FAILED; }
|
||||
bool Component::is_ready() const {
|
||||
@@ -545,12 +516,12 @@ void PollingComponent::call_setup() {
|
||||
|
||||
void PollingComponent::start_poller() {
|
||||
// Register interval.
|
||||
this->set_interval(InternalSchedulerID::POLLING_UPDATE, this->get_update_interval(), [this]() { this->update(); });
|
||||
this->set_interval("update", this->get_update_interval(), [this]() { this->update(); });
|
||||
}
|
||||
|
||||
void PollingComponent::stop_poller() {
|
||||
// Clear the interval to suspend component
|
||||
this->cancel_interval(InternalSchedulerID::POLLING_UPDATE);
|
||||
this->cancel_interval("update");
|
||||
}
|
||||
|
||||
uint32_t PollingComponent::get_update_interval() const { return this->update_interval_; }
|
||||
|
||||
@@ -49,14 +49,6 @@ extern const float LATE;
|
||||
|
||||
static const uint32_t SCHEDULER_DONT_RUN = 4294967295UL;
|
||||
|
||||
/// Type-safe scheduler IDs for core base classes.
|
||||
/// Uses a separate NameType (NUMERIC_ID_INTERNAL) so IDs can never collide
|
||||
/// with component-level NUMERIC_ID values, even if the uint32_t values overlap.
|
||||
enum class InternalSchedulerID : uint32_t {
|
||||
POLLING_UPDATE = 0, // PollingComponent interval
|
||||
DELAY_ACTION = 1, // DelayAction timeout
|
||||
};
|
||||
|
||||
// Forward declaration
|
||||
class PollingComponent;
|
||||
|
||||
@@ -76,7 +68,6 @@ extern const uint8_t STATUS_LED_OK;
|
||||
extern const uint8_t STATUS_LED_WARNING;
|
||||
extern const uint8_t STATUS_LED_ERROR;
|
||||
|
||||
// Remove before 2026.8.0
|
||||
enum class RetryResult { DONE, RETRY };
|
||||
|
||||
extern const uint16_t WARN_IF_BLOCKING_OVER_MS;
|
||||
@@ -343,8 +334,6 @@ class Component {
|
||||
*/
|
||||
void set_interval(uint32_t id, uint32_t interval, std::function<void()> &&f); // NOLINT
|
||||
|
||||
void set_interval(InternalSchedulerID id, uint32_t interval, std::function<void()> &&f); // NOLINT
|
||||
|
||||
void set_interval(uint32_t interval, std::function<void()> &&f); // NOLINT
|
||||
|
||||
/** Cancel an interval function.
|
||||
@@ -357,42 +346,69 @@ class Component {
|
||||
bool cancel_interval(const std::string &name); // NOLINT
|
||||
bool cancel_interval(const char *name); // NOLINT
|
||||
bool cancel_interval(uint32_t id); // NOLINT
|
||||
bool cancel_interval(InternalSchedulerID id); // NOLINT
|
||||
|
||||
/// @deprecated set_retry is deprecated. Use set_timeout or set_interval instead. Removed in 2026.8.0.
|
||||
// Remove before 2026.8.0
|
||||
ESPDEPRECATED("set_retry is deprecated and will be removed in 2026.8.0. Use set_timeout or set_interval instead.",
|
||||
"2026.2.0")
|
||||
/** Set an retry function with a unique name. Empty name means no cancelling possible.
|
||||
*
|
||||
* This will call the retry function f on the next scheduler loop. f should return RetryResult::DONE if
|
||||
* it is successful and no repeat is required. Otherwise, returning RetryResult::RETRY will call f
|
||||
* again in the future.
|
||||
*
|
||||
* The first retry of f happens after `initial_wait_time` milliseconds. The delay between retries is
|
||||
* increased by multiplying by `backoff_increase_factor` each time. If no backoff_increase_factor is
|
||||
* supplied (default = 1.0), the wait time will stay constant.
|
||||
*
|
||||
* The retry function f needs to accept a single argument: the number of attempts remaining. On the
|
||||
* final retry of f, this value will be 0.
|
||||
*
|
||||
* This retry function can also be cancelled by name via cancel_retry().
|
||||
*
|
||||
* IMPORTANT: Do not rely on this having correct timing. This is only called from
|
||||
* loop() and therefore can be significantly delayed.
|
||||
*
|
||||
* REMARK: It is an error to supply a negative or zero `backoff_increase_factor`, and 1.0 will be used instead.
|
||||
*
|
||||
* REMARK: The interval between retries is stored into a `uint32_t`, so this doesn't behave correctly
|
||||
* if `initial_wait_time * (backoff_increase_factor ** (max_attempts - 2))` overflows.
|
||||
*
|
||||
* @param name The identifier for this retry function.
|
||||
* @param initial_wait_time The time in ms before f is called again
|
||||
* @param max_attempts The maximum number of executions
|
||||
* @param f The function (or lambda) that should be called
|
||||
* @param backoff_increase_factor time between retries is multiplied by this factor on every retry after the first
|
||||
* @see cancel_retry()
|
||||
*/
|
||||
// Remove before 2026.7.0
|
||||
ESPDEPRECATED("Use const char* or uint32_t overload instead. Removed in 2026.7.0", "2026.1.0")
|
||||
void set_retry(const std::string &name, uint32_t initial_wait_time, uint8_t max_attempts, // NOLINT
|
||||
std::function<RetryResult(uint8_t)> &&f, float backoff_increase_factor = 1.0f); // NOLINT
|
||||
|
||||
// Remove before 2026.8.0
|
||||
ESPDEPRECATED("set_retry is deprecated and will be removed in 2026.8.0. Use set_timeout or set_interval instead.",
|
||||
"2026.2.0")
|
||||
void set_retry(const char *name, uint32_t initial_wait_time, uint8_t max_attempts, // NOLINT
|
||||
std::function<RetryResult(uint8_t)> &&f, float backoff_increase_factor = 1.0f); // NOLINT
|
||||
|
||||
// Remove before 2026.8.0
|
||||
ESPDEPRECATED("set_retry is deprecated and will be removed in 2026.8.0. Use set_timeout or set_interval instead.",
|
||||
"2026.2.0")
|
||||
/** Set a retry function with a numeric ID (zero heap allocation).
|
||||
*
|
||||
* @param id The numeric identifier for this retry function
|
||||
* @param initial_wait_time The wait time after the first execution
|
||||
* @param max_attempts The max number of attempts
|
||||
* @param f The function to call
|
||||
* @param backoff_increase_factor The factor to increase the retry interval by
|
||||
*/
|
||||
void set_retry(uint32_t id, uint32_t initial_wait_time, uint8_t max_attempts, // NOLINT
|
||||
std::function<RetryResult(uint8_t)> &&f, float backoff_increase_factor = 1.0f); // NOLINT
|
||||
|
||||
// Remove before 2026.8.0
|
||||
ESPDEPRECATED("set_retry is deprecated and will be removed in 2026.8.0. Use set_timeout or set_interval instead.",
|
||||
"2026.2.0")
|
||||
void set_retry(uint32_t initial_wait_time, uint8_t max_attempts, std::function<RetryResult(uint8_t)> &&f, // NOLINT
|
||||
float backoff_increase_factor = 1.0f); // NOLINT
|
||||
|
||||
// Remove before 2026.8.0
|
||||
ESPDEPRECATED("cancel_retry is deprecated and will be removed in 2026.8.0.", "2026.2.0")
|
||||
/** Cancel a retry function.
|
||||
*
|
||||
* @param name The identifier for this retry function.
|
||||
* @return Whether a retry function was deleted.
|
||||
*/
|
||||
// Remove before 2026.7.0
|
||||
ESPDEPRECATED("Use const char* or uint32_t overload instead. Removed in 2026.7.0", "2026.1.0")
|
||||
bool cancel_retry(const std::string &name); // NOLINT
|
||||
// Remove before 2026.8.0
|
||||
ESPDEPRECATED("cancel_retry is deprecated and will be removed in 2026.8.0.", "2026.2.0")
|
||||
bool cancel_retry(const char *name); // NOLINT
|
||||
// Remove before 2026.8.0
|
||||
ESPDEPRECATED("cancel_retry is deprecated and will be removed in 2026.8.0.", "2026.2.0")
|
||||
bool cancel_retry(uint32_t id); // NOLINT
|
||||
bool cancel_retry(const char *name); // NOLINT
|
||||
bool cancel_retry(uint32_t id); // NOLINT
|
||||
|
||||
/** Set a timeout function with a unique name.
|
||||
*
|
||||
@@ -436,8 +452,6 @@ class Component {
|
||||
*/
|
||||
void set_timeout(uint32_t id, uint32_t timeout, std::function<void()> &&f); // NOLINT
|
||||
|
||||
void set_timeout(InternalSchedulerID id, uint32_t timeout, std::function<void()> &&f); // NOLINT
|
||||
|
||||
void set_timeout(uint32_t timeout, std::function<void()> &&f); // NOLINT
|
||||
|
||||
/** Cancel a timeout function.
|
||||
@@ -450,7 +464,6 @@ class Component {
|
||||
bool cancel_timeout(const std::string &name); // NOLINT
|
||||
bool cancel_timeout(const char *name); // NOLINT
|
||||
bool cancel_timeout(uint32_t id); // NOLINT
|
||||
bool cancel_timeout(InternalSchedulerID id); // NOLINT
|
||||
|
||||
/** Defer a callback to the next loop() call.
|
||||
*
|
||||
|
||||
@@ -53,12 +53,9 @@ struct SchedulerNameLog {
|
||||
} else if (name_type == NameType::HASHED_STRING) {
|
||||
ESPHOME_snprintf_P(buffer, sizeof(buffer), ESPHOME_PSTR("hash:0x%08" PRIX32), hash_or_id);
|
||||
return buffer;
|
||||
} else if (name_type == NameType::NUMERIC_ID) {
|
||||
} else { // NUMERIC_ID
|
||||
ESPHOME_snprintf_P(buffer, sizeof(buffer), ESPHOME_PSTR("id:%" PRIu32), hash_or_id);
|
||||
return buffer;
|
||||
} else { // NUMERIC_ID_INTERNAL
|
||||
ESPHOME_snprintf_P(buffer, sizeof(buffer), ESPHOME_PSTR("iid:%" PRIu32), hash_or_id);
|
||||
return buffer;
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -140,9 +137,6 @@ void HOT Scheduler::set_timer_common_(Component *component, SchedulerItem::Type
|
||||
case NameType::NUMERIC_ID:
|
||||
item->set_numeric_id(hash_or_id);
|
||||
break;
|
||||
case NameType::NUMERIC_ID_INTERNAL:
|
||||
item->set_internal_id(hash_or_id);
|
||||
break;
|
||||
}
|
||||
item->type = type;
|
||||
item->callback = std::move(func);
|
||||
@@ -258,11 +252,6 @@ bool HOT Scheduler::cancel_interval(Component *component, uint32_t id) {
|
||||
return this->cancel_item_(component, NameType::NUMERIC_ID, nullptr, id, SchedulerItem::INTERVAL);
|
||||
}
|
||||
|
||||
// Suppress deprecation warnings for RetryResult usage in the still-present (but deprecated) retry implementation.
|
||||
// Remove before 2026.8.0 along with all retry code.
|
||||
#pragma GCC diagnostic push
|
||||
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
|
||||
|
||||
struct RetryArgs {
|
||||
// Ordered to minimize padding on 32-bit systems
|
||||
std::function<RetryResult(uint8_t)> func;
|
||||
@@ -375,8 +364,6 @@ bool HOT Scheduler::cancel_retry(Component *component, uint32_t id) {
|
||||
return this->cancel_retry_(component, NameType::NUMERIC_ID, nullptr, id);
|
||||
}
|
||||
|
||||
#pragma GCC diagnostic pop // End suppression of deprecated RetryResult warnings
|
||||
|
||||
optional<uint32_t> HOT Scheduler::next_schedule_in(uint32_t now) {
|
||||
// IMPORTANT: This method should only be called from the main thread (loop task).
|
||||
// It performs cleanup and accesses items_[0] without holding a lock, which is only
|
||||
@@ -403,19 +390,20 @@ void Scheduler::full_cleanup_removed_items_() {
|
||||
// 4. No operations inside can block or take other locks, so no deadlock risk
|
||||
LockGuard guard{this->lock_};
|
||||
|
||||
// Compact in-place: move valid items forward, recycle removed ones
|
||||
size_t write = 0;
|
||||
for (size_t read = 0; read < this->items_.size(); ++read) {
|
||||
if (!is_item_removed_(this->items_[read].get())) {
|
||||
if (write != read) {
|
||||
this->items_[write] = std::move(this->items_[read]);
|
||||
}
|
||||
++write;
|
||||
std::vector<std::unique_ptr<SchedulerItem>> valid_items;
|
||||
|
||||
// Move all non-removed items to valid_items, recycle removed ones
|
||||
for (auto &item : this->items_) {
|
||||
if (!is_item_removed_(item.get())) {
|
||||
valid_items.push_back(std::move(item));
|
||||
} else {
|
||||
this->recycle_item_main_loop_(std::move(this->items_[read]));
|
||||
// Recycle removed items
|
||||
this->recycle_item_main_loop_(std::move(item));
|
||||
}
|
||||
}
|
||||
this->items_.erase(this->items_.begin() + write, this->items_.end());
|
||||
|
||||
// Replace items_ with the filtered list
|
||||
this->items_ = std::move(valid_items);
|
||||
// Rebuild the heap structure since items are no longer in heap order
|
||||
std::make_heap(this->items_.begin(), this->items_.end(), SchedulerItem::cmp);
|
||||
this->to_remove_ = 0;
|
||||
|
||||
@@ -46,20 +46,11 @@ class Scheduler {
|
||||
void set_timeout(Component *component, const char *name, uint32_t timeout, std::function<void()> func);
|
||||
/// Set a timeout with a numeric ID (zero heap allocation)
|
||||
void set_timeout(Component *component, uint32_t id, uint32_t timeout, std::function<void()> func);
|
||||
/// Set a timeout with an internal scheduler ID (separate namespace from component NUMERIC_ID)
|
||||
void set_timeout(Component *component, InternalSchedulerID id, uint32_t timeout, std::function<void()> func) {
|
||||
this->set_timer_common_(component, SchedulerItem::TIMEOUT, NameType::NUMERIC_ID_INTERNAL, nullptr,
|
||||
static_cast<uint32_t>(id), timeout, std::move(func));
|
||||
}
|
||||
|
||||
ESPDEPRECATED("Use const char* or uint32_t overload instead. Removed in 2026.7.0", "2026.1.0")
|
||||
bool cancel_timeout(Component *component, const std::string &name);
|
||||
bool cancel_timeout(Component *component, const char *name);
|
||||
bool cancel_timeout(Component *component, uint32_t id);
|
||||
bool cancel_timeout(Component *component, InternalSchedulerID id) {
|
||||
return this->cancel_item_(component, NameType::NUMERIC_ID_INTERNAL, nullptr, static_cast<uint32_t>(id),
|
||||
SchedulerItem::TIMEOUT);
|
||||
}
|
||||
|
||||
ESPDEPRECATED("Use const char* or uint32_t overload instead. Removed in 2026.7.0", "2026.1.0")
|
||||
void set_interval(Component *component, const std::string &name, uint32_t interval, std::function<void()> func);
|
||||
@@ -75,45 +66,24 @@ class Scheduler {
|
||||
void set_interval(Component *component, const char *name, uint32_t interval, std::function<void()> func);
|
||||
/// Set an interval with a numeric ID (zero heap allocation)
|
||||
void set_interval(Component *component, uint32_t id, uint32_t interval, std::function<void()> func);
|
||||
/// Set an interval with an internal scheduler ID (separate namespace from component NUMERIC_ID)
|
||||
void set_interval(Component *component, InternalSchedulerID id, uint32_t interval, std::function<void()> func) {
|
||||
this->set_timer_common_(component, SchedulerItem::INTERVAL, NameType::NUMERIC_ID_INTERNAL, nullptr,
|
||||
static_cast<uint32_t>(id), interval, std::move(func));
|
||||
}
|
||||
|
||||
ESPDEPRECATED("Use const char* or uint32_t overload instead. Removed in 2026.7.0", "2026.1.0")
|
||||
bool cancel_interval(Component *component, const std::string &name);
|
||||
bool cancel_interval(Component *component, const char *name);
|
||||
bool cancel_interval(Component *component, uint32_t id);
|
||||
bool cancel_interval(Component *component, InternalSchedulerID id) {
|
||||
return this->cancel_item_(component, NameType::NUMERIC_ID_INTERNAL, nullptr, static_cast<uint32_t>(id),
|
||||
SchedulerItem::INTERVAL);
|
||||
}
|
||||
|
||||
// Remove before 2026.8.0
|
||||
ESPDEPRECATED("set_retry is deprecated and will be removed in 2026.8.0. Use set_timeout or set_interval instead.",
|
||||
"2026.2.0")
|
||||
ESPDEPRECATED("Use const char* or uint32_t overload instead. Removed in 2026.7.0", "2026.1.0")
|
||||
void set_retry(Component *component, const std::string &name, uint32_t initial_wait_time, uint8_t max_attempts,
|
||||
std::function<RetryResult(uint8_t)> func, float backoff_increase_factor = 1.0f);
|
||||
// Remove before 2026.8.0
|
||||
ESPDEPRECATED("set_retry is deprecated and will be removed in 2026.8.0. Use set_timeout or set_interval instead.",
|
||||
"2026.2.0")
|
||||
void set_retry(Component *component, const char *name, uint32_t initial_wait_time, uint8_t max_attempts,
|
||||
std::function<RetryResult(uint8_t)> func, float backoff_increase_factor = 1.0f);
|
||||
// Remove before 2026.8.0
|
||||
ESPDEPRECATED("set_retry is deprecated and will be removed in 2026.8.0. Use set_timeout or set_interval instead.",
|
||||
"2026.2.0")
|
||||
/// Set a retry with a numeric ID (zero heap allocation)
|
||||
void set_retry(Component *component, uint32_t id, uint32_t initial_wait_time, uint8_t max_attempts,
|
||||
std::function<RetryResult(uint8_t)> func, float backoff_increase_factor = 1.0f);
|
||||
|
||||
// Remove before 2026.8.0
|
||||
ESPDEPRECATED("cancel_retry is deprecated and will be removed in 2026.8.0.", "2026.2.0")
|
||||
ESPDEPRECATED("Use const char* or uint32_t overload instead. Removed in 2026.7.0", "2026.1.0")
|
||||
bool cancel_retry(Component *component, const std::string &name);
|
||||
// Remove before 2026.8.0
|
||||
ESPDEPRECATED("cancel_retry is deprecated and will be removed in 2026.8.0.", "2026.2.0")
|
||||
bool cancel_retry(Component *component, const char *name);
|
||||
// Remove before 2026.8.0
|
||||
ESPDEPRECATED("cancel_retry is deprecated and will be removed in 2026.8.0.", "2026.2.0")
|
||||
bool cancel_retry(Component *component, uint32_t id);
|
||||
|
||||
// Calculate when the next scheduled item should run
|
||||
@@ -130,12 +100,11 @@ class Scheduler {
|
||||
void process_to_add();
|
||||
|
||||
// Name storage type discriminator for SchedulerItem
|
||||
// Used to distinguish between static strings, hashed strings, numeric IDs, and internal numeric IDs
|
||||
// Used to distinguish between static strings, hashed strings, and numeric IDs
|
||||
enum class NameType : uint8_t {
|
||||
STATIC_STRING = 0, // const char* pointer to static/flash storage
|
||||
HASHED_STRING = 1, // uint32_t FNV-1a hash of a runtime string
|
||||
NUMERIC_ID = 2, // uint32_t numeric identifier (component-level)
|
||||
NUMERIC_ID_INTERNAL = 3 // uint32_t numeric identifier (core/internal, separate namespace)
|
||||
STATIC_STRING = 0, // const char* pointer to static/flash storage
|
||||
HASHED_STRING = 1, // uint32_t FNV-1a hash of a runtime string
|
||||
NUMERIC_ID = 2 // uint32_t numeric identifier
|
||||
};
|
||||
|
||||
protected:
|
||||
@@ -166,7 +135,7 @@ class Scheduler {
|
||||
|
||||
// Bit-packed fields (4 bits used, 4 bits padding in 1 byte)
|
||||
enum Type : uint8_t { TIMEOUT, INTERVAL } type : 1;
|
||||
NameType name_type_ : 2; // Discriminator for name_ union (0–3, see NameType enum)
|
||||
NameType name_type_ : 2; // Discriminator for name_ union (STATIC_STRING, HASHED_STRING, NUMERIC_ID)
|
||||
bool is_retry : 1; // True if this is a retry timeout
|
||||
// 4 bits padding
|
||||
#else
|
||||
@@ -174,7 +143,7 @@ class Scheduler {
|
||||
// Bit-packed fields (5 bits used, 3 bits padding in 1 byte)
|
||||
enum Type : uint8_t { TIMEOUT, INTERVAL } type : 1;
|
||||
bool remove : 1;
|
||||
NameType name_type_ : 2; // Discriminator for name_ union (0–3, see NameType enum)
|
||||
NameType name_type_ : 2; // Discriminator for name_ union (STATIC_STRING, HASHED_STRING, NUMERIC_ID)
|
||||
bool is_retry : 1; // True if this is a retry timeout
|
||||
// 3 bits padding
|
||||
#endif
|
||||
@@ -237,12 +206,6 @@ class Scheduler {
|
||||
name_type_ = NameType::NUMERIC_ID;
|
||||
}
|
||||
|
||||
// Helper to set an internal numeric ID (separate namespace from NUMERIC_ID)
|
||||
void set_internal_id(uint32_t id) {
|
||||
name_.hash_or_id = id;
|
||||
name_type_ = NameType::NUMERIC_ID_INTERNAL;
|
||||
}
|
||||
|
||||
static bool cmp(const std::unique_ptr<SchedulerItem> &a, const std::unique_ptr<SchedulerItem> &b);
|
||||
|
||||
// Note: We use 48 bits total (32 + 16), stored in a 64-bit value for API compatibility.
|
||||
@@ -268,14 +231,11 @@ class Scheduler {
|
||||
uint32_t hash_or_id, uint32_t delay, std::function<void()> func, bool is_retry = false,
|
||||
bool skip_cancel = false);
|
||||
|
||||
// Common implementation for retry - Remove before 2026.8.0
|
||||
// Common implementation for retry
|
||||
// name_type determines storage type: STATIC_STRING uses static_name, others use hash_or_id
|
||||
#pragma GCC diagnostic push
|
||||
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
|
||||
void set_retry_common_(Component *component, NameType name_type, const char *static_name, uint32_t hash_or_id,
|
||||
uint32_t initial_wait_time, uint8_t max_attempts, std::function<RetryResult(uint8_t)> func,
|
||||
float backoff_increase_factor);
|
||||
#pragma GCC diagnostic pop
|
||||
// Common implementation for cancel_retry
|
||||
bool cancel_retry_(Component *component, NameType name_type, const char *static_name, uint32_t hash_or_id);
|
||||
|
||||
|
||||
@@ -12,7 +12,6 @@ std_shared_ptr = std_ns.class_("shared_ptr")
|
||||
std_string = std_ns.class_("string")
|
||||
std_string_ref = std_ns.namespace("string &")
|
||||
std_vector = std_ns.class_("vector")
|
||||
std_span = std_ns.class_("span")
|
||||
uint8 = global_ns.namespace("uint8_t")
|
||||
uint16 = global_ns.namespace("uint16_t")
|
||||
uint32 = global_ns.namespace("uint32_t")
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user