mirror of
https://github.com/esphome/esphome.git
synced 2026-02-09 17:21:57 +00:00
Compare commits
7 Commits
integratio
...
no_new_to_
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
13c84bf36a | ||
|
|
6ca1b90752 | ||
|
|
fe1aa7e9ba | ||
|
|
8d51e2f580 | ||
|
|
11fb46ad11 | ||
|
|
9245c691d0 | ||
|
|
971a1a3e00 |
@@ -1 +1 @@
|
||||
37ec8d5a343c8d0a485fd2118cbdabcbccd7b9bca197e4a392be75087974dced
|
||||
069fa9526c52f7c580a9ec17c7678d12f142221387e9b561c18f95394d4629a3
|
||||
|
||||
@@ -12,6 +12,7 @@ from .const import (
|
||||
CORE_SUBCATEGORY_PATTERNS,
|
||||
DEMANGLED_PATTERNS,
|
||||
ESPHOME_COMPONENT_PATTERN,
|
||||
SECTION_TO_ATTR,
|
||||
SYMBOL_PATTERNS,
|
||||
)
|
||||
from .demangle import batch_demangle
|
||||
@@ -43,7 +44,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 +57,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:
|
||||
@@ -101,17 +91,6 @@ class ComponentMemory:
|
||||
bss_size: int = 0 # Uninitialized data (ram only)
|
||||
symbol_count: int = 0
|
||||
|
||||
def add_section_size(self, section_name: str, size: int) -> None:
|
||||
"""Add size to the appropriate attribute for a section."""
|
||||
if section_name == ".text":
|
||||
self.text_size += size
|
||||
elif section_name == ".rodata":
|
||||
self.rodata_size += size
|
||||
elif section_name == ".data":
|
||||
self.data_size += size
|
||||
elif section_name == ".bss":
|
||||
self.bss_size += size
|
||||
|
||||
@property
|
||||
def flash_total(self) -> int:
|
||||
"""Total flash usage (text + rodata + data)."""
|
||||
@@ -188,23 +167,12 @@ class MemoryAnalyzer:
|
||||
self._elf_symbol_names: set[str] = set()
|
||||
# SDK symbols not in ELF (static/local symbols from closed-source libs)
|
||||
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()
|
||||
return dict(self.components)
|
||||
|
||||
@@ -287,7 +255,8 @@ class MemoryAnalyzer:
|
||||
comp_mem.symbol_count += 1
|
||||
|
||||
# Update the appropriate size attribute based on section
|
||||
comp_mem.add_section_size(section_name, size)
|
||||
if attr_name := SECTION_TO_ATTR.get(section_name):
|
||||
setattr(comp_mem, attr_name, getattr(comp_mem, attr_name) + size)
|
||||
|
||||
# Track uncategorized symbols
|
||||
if component == "other" and size > 0:
|
||||
@@ -347,19 +316,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,610 +372,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.
|
||||
|
||||
Returns:
|
||||
Path to the directory containing .o files, or None if not found.
|
||||
"""
|
||||
# The ELF is typically at .pioenvs/<env>/firmware.elf
|
||||
# Object files are in .pioenvs/<env>/src/ and .pioenvs/<env>/lib*/
|
||||
pioenvs_dir = self.elf_path.parent
|
||||
if pioenvs_dir.exists() and any(pioenvs_dir.glob("src/*.o")):
|
||||
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.
|
||||
|
||||
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``).
|
||||
|
||||
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.
|
||||
"""
|
||||
for line in output.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"
|
||||
parts_after_colon = line.rsplit(":", 1)
|
||||
if len(parts_after_colon) != 2:
|
||||
continue
|
||||
|
||||
file_path = parts_after_colon[0]
|
||||
fields = parts_after_colon[1].split()
|
||||
# fields: [address, size, type, name]
|
||||
if len(fields) < 4:
|
||||
continue
|
||||
|
||||
sym_name = fields[3]
|
||||
if not sym_name.startswith("CSWTCH$"):
|
||||
continue
|
||||
|
||||
try:
|
||||
size = int(fields[1], 16)
|
||||
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:
|
||||
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())
|
||||
|
||||
def _source_file_to_component(self, source_file: str) -> str:
|
||||
"""Map a source object file path to its component name.
|
||||
|
||||
Args:
|
||||
source_file: Relative path like 'src/esphome/components/wifi/wifi_component.cpp.o'
|
||||
|
||||
Returns:
|
||||
Component name like '[esphome]wifi' or the source file if unknown.
|
||||
"""
|
||||
parts = Path(source_file).parts
|
||||
|
||||
# ESPHome component: src/esphome/components/<name>/...
|
||||
if "components" in parts:
|
||||
idx = parts.index("components")
|
||||
if idx + 1 < len(parts):
|
||||
component_name = parts[idx + 1]
|
||||
if component_name in get_esphome_components():
|
||||
return f"{_COMPONENT_PREFIX_ESPHOME}{component_name}"
|
||||
if component_name in self.external_components:
|
||||
return f"{_COMPONENT_PREFIX_EXTERNAL}{component_name}"
|
||||
|
||||
# ESPHome core: src/esphome/core/... or src/esphome/...
|
||||
if "core" in parts and "esphome" in parts:
|
||||
return _COMPONENT_CORE
|
||||
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
|
||||
return parts[0] if parts else source_file
|
||||
|
||||
def _analyze_cswtch_symbols(self) -> None:
|
||||
"""Analyze CSWTCH (GCC switch table) symbols by tracing to source objects.
|
||||
|
||||
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.
|
||||
"""
|
||||
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)
|
||||
|
||||
if not cswtch_map:
|
||||
_LOGGER.debug("No CSWTCH symbols found in object files or SDK archives")
|
||||
return
|
||||
|
||||
# Collect CSWTCH symbols from the ELF (already parsed in sections)
|
||||
# Include section_name for re-attribution of component totals
|
||||
elf_cswtch = [
|
||||
(symbol_name, size, section_name)
|
||||
for section_name, section in self.sections.items()
|
||||
for symbol_name, size, _ in section.symbols
|
||||
if symbol_name.startswith("CSWTCH$")
|
||||
]
|
||||
|
||||
_LOGGER.debug(
|
||||
"Found %d CSWTCH symbols in ELF, %d unique in object files",
|
||||
len(elf_cswtch),
|
||||
len(cswtch_map),
|
||||
)
|
||||
|
||||
# Match ELF CSWTCH symbols to source files and re-attribute component totals.
|
||||
# _categorize_symbols() already ran and put these into "other" since CSWTCH$
|
||||
# names don't match any component pattern. We move the bytes to the correct
|
||||
# component based on the object file mapping.
|
||||
other_mem = self.components.get("other")
|
||||
|
||||
for sym_name, size, section_name in elf_cswtch:
|
||||
key = f"{sym_name}:{size}"
|
||||
sources = cswtch_map.get(key, [])
|
||||
|
||||
if len(sources) == 1:
|
||||
source_file = sources[0][0]
|
||||
component = self._source_file_to_component(source_file)
|
||||
elif len(sources) > 1:
|
||||
# Ambiguous - multiple object files have same CSWTCH name+size
|
||||
source_file = "ambiguous"
|
||||
component = "ambiguous"
|
||||
_LOGGER.debug(
|
||||
"Ambiguous CSWTCH %s (%d B) found in %d files: %s",
|
||||
sym_name,
|
||||
size,
|
||||
len(sources),
|
||||
", ".join(src for src, _ in sources),
|
||||
)
|
||||
else:
|
||||
source_file = "unknown"
|
||||
component = "unknown"
|
||||
|
||||
self._cswtch_symbols.append((sym_name, size, source_file, component))
|
||||
|
||||
# Re-attribute from "other" to the correct component
|
||||
if (
|
||||
component not in ("other", "unknown", "ambiguous")
|
||||
and other_mem is not None
|
||||
):
|
||||
other_mem.add_section_size(section_name, -size)
|
||||
if component not in self.components:
|
||||
self.components[component] = ComponentMemory(component)
|
||||
self.components[component].add_section_size(section_name, size)
|
||||
|
||||
# Sort by size descending
|
||||
self._cswtch_symbols.sort(key=lambda x: x[1], reverse=True)
|
||||
|
||||
total_size = sum(size for _, size, _, _ in self._cswtch_symbols)
|
||||
_LOGGER.debug(
|
||||
"CSWTCH analysis: %d symbols, %d bytes total",
|
||||
len(self._cswtch_symbols),
|
||||
total_size,
|
||||
)
|
||||
|
||||
def get_unattributed_ram(self) -> tuple[int, int, int]:
|
||||
"""Get unattributed RAM sizes (SDK/framework overhead).
|
||||
|
||||
|
||||
@@ -14,7 +14,6 @@ from . import (
|
||||
_COMPONENT_CORE,
|
||||
_COMPONENT_PREFIX_ESPHOME,
|
||||
_COMPONENT_PREFIX_EXTERNAL,
|
||||
_COMPONENT_PREFIX_LIB,
|
||||
RAM_SECTIONS,
|
||||
MemoryAnalyzer,
|
||||
)
|
||||
@@ -185,52 +184,6 @@ class MemoryAnalyzerCLI(MemoryAnalyzer):
|
||||
f"{i + 1:>2}. {size:>7,} B {section_label:<8} {demangled_display:<{self.COL_TOP_SYMBOL_NAME}} {component}"
|
||||
)
|
||||
|
||||
def _add_cswtch_analysis(self, lines: list[str]) -> None:
|
||||
"""Add CSWTCH (GCC switch table lookup) analysis section."""
|
||||
self._add_section_header(lines, "CSWTCH Analysis (GCC Switch Table Lookups)")
|
||||
|
||||
total_size = sum(size for _, size, _, _ in self._cswtch_symbols)
|
||||
lines.append(
|
||||
f"Total: {len(self._cswtch_symbols)} switch table(s), {total_size:,} B"
|
||||
)
|
||||
lines.append("")
|
||||
|
||||
# Group by component
|
||||
by_component: dict[str, list[tuple[str, int, str]]] = defaultdict(list)
|
||||
for sym_name, size, source_file, component in self._cswtch_symbols:
|
||||
by_component[component].append((sym_name, size, source_file))
|
||||
|
||||
# Sort components by total size descending
|
||||
sorted_components = sorted(
|
||||
by_component.items(),
|
||||
key=lambda x: sum(s[1] for s in x[1]),
|
||||
reverse=True,
|
||||
)
|
||||
|
||||
for component, symbols in sorted_components:
|
||||
comp_total = sum(s[1] for s in symbols)
|
||||
lines.append(f"{component} ({comp_total:,} B, {len(symbols)} tables):")
|
||||
|
||||
# Group by source file within component
|
||||
by_file: dict[str, list[tuple[str, int]]] = defaultdict(list)
|
||||
for sym_name, size, source_file in symbols:
|
||||
by_file[source_file].append((sym_name, size))
|
||||
|
||||
for source_file, file_symbols in sorted(
|
||||
by_file.items(),
|
||||
key=lambda x: sum(s[1] for s in x[1]),
|
||||
reverse=True,
|
||||
):
|
||||
file_total = sum(s[1] for s in file_symbols)
|
||||
lines.append(
|
||||
f" {source_file} ({file_total:,} B, {len(file_symbols)} tables)"
|
||||
)
|
||||
for sym_name, size in sorted(
|
||||
file_symbols, key=lambda x: x[1], reverse=True
|
||||
):
|
||||
lines.append(f" {size:>6,} B {sym_name}")
|
||||
lines.append("")
|
||||
|
||||
def generate_report(self, detailed: bool = False) -> str:
|
||||
"""Generate a formatted memory report."""
|
||||
components = sorted(
|
||||
@@ -408,11 +361,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 +371,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 +389,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:
|
||||
@@ -529,10 +471,6 @@ class MemoryAnalyzerCLI(MemoryAnalyzer):
|
||||
lines.append(f" ... and {len(large_ram_syms) - 10} more")
|
||||
lines.append("")
|
||||
|
||||
# CSWTCH (GCC switch table) analysis
|
||||
if self._cswtch_symbols:
|
||||
self._add_cswtch_analysis(lines)
|
||||
|
||||
lines.append(
|
||||
"Note: This analysis covers symbols in the ELF file. Some runtime allocations may not be included."
|
||||
)
|
||||
|
||||
@@ -66,6 +66,15 @@ SECTION_MAPPING = {
|
||||
),
|
||||
}
|
||||
|
||||
# Section to ComponentMemory attribute mapping
|
||||
# Maps section names to the attribute name in ComponentMemory dataclass
|
||||
SECTION_TO_ATTR = {
|
||||
".text": "text_size",
|
||||
".rodata": "rodata_size",
|
||||
".data": "data_size",
|
||||
".bss": "bss_size",
|
||||
}
|
||||
|
||||
# Component identification rules
|
||||
# Symbol patterns: patterns found in raw symbol names
|
||||
SYMBOL_PATTERNS = {
|
||||
@@ -504,9 +513,7 @@ SYMBOL_PATTERNS = {
|
||||
"__FUNCTION__$",
|
||||
"DAYS_IN_MONTH",
|
||||
"_DAYS_BEFORE_MONTH",
|
||||
# Note: CSWTCH$ symbols are GCC switch table lookup tables.
|
||||
# They are attributed to their source object files via _analyze_cswtch_symbols()
|
||||
# rather than being lumped into libc.
|
||||
"CSWTCH$",
|
||||
"dst$",
|
||||
"sulp",
|
||||
"_strtol_l", # String to long with locale
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -1,15 +1,32 @@
|
||||
#include "alarm_control_panel_state.h"
|
||||
#include "esphome/core/progmem.h"
|
||||
|
||||
namespace esphome::alarm_control_panel {
|
||||
|
||||
// Alarm control panel state strings indexed by AlarmControlPanelState enum (0-9)
|
||||
PROGMEM_STRING_TABLE(AlarmControlPanelStateStrings, "DISARMED", "ARMED_HOME", "ARMED_AWAY", "ARMED_NIGHT",
|
||||
"ARMED_VACATION", "ARMED_CUSTOM_BYPASS", "PENDING", "ARMING", "DISARMING", "TRIGGERED", "UNKNOWN");
|
||||
|
||||
const LogString *alarm_control_panel_state_to_string(AlarmControlPanelState state) {
|
||||
return AlarmControlPanelStateStrings::get_log_str(static_cast<uint8_t>(state),
|
||||
AlarmControlPanelStateStrings::LAST_INDEX);
|
||||
switch (state) {
|
||||
case ACP_STATE_DISARMED:
|
||||
return LOG_STR("DISARMED");
|
||||
case ACP_STATE_ARMED_HOME:
|
||||
return LOG_STR("ARMED_HOME");
|
||||
case ACP_STATE_ARMED_AWAY:
|
||||
return LOG_STR("ARMED_AWAY");
|
||||
case ACP_STATE_ARMED_NIGHT:
|
||||
return LOG_STR("ARMED_NIGHT");
|
||||
case ACP_STATE_ARMED_VACATION:
|
||||
return LOG_STR("ARMED_VACATION");
|
||||
case ACP_STATE_ARMED_CUSTOM_BYPASS:
|
||||
return LOG_STR("ARMED_CUSTOM_BYPASS");
|
||||
case ACP_STATE_PENDING:
|
||||
return LOG_STR("PENDING");
|
||||
case ACP_STATE_ARMING:
|
||||
return LOG_STR("ARMING");
|
||||
case ACP_STATE_DISARMING:
|
||||
return LOG_STR("DISARMING");
|
||||
case ACP_STATE_TRIGGERED:
|
||||
return LOG_STR("TRIGGERED");
|
||||
default:
|
||||
return LOG_STR("UNKNOWN");
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace esphome::alarm_control_panel
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -47,72 +47,72 @@ class APIConnection final : public APIServerConnection {
|
||||
#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 APIServerConnection {
|
||||
#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 APIServerConnection {
|
||||
#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 APIServerConnection {
|
||||
|
||||
#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 APIServerConnection {
|
||||
#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 APIServerConnection {
|
||||
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 APIServerConnection {
|
||||
#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 APIServerConnection {
|
||||
// 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
|
||||
|
||||
@@ -15,9 +15,6 @@ 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) {
|
||||
@@ -32,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 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 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 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: {
|
||||
@@ -135,10 +146,12 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
|
||||
#endif
|
||||
#ifdef USE_API_HOMEASSISTANT_SERVICES
|
||||
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
|
||||
@@ -153,10 +166,12 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
|
||||
}
|
||||
#ifdef USE_API_HOMEASSISTANT_STATES
|
||||
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
|
||||
@@ -360,19 +375,23 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
|
||||
#endif
|
||||
#ifdef USE_BLUETOOTH_PROXY
|
||||
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 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
|
||||
@@ -623,6 +642,209 @@ 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) {
|
||||
|
||||
@@ -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
|
||||
@@ -229,7 +229,270 @@ class APIServerConnectionBase : public ProtoService {
|
||||
};
|
||||
|
||||
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;
|
||||
};
|
||||
|
||||
|
||||
@@ -25,7 +25,9 @@ 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)); }
|
||||
template<typename T> static std::string value_to_string(T &&val) {
|
||||
return to_string(std::forward<T>(val)); // NOLINT
|
||||
}
|
||||
|
||||
// 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;
|
||||
|
||||
@@ -1,44 +1,109 @@
|
||||
#include "climate_mode.h"
|
||||
#include "esphome/core/progmem.h"
|
||||
|
||||
namespace esphome::climate {
|
||||
|
||||
// Climate mode strings indexed by ClimateMode enum (0-6): OFF, HEAT_COOL, COOL, HEAT, FAN_ONLY, DRY, AUTO
|
||||
PROGMEM_STRING_TABLE(ClimateModeStrings, "OFF", "HEAT_COOL", "COOL", "HEAT", "FAN_ONLY", "DRY", "AUTO", "UNKNOWN");
|
||||
|
||||
const LogString *climate_mode_to_string(ClimateMode mode) {
|
||||
return ClimateModeStrings::get_log_str(static_cast<uint8_t>(mode), ClimateModeStrings::LAST_INDEX);
|
||||
switch (mode) {
|
||||
case CLIMATE_MODE_OFF:
|
||||
return LOG_STR("OFF");
|
||||
case CLIMATE_MODE_HEAT_COOL:
|
||||
return LOG_STR("HEAT_COOL");
|
||||
case CLIMATE_MODE_AUTO:
|
||||
return LOG_STR("AUTO");
|
||||
case CLIMATE_MODE_COOL:
|
||||
return LOG_STR("COOL");
|
||||
case CLIMATE_MODE_HEAT:
|
||||
return LOG_STR("HEAT");
|
||||
case CLIMATE_MODE_FAN_ONLY:
|
||||
return LOG_STR("FAN_ONLY");
|
||||
case CLIMATE_MODE_DRY:
|
||||
return LOG_STR("DRY");
|
||||
default:
|
||||
return LOG_STR("UNKNOWN");
|
||||
}
|
||||
}
|
||||
|
||||
// Climate action strings indexed by ClimateAction enum (0,2-6): OFF, (gap), COOLING, HEATING, IDLE, DRYING, FAN
|
||||
PROGMEM_STRING_TABLE(ClimateActionStrings, "OFF", "UNKNOWN", "COOLING", "HEATING", "IDLE", "DRYING", "FAN", "UNKNOWN");
|
||||
|
||||
const LogString *climate_action_to_string(ClimateAction action) {
|
||||
return ClimateActionStrings::get_log_str(static_cast<uint8_t>(action), ClimateActionStrings::LAST_INDEX);
|
||||
switch (action) {
|
||||
case CLIMATE_ACTION_OFF:
|
||||
return LOG_STR("OFF");
|
||||
case CLIMATE_ACTION_COOLING:
|
||||
return LOG_STR("COOLING");
|
||||
case CLIMATE_ACTION_HEATING:
|
||||
return LOG_STR("HEATING");
|
||||
case CLIMATE_ACTION_IDLE:
|
||||
return LOG_STR("IDLE");
|
||||
case CLIMATE_ACTION_DRYING:
|
||||
return LOG_STR("DRYING");
|
||||
case CLIMATE_ACTION_FAN:
|
||||
return LOG_STR("FAN");
|
||||
default:
|
||||
return LOG_STR("UNKNOWN");
|
||||
}
|
||||
}
|
||||
|
||||
// Climate fan mode strings indexed by ClimateFanMode enum (0-9): ON, OFF, AUTO, LOW, MEDIUM, HIGH, MIDDLE, FOCUS,
|
||||
// DIFFUSE, QUIET
|
||||
PROGMEM_STRING_TABLE(ClimateFanModeStrings, "ON", "OFF", "AUTO", "LOW", "MEDIUM", "HIGH", "MIDDLE", "FOCUS", "DIFFUSE",
|
||||
"QUIET", "UNKNOWN");
|
||||
|
||||
const LogString *climate_fan_mode_to_string(ClimateFanMode fan_mode) {
|
||||
return ClimateFanModeStrings::get_log_str(static_cast<uint8_t>(fan_mode), ClimateFanModeStrings::LAST_INDEX);
|
||||
switch (fan_mode) {
|
||||
case climate::CLIMATE_FAN_ON:
|
||||
return LOG_STR("ON");
|
||||
case climate::CLIMATE_FAN_OFF:
|
||||
return LOG_STR("OFF");
|
||||
case climate::CLIMATE_FAN_AUTO:
|
||||
return LOG_STR("AUTO");
|
||||
case climate::CLIMATE_FAN_LOW:
|
||||
return LOG_STR("LOW");
|
||||
case climate::CLIMATE_FAN_MEDIUM:
|
||||
return LOG_STR("MEDIUM");
|
||||
case climate::CLIMATE_FAN_HIGH:
|
||||
return LOG_STR("HIGH");
|
||||
case climate::CLIMATE_FAN_MIDDLE:
|
||||
return LOG_STR("MIDDLE");
|
||||
case climate::CLIMATE_FAN_FOCUS:
|
||||
return LOG_STR("FOCUS");
|
||||
case climate::CLIMATE_FAN_DIFFUSE:
|
||||
return LOG_STR("DIFFUSE");
|
||||
case climate::CLIMATE_FAN_QUIET:
|
||||
return LOG_STR("QUIET");
|
||||
default:
|
||||
return LOG_STR("UNKNOWN");
|
||||
}
|
||||
}
|
||||
|
||||
// Climate swing mode strings indexed by ClimateSwingMode enum (0-3): OFF, BOTH, VERTICAL, HORIZONTAL
|
||||
PROGMEM_STRING_TABLE(ClimateSwingModeStrings, "OFF", "BOTH", "VERTICAL", "HORIZONTAL", "UNKNOWN");
|
||||
|
||||
const LogString *climate_swing_mode_to_string(ClimateSwingMode swing_mode) {
|
||||
return ClimateSwingModeStrings::get_log_str(static_cast<uint8_t>(swing_mode), ClimateSwingModeStrings::LAST_INDEX);
|
||||
switch (swing_mode) {
|
||||
case climate::CLIMATE_SWING_OFF:
|
||||
return LOG_STR("OFF");
|
||||
case climate::CLIMATE_SWING_BOTH:
|
||||
return LOG_STR("BOTH");
|
||||
case climate::CLIMATE_SWING_VERTICAL:
|
||||
return LOG_STR("VERTICAL");
|
||||
case climate::CLIMATE_SWING_HORIZONTAL:
|
||||
return LOG_STR("HORIZONTAL");
|
||||
default:
|
||||
return LOG_STR("UNKNOWN");
|
||||
}
|
||||
}
|
||||
|
||||
// Climate preset strings indexed by ClimatePreset enum (0-7): NONE, HOME, AWAY, BOOST, COMFORT, ECO, SLEEP, ACTIVITY
|
||||
PROGMEM_STRING_TABLE(ClimatePresetStrings, "NONE", "HOME", "AWAY", "BOOST", "COMFORT", "ECO", "SLEEP", "ACTIVITY",
|
||||
"UNKNOWN");
|
||||
|
||||
const LogString *climate_preset_to_string(ClimatePreset preset) {
|
||||
return ClimatePresetStrings::get_log_str(static_cast<uint8_t>(preset), ClimatePresetStrings::LAST_INDEX);
|
||||
switch (preset) {
|
||||
case climate::CLIMATE_PRESET_NONE:
|
||||
return LOG_STR("NONE");
|
||||
case climate::CLIMATE_PRESET_HOME:
|
||||
return LOG_STR("HOME");
|
||||
case climate::CLIMATE_PRESET_ECO:
|
||||
return LOG_STR("ECO");
|
||||
case climate::CLIMATE_PRESET_AWAY:
|
||||
return LOG_STR("AWAY");
|
||||
case climate::CLIMATE_PRESET_BOOST:
|
||||
return LOG_STR("BOOST");
|
||||
case climate::CLIMATE_PRESET_COMFORT:
|
||||
return LOG_STR("COMFORT");
|
||||
case climate::CLIMATE_PRESET_SLEEP:
|
||||
return LOG_STR("SLEEP");
|
||||
case climate::CLIMATE_PRESET_ACTIVITY:
|
||||
return LOG_STR("ACTIVITY");
|
||||
default:
|
||||
return LOG_STR("UNKNOWN");
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace esphome::climate
|
||||
|
||||
@@ -19,11 +19,17 @@ const LogString *cover_command_to_str(float pos) {
|
||||
return LOG_STR("UNKNOWN");
|
||||
}
|
||||
}
|
||||
// Cover operation strings indexed by CoverOperation enum (0-2): IDLE, OPENING, CLOSING, plus UNKNOWN
|
||||
PROGMEM_STRING_TABLE(CoverOperationStrings, "IDLE", "OPENING", "CLOSING", "UNKNOWN");
|
||||
|
||||
const LogString *cover_operation_to_str(CoverOperation op) {
|
||||
return CoverOperationStrings::get_log_str(static_cast<uint8_t>(op), CoverOperationStrings::LAST_INDEX);
|
||||
switch (op) {
|
||||
case COVER_OPERATION_IDLE:
|
||||
return LOG_STR("IDLE");
|
||||
case COVER_OPERATION_OPENING:
|
||||
return LOG_STR("OPENING");
|
||||
case COVER_OPERATION_CLOSING:
|
||||
return LOG_STR("CLOSING");
|
||||
default:
|
||||
return LOG_STR("UNKNOWN");
|
||||
}
|
||||
}
|
||||
|
||||
Cover::Cover() : position{COVER_OPEN} {}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -28,28 +28,15 @@ void DlmsMeterComponent::dump_config() {
|
||||
|
||||
void DlmsMeterComponent::loop() {
|
||||
// Read while data is available, netznoe uses two frames so allow 2x max frame length
|
||||
int avail = this->available();
|
||||
if (avail > 0) {
|
||||
size_t remaining = MBUS_MAX_FRAME_LENGTH * 2 - this->receive_buffer_.size();
|
||||
if (remaining == 0) {
|
||||
while (this->available()) {
|
||||
if (this->receive_buffer_.size() >= MBUS_MAX_FRAME_LENGTH * 2) {
|
||||
ESP_LOGW(TAG, "Receive buffer full, dropping remaining bytes");
|
||||
} else {
|
||||
// Read all available bytes in batches to reduce UART call overhead.
|
||||
// Cap reads to remaining buffer capacity.
|
||||
if (static_cast<size_t>(avail) > remaining) {
|
||||
avail = remaining;
|
||||
}
|
||||
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;
|
||||
this->receive_buffer_.insert(this->receive_buffer_.end(), buf, buf + to_read);
|
||||
this->last_read_ = millis();
|
||||
}
|
||||
break;
|
||||
}
|
||||
uint8_t c;
|
||||
this->read_byte(&c);
|
||||
this->receive_buffer_.push_back(c);
|
||||
this->last_read_ = millis();
|
||||
}
|
||||
|
||||
if (!this->receive_buffer_.empty() && millis() - this->last_read_ > this->read_timeout_) {
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -40,7 +40,9 @@ bool Dsmr::ready_to_request_data_() {
|
||||
this->start_requesting_data_();
|
||||
}
|
||||
if (!this->requesting_data_) {
|
||||
this->drain_rx_buffer_();
|
||||
while (this->available()) {
|
||||
this->read();
|
||||
}
|
||||
}
|
||||
}
|
||||
return this->requesting_data_;
|
||||
@@ -113,18 +115,10 @@ void Dsmr::stop_requesting_data_() {
|
||||
} else {
|
||||
ESP_LOGV(TAG, "Stop reading data from P1 port");
|
||||
}
|
||||
this->drain_rx_buffer_();
|
||||
this->requesting_data_ = false;
|
||||
}
|
||||
}
|
||||
|
||||
void Dsmr::drain_rx_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;
|
||||
while (this->available()) {
|
||||
this->read();
|
||||
}
|
||||
this->requesting_data_ = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -139,144 +133,120 @@ void Dsmr::reset_telegram_() {
|
||||
|
||||
void Dsmr::receive_telegram_() {
|
||||
while (this->available_within_timeout_()) {
|
||||
// Read all available bytes in batches to reduce UART call overhead.
|
||||
uint8_t buf[64];
|
||||
int avail = this->available();
|
||||
while (avail > 0) {
|
||||
size_t to_read = std::min(static_cast<size_t>(avail), sizeof(buf));
|
||||
if (!this->read_array(buf, to_read))
|
||||
return;
|
||||
avail -= to_read;
|
||||
const char c = this->read();
|
||||
|
||||
for (size_t i = 0; i < to_read; i++) {
|
||||
const char c = static_cast<char>(buf[i]);
|
||||
// Find a new telegram header, i.e. forward slash.
|
||||
if (c == '/') {
|
||||
ESP_LOGV(TAG, "Header of telegram found");
|
||||
this->reset_telegram_();
|
||||
this->header_found_ = true;
|
||||
}
|
||||
if (!this->header_found_)
|
||||
continue;
|
||||
|
||||
// Find a new telegram header, i.e. forward slash.
|
||||
if (c == '/') {
|
||||
ESP_LOGV(TAG, "Header of telegram found");
|
||||
this->reset_telegram_();
|
||||
this->header_found_ = true;
|
||||
}
|
||||
if (!this->header_found_)
|
||||
continue;
|
||||
// Check for buffer overflow.
|
||||
if (this->bytes_read_ >= this->max_telegram_len_) {
|
||||
this->reset_telegram_();
|
||||
ESP_LOGE(TAG, "Error: telegram larger than buffer (%d bytes)", this->max_telegram_len_);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check for buffer overflow.
|
||||
if (this->bytes_read_ >= this->max_telegram_len_) {
|
||||
this->reset_telegram_();
|
||||
ESP_LOGE(TAG, "Error: telegram larger than buffer (%d bytes)", this->max_telegram_len_);
|
||||
return;
|
||||
}
|
||||
|
||||
// Some v2.2 or v3 meters will send a new value which starts with '('
|
||||
// in a new line, while the value belongs to the previous ObisId. For
|
||||
// proper parsing, remove these new line characters.
|
||||
if (c == '(') {
|
||||
while (true) {
|
||||
auto previous_char = this->telegram_[this->bytes_read_ - 1];
|
||||
if (previous_char == '\n' || previous_char == '\r') {
|
||||
this->bytes_read_--;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Store the byte in the buffer.
|
||||
this->telegram_[this->bytes_read_] = c;
|
||||
this->bytes_read_++;
|
||||
|
||||
// Check for a footer, i.e. exclamation mark, followed by a hex checksum.
|
||||
if (c == '!') {
|
||||
ESP_LOGV(TAG, "Footer of telegram found");
|
||||
this->footer_found_ = true;
|
||||
continue;
|
||||
}
|
||||
// Check for the end of the hex checksum, i.e. a newline.
|
||||
if (this->footer_found_ && c == '\n') {
|
||||
// Parse the telegram and publish sensor values.
|
||||
this->parse_telegram();
|
||||
this->reset_telegram_();
|
||||
return;
|
||||
// Some v2.2 or v3 meters will send a new value which starts with '('
|
||||
// in a new line, while the value belongs to the previous ObisId. For
|
||||
// proper parsing, remove these new line characters.
|
||||
if (c == '(') {
|
||||
while (true) {
|
||||
auto previous_char = this->telegram_[this->bytes_read_ - 1];
|
||||
if (previous_char == '\n' || previous_char == '\r') {
|
||||
this->bytes_read_--;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Store the byte in the buffer.
|
||||
this->telegram_[this->bytes_read_] = c;
|
||||
this->bytes_read_++;
|
||||
|
||||
// Check for a footer, i.e. exclamation mark, followed by a hex checksum.
|
||||
if (c == '!') {
|
||||
ESP_LOGV(TAG, "Footer of telegram found");
|
||||
this->footer_found_ = true;
|
||||
continue;
|
||||
}
|
||||
// Check for the end of the hex checksum, i.e. a newline.
|
||||
if (this->footer_found_ && c == '\n') {
|
||||
// Parse the telegram and publish sensor values.
|
||||
this->parse_telegram();
|
||||
this->reset_telegram_();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Dsmr::receive_encrypted_telegram_() {
|
||||
while (this->available_within_timeout_()) {
|
||||
// Read all available bytes in batches to reduce UART call overhead.
|
||||
uint8_t buf[64];
|
||||
int avail = this->available();
|
||||
while (avail > 0) {
|
||||
size_t to_read = std::min(static_cast<size_t>(avail), sizeof(buf));
|
||||
if (!this->read_array(buf, to_read))
|
||||
return;
|
||||
avail -= to_read;
|
||||
const char c = this->read();
|
||||
|
||||
for (size_t i = 0; i < to_read; i++) {
|
||||
const char c = static_cast<char>(buf[i]);
|
||||
|
||||
// Find a new telegram start byte.
|
||||
if (!this->header_found_) {
|
||||
if ((uint8_t) c != 0xDB) {
|
||||
continue;
|
||||
}
|
||||
ESP_LOGV(TAG, "Start byte 0xDB of encrypted telegram found");
|
||||
this->reset_telegram_();
|
||||
this->header_found_ = true;
|
||||
}
|
||||
|
||||
// Check for buffer overflow.
|
||||
if (this->crypt_bytes_read_ >= this->max_telegram_len_) {
|
||||
this->reset_telegram_();
|
||||
ESP_LOGE(TAG, "Error: encrypted telegram larger than buffer (%d bytes)", this->max_telegram_len_);
|
||||
return;
|
||||
}
|
||||
|
||||
// Store the byte in the buffer.
|
||||
this->crypt_telegram_[this->crypt_bytes_read_] = c;
|
||||
this->crypt_bytes_read_++;
|
||||
|
||||
// Read the length of the incoming encrypted telegram.
|
||||
if (this->crypt_telegram_len_ == 0 && this->crypt_bytes_read_ > 20) {
|
||||
// Complete header + data bytes
|
||||
this->crypt_telegram_len_ = 13 + (this->crypt_telegram_[11] << 8 | this->crypt_telegram_[12]);
|
||||
ESP_LOGV(TAG, "Encrypted telegram length: %d bytes", this->crypt_telegram_len_);
|
||||
}
|
||||
|
||||
// Check for the end of the encrypted telegram.
|
||||
if (this->crypt_telegram_len_ == 0 || this->crypt_bytes_read_ != this->crypt_telegram_len_) {
|
||||
continue;
|
||||
}
|
||||
ESP_LOGV(TAG, "End of encrypted telegram found");
|
||||
|
||||
// Decrypt the encrypted telegram.
|
||||
GCM<AES128> *gcmaes128{new GCM<AES128>()};
|
||||
gcmaes128->setKey(this->decryption_key_.data(), gcmaes128->keySize());
|
||||
// the iv is 8 bytes of the system title + 4 bytes frame counter
|
||||
// system title is at byte 2 and frame counter at byte 15
|
||||
for (int i = 10; i < 14; i++)
|
||||
this->crypt_telegram_[i] = this->crypt_telegram_[i + 4];
|
||||
constexpr uint16_t iv_size{12};
|
||||
gcmaes128->setIV(&this->crypt_telegram_[2], iv_size);
|
||||
gcmaes128->decrypt(reinterpret_cast<uint8_t *>(this->telegram_),
|
||||
// the ciphertext start at byte 18
|
||||
&this->crypt_telegram_[18],
|
||||
// cipher size
|
||||
this->crypt_bytes_read_ - 17);
|
||||
delete gcmaes128; // NOLINT(cppcoreguidelines-owning-memory)
|
||||
|
||||
this->bytes_read_ = strnlen(this->telegram_, this->max_telegram_len_);
|
||||
ESP_LOGV(TAG, "Decrypted telegram size: %d bytes", this->bytes_read_);
|
||||
ESP_LOGVV(TAG, "Decrypted telegram: %s", this->telegram_);
|
||||
|
||||
// Parse the decrypted telegram and publish sensor values.
|
||||
this->parse_telegram();
|
||||
this->reset_telegram_();
|
||||
return;
|
||||
// Find a new telegram start byte.
|
||||
if (!this->header_found_) {
|
||||
if ((uint8_t) c != 0xDB) {
|
||||
continue;
|
||||
}
|
||||
ESP_LOGV(TAG, "Start byte 0xDB of encrypted telegram found");
|
||||
this->reset_telegram_();
|
||||
this->header_found_ = true;
|
||||
}
|
||||
|
||||
// Check for buffer overflow.
|
||||
if (this->crypt_bytes_read_ >= this->max_telegram_len_) {
|
||||
this->reset_telegram_();
|
||||
ESP_LOGE(TAG, "Error: encrypted telegram larger than buffer (%d bytes)", this->max_telegram_len_);
|
||||
return;
|
||||
}
|
||||
|
||||
// Store the byte in the buffer.
|
||||
this->crypt_telegram_[this->crypt_bytes_read_] = c;
|
||||
this->crypt_bytes_read_++;
|
||||
|
||||
// Read the length of the incoming encrypted telegram.
|
||||
if (this->crypt_telegram_len_ == 0 && this->crypt_bytes_read_ > 20) {
|
||||
// Complete header + data bytes
|
||||
this->crypt_telegram_len_ = 13 + (this->crypt_telegram_[11] << 8 | this->crypt_telegram_[12]);
|
||||
ESP_LOGV(TAG, "Encrypted telegram length: %d bytes", this->crypt_telegram_len_);
|
||||
}
|
||||
|
||||
// Check for the end of the encrypted telegram.
|
||||
if (this->crypt_telegram_len_ == 0 || this->crypt_bytes_read_ != this->crypt_telegram_len_) {
|
||||
continue;
|
||||
}
|
||||
ESP_LOGV(TAG, "End of encrypted telegram found");
|
||||
|
||||
// Decrypt the encrypted telegram.
|
||||
GCM<AES128> *gcmaes128{new GCM<AES128>()};
|
||||
gcmaes128->setKey(this->decryption_key_.data(), gcmaes128->keySize());
|
||||
// the iv is 8 bytes of the system title + 4 bytes frame counter
|
||||
// system title is at byte 2 and frame counter at byte 15
|
||||
for (int i = 10; i < 14; i++)
|
||||
this->crypt_telegram_[i] = this->crypt_telegram_[i + 4];
|
||||
constexpr uint16_t iv_size{12};
|
||||
gcmaes128->setIV(&this->crypt_telegram_[2], iv_size);
|
||||
gcmaes128->decrypt(reinterpret_cast<uint8_t *>(this->telegram_),
|
||||
// the ciphertext start at byte 18
|
||||
&this->crypt_telegram_[18],
|
||||
// cipher size
|
||||
this->crypt_bytes_read_ - 17);
|
||||
delete gcmaes128; // NOLINT(cppcoreguidelines-owning-memory)
|
||||
|
||||
this->bytes_read_ = strnlen(this->telegram_, this->max_telegram_len_);
|
||||
ESP_LOGV(TAG, "Decrypted telegram size: %d bytes", this->bytes_read_);
|
||||
ESP_LOGVV(TAG, "Decrypted telegram: %s", this->telegram_);
|
||||
|
||||
// Parse the decrypted telegram and publish sensor values.
|
||||
this->parse_telegram();
|
||||
this->reset_telegram_();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -85,7 +85,6 @@ class Dsmr : public Component, public uart::UARTDevice {
|
||||
void receive_telegram_();
|
||||
void receive_encrypted_telegram_();
|
||||
void reset_telegram_();
|
||||
void drain_rx_buffer_();
|
||||
|
||||
/// Wait for UART data to become available within the read timeout.
|
||||
///
|
||||
|
||||
@@ -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,),
|
||||
),
|
||||
)
|
||||
@@ -10,11 +10,20 @@
|
||||
#ifdef USE_ESP32
|
||||
#ifdef USE_ESP32_BLE_ADVERTISING
|
||||
|
||||
#ifndef CONFIG_ESP_HOSTED_ENABLE_BT_BLUEDROID
|
||||
#include <esp_bt.h>
|
||||
#endif
|
||||
#include <esp_gap_ble_api.h>
|
||||
#include <esp_gatts_api.h>
|
||||
|
||||
namespace esphome::esp32_ble {
|
||||
|
||||
using raw_adv_data_t = struct {
|
||||
uint8_t *data;
|
||||
size_t length;
|
||||
esp_power_level_t power_level;
|
||||
};
|
||||
|
||||
class ESPBTUUID;
|
||||
|
||||
class BLEAdvertising {
|
||||
|
||||
@@ -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;
|
||||
std::string to_string() const; // NOLINT
|
||||
const char *to_str(std::span<char, UUID_STR_LEN> output) const;
|
||||
|
||||
protected:
|
||||
|
||||
@@ -53,10 +53,8 @@ CONFIG_SCHEMA = cv.All(
|
||||
cv.Optional(CONF_MEASURED_POWER, default=-59): cv.int_range(
|
||||
min=-128, max=0
|
||||
),
|
||||
cv.OnlyWithout(CONF_TX_POWER, "esp32_hosted", default="3dBm"): cv.All(
|
||||
cv.conflicts_with_component("esp32_hosted"),
|
||||
cv.decibel,
|
||||
cv.enum(esp32_ble.TX_POWER_LEVELS, int=True),
|
||||
cv.Optional(CONF_TX_POWER, default="3dBm"): cv.All(
|
||||
cv.decibel, cv.enum(esp32_ble.TX_POWER_LEVELS, int=True)
|
||||
),
|
||||
}
|
||||
).extend(cv.COMPONENT_SCHEMA),
|
||||
@@ -84,10 +82,7 @@ async def to_code(config):
|
||||
cg.add(var.set_min_interval(config[CONF_MIN_INTERVAL]))
|
||||
cg.add(var.set_max_interval(config[CONF_MAX_INTERVAL]))
|
||||
cg.add(var.set_measured_power(config[CONF_MEASURED_POWER]))
|
||||
|
||||
# TX power control only available on native Bluetooth (not ESP-Hosted)
|
||||
if CONF_TX_POWER in config:
|
||||
cg.add(var.set_tx_power(config[CONF_TX_POWER]))
|
||||
cg.add(var.set_tx_power(config[CONF_TX_POWER]))
|
||||
|
||||
cg.add_define("USE_ESP32_BLE_ADVERTISING")
|
||||
|
||||
|
||||
@@ -36,16 +36,11 @@ void ESP32BLEBeacon::dump_config() {
|
||||
}
|
||||
}
|
||||
*bpos = '\0';
|
||||
#ifndef CONFIG_ESP_HOSTED_ENABLE_BT_BLUEDROID
|
||||
ESP_LOGCONFIG(TAG,
|
||||
" UUID: %s, Major: %u, Minor: %u, Min Interval: %ums, Max Interval: %ums, Measured Power: %d"
|
||||
", TX Power: %ddBm",
|
||||
uuid, this->major_, this->minor_, this->min_interval_, this->max_interval_, this->measured_power_,
|
||||
(this->tx_power_ * 3) - 12);
|
||||
#else
|
||||
ESP_LOGCONFIG(TAG, " UUID: %s, Major: %u, Minor: %u, Min Interval: %ums, Max Interval: %ums, Measured Power: %d",
|
||||
uuid, this->major_, this->minor_, this->min_interval_, this->max_interval_, this->measured_power_);
|
||||
#endif
|
||||
}
|
||||
|
||||
float ESP32BLEBeacon::get_setup_priority() const { return setup_priority::AFTER_BLUETOOTH; }
|
||||
@@ -79,14 +74,11 @@ void ESP32BLEBeacon::on_advertise_() {
|
||||
ibeacon_adv_data.ibeacon_vendor.major = byteswap(this->major_);
|
||||
ibeacon_adv_data.ibeacon_vendor.measured_power = static_cast<uint8_t>(this->measured_power_);
|
||||
|
||||
esp_err_t err;
|
||||
#ifndef CONFIG_ESP_HOSTED_ENABLE_BT_BLUEDROID
|
||||
ESP_LOGD(TAG, "Setting BLE TX power");
|
||||
err = esp_ble_tx_power_set(ESP_BLE_PWR_TYPE_ADV, this->tx_power_);
|
||||
esp_err_t err = esp_ble_tx_power_set(ESP_BLE_PWR_TYPE_ADV, this->tx_power_);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGW(TAG, "esp_ble_tx_power_set failed: %s", esp_err_to_name(err));
|
||||
}
|
||||
#endif
|
||||
err = esp_ble_gap_config_adv_data_raw((uint8_t *) &ibeacon_adv_data, sizeof(ibeacon_adv_data));
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "esp_ble_gap_config_adv_data_raw failed: %s", esp_err_to_name(err));
|
||||
|
||||
@@ -48,9 +48,7 @@ class ESP32BLEBeacon : public Component, public GAPEventHandler, public Parented
|
||||
void set_min_interval(uint16_t val) { this->min_interval_ = val; }
|
||||
void set_max_interval(uint16_t val) { this->max_interval_ = val; }
|
||||
void set_measured_power(int8_t val) { this->measured_power_ = val; }
|
||||
#ifndef CONFIG_ESP_HOSTED_ENABLE_BT_BLUEDROID
|
||||
void set_tx_power(esp_power_level_t val) { this->tx_power_ = val; }
|
||||
#endif
|
||||
void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) override;
|
||||
|
||||
protected:
|
||||
@@ -62,9 +60,7 @@ class ESP32BLEBeacon : public Component, public GAPEventHandler, public Parented
|
||||
uint16_t min_interval_{};
|
||||
uint16_t max_interval_{};
|
||||
int8_t measured_power_{};
|
||||
#ifndef CONFIG_ESP_HOSTED_ENABLE_BT_BLUEDROID
|
||||
esp_power_level_t tx_power_{};
|
||||
#endif
|
||||
esp_ble_adv_params_t ble_adv_params_;
|
||||
bool advertising_{false};
|
||||
};
|
||||
|
||||
@@ -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};
|
||||
|
||||
@@ -2,18 +2,21 @@
|
||||
#include "esphome/core/defines.h"
|
||||
#include "esphome/core/controller_registry.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/progmem.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace fan {
|
||||
|
||||
static const char *const TAG = "fan";
|
||||
|
||||
// Fan direction strings indexed by FanDirection enum (0-1): FORWARD, REVERSE, plus UNKNOWN
|
||||
PROGMEM_STRING_TABLE(FanDirectionStrings, "FORWARD", "REVERSE", "UNKNOWN");
|
||||
|
||||
const LogString *fan_direction_to_string(FanDirection direction) {
|
||||
return FanDirectionStrings::get_log_str(static_cast<uint8_t>(direction), FanDirectionStrings::LAST_INDEX);
|
||||
switch (direction) {
|
||||
case FanDirection::FORWARD:
|
||||
return LOG_STR("FORWARD");
|
||||
case FanDirection::REVERSE:
|
||||
return LOG_STR("REVERSE");
|
||||
default:
|
||||
return LOG_STR("UNKNOWN");
|
||||
}
|
||||
}
|
||||
|
||||
FanCall &FanCall::set_preset_mode(const std::string &preset_mode) {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
#include "gpio_binary_sensor.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/progmem.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace gpio {
|
||||
@@ -8,12 +7,17 @@ namespace gpio {
|
||||
static const char *const TAG = "gpio.binary_sensor";
|
||||
|
||||
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_DEBUG
|
||||
// Interrupt type strings indexed by edge-triggered InterruptType values:
|
||||
// indices 1-3: RISING_EDGE, FALLING_EDGE, ANY_EDGE; other values (e.g. level-triggered) map to UNKNOWN (index 0).
|
||||
PROGMEM_STRING_TABLE(InterruptTypeStrings, "UNKNOWN", "RISING_EDGE", "FALLING_EDGE", "ANY_EDGE");
|
||||
|
||||
static const LogString *interrupt_type_to_string(gpio::InterruptType type) {
|
||||
return InterruptTypeStrings::get_log_str(static_cast<uint8_t>(type), 0);
|
||||
switch (type) {
|
||||
case gpio::INTERRUPT_RISING_EDGE:
|
||||
return LOG_STR("RISING_EDGE");
|
||||
case gpio::INTERRUPT_FALLING_EDGE:
|
||||
return LOG_STR("FALLING_EDGE");
|
||||
case gpio::INTERRUPT_ANY_EDGE:
|
||||
return LOG_STR("ANY_EDGE");
|
||||
default:
|
||||
return LOG_STR("UNKNOWN");
|
||||
}
|
||||
}
|
||||
|
||||
static const LogString *gpio_mode_to_string(bool use_interrupt) {
|
||||
|
||||
@@ -133,10 +133,20 @@ std::shared_ptr<HttpContainer> HttpRequestArduino::perform(const std::string &ur
|
||||
|
||||
// HTTPClient::getSize() returns -1 for chunked transfer encoding (no Content-Length).
|
||||
// When cast to size_t, -1 becomes SIZE_MAX (4294967295 on 32-bit).
|
||||
// The read() method uses a chunked transfer encoding decoder (read_chunked_) to strip
|
||||
// chunk framing and deliver only decoded content. When the final 0-size chunk is received,
|
||||
// is_chunked_ is cleared and content_length is set to the actual decoded size, so
|
||||
// is_read_complete() returns true and callers exit their read loops correctly.
|
||||
// The read() method handles this: bytes_read_ can never reach SIZE_MAX, so the
|
||||
// early return check (bytes_read_ >= content_length) will never trigger.
|
||||
//
|
||||
// TODO: Chunked transfer encoding is NOT properly supported on Arduino.
|
||||
// The implementation in #7884 was incomplete - it only works correctly on ESP-IDF where
|
||||
// esp_http_client_read() decodes chunks internally. On Arduino, using getStreamPtr()
|
||||
// returns raw TCP data with chunk framing (e.g., "12a\r\n{json}\r\n0\r\n\r\n") instead
|
||||
// of decoded content. This wasn't noticed because requests would complete and payloads
|
||||
// were only examined on IDF. The long transfer times were also masked by the misleading
|
||||
// "HTTP on Arduino version >= 3.1 is **very** slow" warning above. This causes two issues:
|
||||
// 1. Response body is corrupted - contains chunk size headers mixed with data
|
||||
// 2. Cannot detect end of transfer - connection stays open (keep-alive), causing timeout
|
||||
// The proper fix would be to use getString() for chunked responses, which decodes chunks
|
||||
// internally, but this buffers the entire response in memory.
|
||||
int content_length = container->client_.getSize();
|
||||
ESP_LOGD(TAG, "Content-Length: %d", content_length);
|
||||
container->content_length = (size_t) content_length;
|
||||
@@ -164,10 +174,6 @@ std::shared_ptr<HttpContainer> HttpRequestArduino::perform(const std::string &ur
|
||||
// > 0: bytes read
|
||||
// 0: no data yet, retry <-- NOTE: 0 means retry, NOT EOF!
|
||||
// < 0: error/connection closed <-- connection closed returns -1, not 0
|
||||
//
|
||||
// For chunked transfer encoding, read_chunked_() decodes chunk framing and delivers
|
||||
// only the payload data. When the final 0-size chunk is received, it clears is_chunked_
|
||||
// and sets content_length = bytes_read_ so is_read_complete() returns true.
|
||||
int HttpContainerArduino::read(uint8_t *buf, size_t max_len) {
|
||||
const uint32_t start = millis();
|
||||
watchdog::WatchdogManager wdm(this->parent_->get_watchdog_timeout());
|
||||
@@ -178,42 +184,24 @@ int HttpContainerArduino::read(uint8_t *buf, size_t max_len) {
|
||||
return HTTP_ERROR_CONNECTION_CLOSED;
|
||||
}
|
||||
|
||||
if (this->is_chunked_) {
|
||||
int result = this->read_chunked_(buf, max_len, stream_ptr);
|
||||
this->duration_ms += (millis() - start);
|
||||
if (result > 0) {
|
||||
return result;
|
||||
}
|
||||
// result <= 0: check for completion or errors
|
||||
if (this->is_read_complete()) {
|
||||
return 0; // Chunked transfer complete (final 0-size chunk received)
|
||||
}
|
||||
if (result < 0) {
|
||||
return result; // Stream error during chunk decoding
|
||||
}
|
||||
// read_chunked_ returned 0: no data was available (available() was 0).
|
||||
// This happens when the TCP buffer is empty - either more data is in flight,
|
||||
// or the connection dropped. Arduino's connected() returns false only when
|
||||
// both the remote has closed AND the receive buffer is empty, so any buffered
|
||||
// data is fully drained before we report the drop.
|
||||
if (!stream_ptr->connected()) {
|
||||
return HTTP_ERROR_CONNECTION_CLOSED;
|
||||
}
|
||||
return 0; // No data yet, caller should retry
|
||||
}
|
||||
|
||||
// Non-chunked path
|
||||
int available_data = stream_ptr->available();
|
||||
// For chunked transfer encoding, HTTPClient::getSize() returns -1, which becomes SIZE_MAX when
|
||||
// cast to size_t. SIZE_MAX - bytes_read_ is still huge, so it won't limit the read.
|
||||
size_t remaining = (this->content_length > 0) ? (this->content_length - this->bytes_read_) : max_len;
|
||||
int bufsize = std::min(max_len, std::min(remaining, (size_t) available_data));
|
||||
|
||||
if (bufsize == 0) {
|
||||
this->duration_ms += (millis() - start);
|
||||
// Check if we've read all expected content (non-chunked only)
|
||||
// For chunked encoding (content_length == SIZE_MAX), is_read_complete() returns false
|
||||
if (this->is_read_complete()) {
|
||||
return 0; // All content read successfully
|
||||
}
|
||||
// No data available - check if connection is still open
|
||||
// For chunked encoding, !connected() after reading means EOF (all chunks received)
|
||||
// For known content_length with bytes_read_ < content_length, it means connection dropped
|
||||
if (!stream_ptr->connected()) {
|
||||
return HTTP_ERROR_CONNECTION_CLOSED;
|
||||
return HTTP_ERROR_CONNECTION_CLOSED; // Connection closed or EOF for chunked
|
||||
}
|
||||
return 0; // No data yet, caller should retry
|
||||
}
|
||||
@@ -227,143 +215,6 @@ int HttpContainerArduino::read(uint8_t *buf, size_t max_len) {
|
||||
return read_len;
|
||||
}
|
||||
|
||||
void HttpContainerArduino::chunk_header_complete_() {
|
||||
if (this->chunk_remaining_ == 0) {
|
||||
this->chunk_state_ = ChunkedState::CHUNK_TRAILER;
|
||||
this->chunk_remaining_ = 1; // repurpose as at-start-of-line flag
|
||||
} else {
|
||||
this->chunk_state_ = ChunkedState::CHUNK_DATA;
|
||||
}
|
||||
}
|
||||
|
||||
// Chunked transfer encoding decoder
|
||||
//
|
||||
// On Arduino, getStreamPtr() returns raw TCP data. For chunked responses, this includes
|
||||
// chunk framing (size headers, CRLF delimiters) mixed with payload data. This decoder
|
||||
// strips the framing and delivers only decoded content to the caller.
|
||||
//
|
||||
// Chunk format (RFC 9112 Section 7.1):
|
||||
// <hex-size>[;extension]\r\n
|
||||
// <data bytes>\r\n
|
||||
// ...
|
||||
// 0\r\n
|
||||
// [trailer-field\r\n]*
|
||||
// \r\n
|
||||
//
|
||||
// Non-blocking: only processes bytes already in the TCP receive buffer.
|
||||
// State (chunk_state_, chunk_remaining_) is preserved between calls, so partial
|
||||
// chunk headers or split \r\n sequences resume correctly on the next call.
|
||||
// Framing bytes (hex sizes, \r\n) may be consumed without producing output;
|
||||
// the caller sees 0 and retries via the normal read timeout logic.
|
||||
//
|
||||
// WiFiClient::read() returns -1 on error despite available() > 0 (connection reset
|
||||
// between check and read). On any stream error (c < 0 or readBytes <= 0), we return
|
||||
// already-decoded data if any; otherwise HTTP_ERROR_CONNECTION_CLOSED. The error
|
||||
// will surface again on the next call since the stream stays broken.
|
||||
//
|
||||
// Returns: > 0 decoded bytes, 0 no data available, < 0 error
|
||||
int HttpContainerArduino::read_chunked_(uint8_t *buf, size_t max_len, WiFiClient *stream) {
|
||||
int total_decoded = 0;
|
||||
|
||||
while (total_decoded < (int) max_len && this->chunk_state_ != ChunkedState::COMPLETE) {
|
||||
// Non-blocking: only process what's already buffered
|
||||
if (stream->available() == 0)
|
||||
break;
|
||||
|
||||
// CHUNK_DATA reads multiple bytes; handle before the single-byte switch
|
||||
if (this->chunk_state_ == ChunkedState::CHUNK_DATA) {
|
||||
// Only read what's available, what fits in buf, and what remains in this chunk
|
||||
size_t to_read =
|
||||
std::min({max_len - (size_t) total_decoded, this->chunk_remaining_, (size_t) stream->available()});
|
||||
if (to_read == 0)
|
||||
break;
|
||||
App.feed_wdt();
|
||||
int read_len = stream->readBytes(buf + total_decoded, to_read);
|
||||
if (read_len <= 0)
|
||||
return total_decoded > 0 ? total_decoded : HTTP_ERROR_CONNECTION_CLOSED;
|
||||
total_decoded += read_len;
|
||||
this->chunk_remaining_ -= read_len;
|
||||
this->bytes_read_ += read_len;
|
||||
if (this->chunk_remaining_ == 0)
|
||||
this->chunk_state_ = ChunkedState::CHUNK_DATA_TRAIL;
|
||||
continue;
|
||||
}
|
||||
|
||||
// All other states consume a single byte
|
||||
int c = stream->read();
|
||||
if (c < 0)
|
||||
return total_decoded > 0 ? total_decoded : HTTP_ERROR_CONNECTION_CLOSED;
|
||||
|
||||
switch (this->chunk_state_) {
|
||||
// Parse hex chunk size, one byte at a time: "<hex>[;ext]\r\n"
|
||||
// Note: if no hex digits are parsed (e.g., bare \r\n), chunk_remaining_ stays 0
|
||||
// and is treated as the final chunk. This is intentionally lenient — on embedded
|
||||
// devices, rejecting malformed framing is less useful than terminating cleanly.
|
||||
// Overflow of chunk_remaining_ from extremely long hex strings (>8 digits on
|
||||
// 32-bit) is not checked; >4GB chunks are unrealistic on embedded targets and
|
||||
// would simply cause fewer bytes to be read from that chunk.
|
||||
case ChunkedState::CHUNK_HEADER:
|
||||
if (c == '\n') {
|
||||
// \n terminates the size line; chunk_remaining_ == 0 means last chunk
|
||||
this->chunk_header_complete_();
|
||||
} else {
|
||||
uint8_t hex = parse_hex_char(c);
|
||||
if (hex != INVALID_HEX_CHAR) {
|
||||
this->chunk_remaining_ = (this->chunk_remaining_ << 4) | hex;
|
||||
} else if (c != '\r') {
|
||||
this->chunk_state_ = ChunkedState::CHUNK_HEADER_EXT; // ';' starts extension, skip to \n
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
// Skip chunk extension bytes until \n (e.g., ";name=value\r\n")
|
||||
case ChunkedState::CHUNK_HEADER_EXT:
|
||||
if (c == '\n') {
|
||||
this->chunk_header_complete_();
|
||||
}
|
||||
break;
|
||||
|
||||
// Consume \r\n trailing each chunk's data
|
||||
case ChunkedState::CHUNK_DATA_TRAIL:
|
||||
if (c == '\n') {
|
||||
this->chunk_state_ = ChunkedState::CHUNK_HEADER;
|
||||
this->chunk_remaining_ = 0; // reset for next chunk's hex accumulation
|
||||
}
|
||||
// else: \r is consumed silently, next iteration gets \n
|
||||
break;
|
||||
|
||||
// Consume optional trailer headers and terminating empty line after final chunk.
|
||||
// Per RFC 9112 Section 7.1: "0\r\n" is followed by optional "field\r\n" lines
|
||||
// and a final "\r\n". chunk_remaining_ is repurposed as a flag: 1 = at start
|
||||
// of line (may be the empty terminator), 0 = mid-line (reading a trailer field).
|
||||
case ChunkedState::CHUNK_TRAILER:
|
||||
if (c == '\n') {
|
||||
if (this->chunk_remaining_ != 0) {
|
||||
this->chunk_state_ = ChunkedState::COMPLETE; // Empty line terminates trailers
|
||||
} else {
|
||||
this->chunk_remaining_ = 1; // End of trailer field, at start of next line
|
||||
}
|
||||
} else if (c != '\r') {
|
||||
this->chunk_remaining_ = 0; // Non-CRLF char: reading a trailer field
|
||||
}
|
||||
// \r doesn't change the flag — it's part of \r\n line endings
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
if (this->chunk_state_ == ChunkedState::COMPLETE) {
|
||||
// Clear chunked flag and set content_length to actual decoded size so
|
||||
// is_read_complete() returns true and callers exit their read loops
|
||||
this->is_chunked_ = false;
|
||||
this->content_length = this->bytes_read_;
|
||||
}
|
||||
}
|
||||
|
||||
return total_decoded;
|
||||
}
|
||||
|
||||
void HttpContainerArduino::end() {
|
||||
watchdog::WatchdogManager wdm(this->parent_->get_watchdog_timeout());
|
||||
this->client_.end();
|
||||
|
||||
@@ -18,17 +18,6 @@
|
||||
namespace esphome::http_request {
|
||||
|
||||
class HttpRequestArduino;
|
||||
|
||||
/// State machine for decoding chunked transfer encoding on Arduino
|
||||
enum class ChunkedState : uint8_t {
|
||||
CHUNK_HEADER, ///< Reading hex digits of chunk size
|
||||
CHUNK_HEADER_EXT, ///< Skipping chunk extensions until \n
|
||||
CHUNK_DATA, ///< Reading chunk data bytes
|
||||
CHUNK_DATA_TRAIL, ///< Skipping \r\n after chunk data
|
||||
CHUNK_TRAILER, ///< Consuming trailer headers after final 0-size chunk
|
||||
COMPLETE, ///< Finished: final chunk and trailers consumed
|
||||
};
|
||||
|
||||
class HttpContainerArduino : public HttpContainer {
|
||||
public:
|
||||
int read(uint8_t *buf, size_t max_len) override;
|
||||
@@ -37,13 +26,6 @@ class HttpContainerArduino : public HttpContainer {
|
||||
protected:
|
||||
friend class HttpRequestArduino;
|
||||
HTTPClient client_{};
|
||||
|
||||
/// Decode chunked transfer encoding from the raw stream
|
||||
int read_chunked_(uint8_t *buf, size_t max_len, WiFiClient *stream);
|
||||
/// Transition from chunk header to data or trailer based on parsed size
|
||||
void chunk_header_complete_();
|
||||
ChunkedState chunk_state_{ChunkedState::CHUNK_HEADER};
|
||||
size_t chunk_remaining_{0}; ///< Bytes remaining in current chunk
|
||||
};
|
||||
|
||||
class HttpRequestArduino : public HttpRequestComponent {
|
||||
|
||||
@@ -133,10 +133,8 @@ uint8_t OtaHttpRequestComponent::do_ota_() {
|
||||
auto result = http_read_loop_result(bufsize_or_error, last_data_time, read_timeout, container->is_read_complete());
|
||||
if (result == HttpReadLoopResult::RETRY)
|
||||
continue;
|
||||
// For non-chunked responses, COMPLETE is unreachable (loop condition checks bytes_read < content_length).
|
||||
// For chunked responses, the decoder sets content_length = bytes_read when the final chunk arrives,
|
||||
// which causes the loop condition to terminate. But COMPLETE can still be returned if the decoder
|
||||
// finishes mid-read, so this is needed for correctness.
|
||||
// Note: COMPLETE is currently unreachable since the loop condition checks bytes_read < content_length,
|
||||
// but this is defensive code in case chunked transfer encoding support is added for OTA in the future.
|
||||
if (result == HttpReadLoopResult::COMPLETE)
|
||||
break;
|
||||
if (result != HttpReadLoopResult::DATA) {
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -335,10 +335,9 @@ void LD2420Component::revert_config_action() {
|
||||
|
||||
void LD2420Component::loop() {
|
||||
// If there is a active send command do not process it here, the send command call will handle it.
|
||||
if (this->cmd_active_) {
|
||||
return;
|
||||
while (!this->cmd_active_ && this->available()) {
|
||||
this->readline_(this->read(), this->buffer_data_, MAX_LINE_LENGTH);
|
||||
}
|
||||
this->read_batch_(this->buffer_data_);
|
||||
}
|
||||
|
||||
void LD2420Component::update_radar_data(uint16_t const *gate_energy, uint8_t sample_number) {
|
||||
@@ -540,23 +539,6 @@ void LD2420Component::handle_simple_mode_(const uint8_t *inbuf, int len) {
|
||||
}
|
||||
}
|
||||
|
||||
void LD2420Component::read_batch_(std::span<uint8_t, MAX_LINE_LENGTH> buffer) {
|
||||
// Read all available bytes in batches to reduce UART call overhead.
|
||||
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], buffer.data(), buffer.size());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void LD2420Component::handle_ack_data_(uint8_t *buffer, int len) {
|
||||
this->cmd_reply_.command = buffer[CMD_FRAME_COMMAND];
|
||||
this->cmd_reply_.length = buffer[CMD_FRAME_DATA_LENGTH];
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
#include "esphome/components/uart/uart.h"
|
||||
#include "esphome/core/automation.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include <span>
|
||||
#ifdef USE_TEXT_SENSOR
|
||||
#include "esphome/components/text_sensor/text_sensor.h"
|
||||
#endif
|
||||
@@ -166,7 +165,6 @@ class LD2420Component : public Component, public uart::UARTDevice {
|
||||
void handle_energy_mode_(uint8_t *buffer, int len);
|
||||
void handle_ack_data_(uint8_t *buffer, int len);
|
||||
void readline_(int rx_data, uint8_t *buffer, int len);
|
||||
void read_batch_(std::span<uint8_t, MAX_LINE_LENGTH> buffer);
|
||||
void set_calibration_(bool state) { this->calibration_ = state; };
|
||||
bool get_calibration_() { return this->calibration_; };
|
||||
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
#include "light_state.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/optional.h"
|
||||
#include "esphome/core/progmem.h"
|
||||
|
||||
namespace esphome::light {
|
||||
|
||||
@@ -52,13 +51,26 @@ static void log_invalid_parameter(const char *name, const LogString *message) {
|
||||
return *this; \
|
||||
}
|
||||
|
||||
// Color mode human-readable strings indexed by ColorModeBitPolicy::to_bit() (0-9)
|
||||
// Index 0 is Unknown (for ColorMode::UNKNOWN), also used as fallback for out-of-range
|
||||
PROGMEM_STRING_TABLE(ColorModeHumanStrings, "Unknown", "On/Off", "Brightness", "White", "Color temperature",
|
||||
"Cold/warm white", "RGB", "RGBW", "RGB + color temperature", "RGB + cold/warm white");
|
||||
|
||||
static const LogString *color_mode_to_human(ColorMode color_mode) {
|
||||
return ColorModeHumanStrings::get_log_str(ColorModeBitPolicy::to_bit(color_mode), 0);
|
||||
if (color_mode == ColorMode::ON_OFF)
|
||||
return LOG_STR("On/Off");
|
||||
if (color_mode == ColorMode::BRIGHTNESS)
|
||||
return LOG_STR("Brightness");
|
||||
if (color_mode == ColorMode::WHITE)
|
||||
return LOG_STR("White");
|
||||
if (color_mode == ColorMode::COLOR_TEMPERATURE)
|
||||
return LOG_STR("Color temperature");
|
||||
if (color_mode == ColorMode::COLD_WARM_WHITE)
|
||||
return LOG_STR("Cold/warm white");
|
||||
if (color_mode == ColorMode::RGB)
|
||||
return LOG_STR("RGB");
|
||||
if (color_mode == ColorMode::RGB_WHITE)
|
||||
return LOG_STR("RGBW");
|
||||
if (color_mode == ColorMode::RGB_COLD_WARM_WHITE)
|
||||
return LOG_STR("RGB + cold/warm white");
|
||||
if (color_mode == ColorMode::RGB_COLOR_TEMPERATURE)
|
||||
return LOG_STR("RGB + color temperature");
|
||||
return LOG_STR("Unknown");
|
||||
}
|
||||
|
||||
// Helper to log percentage values
|
||||
@@ -445,52 +457,6 @@ ColorMode LightCall::compute_color_mode_() {
|
||||
LOG_STR_ARG(color_mode_to_human(color_mode)));
|
||||
return color_mode;
|
||||
}
|
||||
// PROGMEM lookup table for get_suitable_color_modes_mask_().
|
||||
// Maps 4-bit key (white | ct<<1 | cwww<<2 | rgb<<3) to color mode bitmask.
|
||||
// Packed into uint8_t by right-shifting by PACK_SHIFT since the lower bits
|
||||
// (UNKNOWN, ON_OFF, BRIGHTNESS) are never present in suitable mode masks.
|
||||
static constexpr unsigned PACK_SHIFT = ColorModeBitPolicy::to_bit(ColorMode::WHITE);
|
||||
// clang-format off
|
||||
static constexpr uint8_t SUITABLE_COLOR_MODES[] PROGMEM = {
|
||||
// [0] none - all modes with brightness
|
||||
static_cast<uint8_t>(ColorModeMask({ColorMode::RGB_WHITE, ColorMode::RGB_COLOR_TEMPERATURE,
|
||||
ColorMode::RGB_COLD_WARM_WHITE, ColorMode::RGB, ColorMode::WHITE, ColorMode::COLOR_TEMPERATURE,
|
||||
ColorMode::COLD_WARM_WHITE}).get_mask() >> PACK_SHIFT),
|
||||
// [1] white only
|
||||
static_cast<uint8_t>(ColorModeMask({ColorMode::WHITE, ColorMode::RGB_WHITE, ColorMode::RGB_COLOR_TEMPERATURE,
|
||||
ColorMode::COLD_WARM_WHITE, ColorMode::RGB_COLD_WARM_WHITE}).get_mask() >> PACK_SHIFT),
|
||||
// [2] ct only
|
||||
static_cast<uint8_t>(ColorModeMask({ColorMode::COLOR_TEMPERATURE, ColorMode::RGB_COLOR_TEMPERATURE,
|
||||
ColorMode::COLD_WARM_WHITE, ColorMode::RGB_COLD_WARM_WHITE}).get_mask() >> PACK_SHIFT),
|
||||
// [3] white + ct
|
||||
static_cast<uint8_t>(ColorModeMask({ColorMode::COLD_WARM_WHITE, ColorMode::RGB_COLOR_TEMPERATURE,
|
||||
ColorMode::RGB_COLD_WARM_WHITE}).get_mask() >> PACK_SHIFT),
|
||||
// [4] cwww only
|
||||
static_cast<uint8_t>(ColorModeMask({ColorMode::COLD_WARM_WHITE,
|
||||
ColorMode::RGB_COLD_WARM_WHITE}).get_mask() >> PACK_SHIFT),
|
||||
0, // [5] white + cwww (conflicting)
|
||||
0, // [6] ct + cwww (conflicting)
|
||||
0, // [7] white + ct + cwww (conflicting)
|
||||
// [8] rgb only
|
||||
static_cast<uint8_t>(ColorModeMask({ColorMode::RGB, ColorMode::RGB_WHITE, ColorMode::RGB_COLOR_TEMPERATURE,
|
||||
ColorMode::RGB_COLD_WARM_WHITE}).get_mask() >> PACK_SHIFT),
|
||||
// [9] rgb + white
|
||||
static_cast<uint8_t>(ColorModeMask({ColorMode::RGB_WHITE, ColorMode::RGB_COLOR_TEMPERATURE,
|
||||
ColorMode::RGB_COLD_WARM_WHITE}).get_mask() >> PACK_SHIFT),
|
||||
// [10] rgb + ct
|
||||
static_cast<uint8_t>(ColorModeMask({ColorMode::RGB_COLOR_TEMPERATURE,
|
||||
ColorMode::RGB_COLD_WARM_WHITE}).get_mask() >> PACK_SHIFT),
|
||||
// [11] rgb + white + ct
|
||||
static_cast<uint8_t>(ColorModeMask({ColorMode::RGB_COLOR_TEMPERATURE,
|
||||
ColorMode::RGB_COLD_WARM_WHITE}).get_mask() >> PACK_SHIFT),
|
||||
// [12] rgb + cwww
|
||||
static_cast<uint8_t>(ColorModeMask({ColorMode::RGB_COLD_WARM_WHITE}).get_mask() >> PACK_SHIFT),
|
||||
0, // [13] rgb + white + cwww (conflicting)
|
||||
0, // [14] rgb + ct + cwww (conflicting)
|
||||
0, // [15] all (conflicting)
|
||||
};
|
||||
// clang-format on
|
||||
|
||||
color_mode_bitmask_t LightCall::get_suitable_color_modes_mask_() {
|
||||
bool has_white = this->has_white() && this->white_ > 0.0f;
|
||||
bool has_ct = this->has_color_temperature();
|
||||
@@ -500,8 +466,46 @@ color_mode_bitmask_t LightCall::get_suitable_color_modes_mask_() {
|
||||
(this->has_red() || this->has_green() || this->has_blue());
|
||||
|
||||
// Build key from flags: [rgb][cwww][ct][white]
|
||||
uint8_t key = has_white | (has_ct << 1) | (has_cwww << 2) | (has_rgb << 3);
|
||||
return static_cast<color_mode_bitmask_t>(progmem_read_byte(&SUITABLE_COLOR_MODES[key])) << PACK_SHIFT;
|
||||
#define KEY(white, ct, cwww, rgb) ((white) << 0 | (ct) << 1 | (cwww) << 2 | (rgb) << 3)
|
||||
|
||||
uint8_t key = KEY(has_white, has_ct, has_cwww, has_rgb);
|
||||
|
||||
switch (key) {
|
||||
case KEY(true, false, false, false): // white only
|
||||
return ColorModeMask({ColorMode::WHITE, ColorMode::RGB_WHITE, ColorMode::RGB_COLOR_TEMPERATURE,
|
||||
ColorMode::COLD_WARM_WHITE, ColorMode::RGB_COLD_WARM_WHITE})
|
||||
.get_mask();
|
||||
case KEY(false, true, false, false): // ct only
|
||||
return ColorModeMask({ColorMode::COLOR_TEMPERATURE, ColorMode::RGB_COLOR_TEMPERATURE, ColorMode::COLD_WARM_WHITE,
|
||||
ColorMode::RGB_COLD_WARM_WHITE})
|
||||
.get_mask();
|
||||
case KEY(true, true, false, false): // white + ct
|
||||
return ColorModeMask(
|
||||
{ColorMode::COLD_WARM_WHITE, ColorMode::RGB_COLOR_TEMPERATURE, ColorMode::RGB_COLD_WARM_WHITE})
|
||||
.get_mask();
|
||||
case KEY(false, false, true, false): // cwww only
|
||||
return ColorModeMask({ColorMode::COLD_WARM_WHITE, ColorMode::RGB_COLD_WARM_WHITE}).get_mask();
|
||||
case KEY(false, false, false, false): // none
|
||||
return ColorModeMask({ColorMode::RGB_WHITE, ColorMode::RGB_COLOR_TEMPERATURE, ColorMode::RGB_COLD_WARM_WHITE,
|
||||
ColorMode::RGB, ColorMode::WHITE, ColorMode::COLOR_TEMPERATURE, ColorMode::COLD_WARM_WHITE})
|
||||
.get_mask();
|
||||
case KEY(true, false, false, true): // rgb + white
|
||||
return ColorModeMask({ColorMode::RGB_WHITE, ColorMode::RGB_COLOR_TEMPERATURE, ColorMode::RGB_COLD_WARM_WHITE})
|
||||
.get_mask();
|
||||
case KEY(false, true, false, true): // rgb + ct
|
||||
case KEY(true, true, false, true): // rgb + white + ct
|
||||
return ColorModeMask({ColorMode::RGB_COLOR_TEMPERATURE, ColorMode::RGB_COLD_WARM_WHITE}).get_mask();
|
||||
case KEY(false, false, true, true): // rgb + cwww
|
||||
return ColorModeMask({ColorMode::RGB_COLD_WARM_WHITE}).get_mask();
|
||||
case KEY(false, false, false, true): // rgb only
|
||||
return ColorModeMask({ColorMode::RGB, ColorMode::RGB_WHITE, ColorMode::RGB_COLOR_TEMPERATURE,
|
||||
ColorMode::RGB_COLD_WARM_WHITE})
|
||||
.get_mask();
|
||||
default:
|
||||
return 0; // conflicting flags
|
||||
}
|
||||
|
||||
#undef KEY
|
||||
}
|
||||
|
||||
LightCall &LightCall::set_effect(const char *effect, size_t len) {
|
||||
|
||||
@@ -9,19 +9,32 @@ namespace esphome::light {
|
||||
|
||||
// See https://www.home-assistant.io/integrations/light.mqtt/#json-schema for documentation on the schema
|
||||
|
||||
// Color mode JSON strings - packed into flash with compile-time generated offsets.
|
||||
// Indexed by ColorModeBitPolicy bit index (1-9), so index 0 maps to bit 1 ("onoff").
|
||||
PROGMEM_STRING_TABLE(ColorModeStrings, "onoff", "brightness", "white", "color_temp", "cwww", "rgb", "rgbw", "rgbct",
|
||||
"rgbww");
|
||||
|
||||
// Get JSON string for color mode. Returns nullptr for UNKNOWN (bit 0).
|
||||
// Returns ProgmemStr so ArduinoJson knows to handle PROGMEM strings on ESP8266.
|
||||
// Get JSON string for color mode.
|
||||
// ColorMode enum values are sparse bitmasks (0, 1, 3, 7, 11, 19, 35, 39, 47, 51) which would
|
||||
// generate a large jump table. Converting to bit index (0-9) allows a compact switch.
|
||||
static ProgmemStr get_color_mode_json_str(ColorMode mode) {
|
||||
unsigned bit = ColorModeBitPolicy::to_bit(mode);
|
||||
if (bit == 0)
|
||||
return nullptr;
|
||||
// bit is 1-9 for valid modes, so bit-1 is always valid (0-8). LAST_INDEX fallback never used.
|
||||
return ColorModeStrings::get_progmem_str(bit - 1, ColorModeStrings::LAST_INDEX);
|
||||
switch (ColorModeBitPolicy::to_bit(mode)) {
|
||||
case 1:
|
||||
return ESPHOME_F("onoff");
|
||||
case 2:
|
||||
return ESPHOME_F("brightness");
|
||||
case 3:
|
||||
return ESPHOME_F("white");
|
||||
case 4:
|
||||
return ESPHOME_F("color_temp");
|
||||
case 5:
|
||||
return ESPHOME_F("cwww");
|
||||
case 6:
|
||||
return ESPHOME_F("rgb");
|
||||
case 7:
|
||||
return ESPHOME_F("rgbw");
|
||||
case 8:
|
||||
return ESPHOME_F("rgbct");
|
||||
case 9:
|
||||
return ESPHOME_F("rgbww");
|
||||
default:
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
void LightJSONSchema::dump_json(LightState &state, JsonObject root) {
|
||||
|
||||
@@ -8,12 +8,22 @@ namespace esphome::lock {
|
||||
|
||||
static const char *const TAG = "lock";
|
||||
|
||||
// Lock state strings indexed by LockState enum (0-5): NONE(UNKNOWN), LOCKED, UNLOCKED, JAMMED, LOCKING, UNLOCKING
|
||||
// Index 0 is UNKNOWN (for LOCK_STATE_NONE), also used as fallback for out-of-range
|
||||
PROGMEM_STRING_TABLE(LockStateStrings, "UNKNOWN", "LOCKED", "UNLOCKED", "JAMMED", "LOCKING", "UNLOCKING");
|
||||
|
||||
const LogString *lock_state_to_string(LockState state) {
|
||||
return LockStateStrings::get_log_str(static_cast<uint8_t>(state), 0);
|
||||
switch (state) {
|
||||
case LOCK_STATE_LOCKED:
|
||||
return LOG_STR("LOCKED");
|
||||
case LOCK_STATE_UNLOCKED:
|
||||
return LOG_STR("UNLOCKED");
|
||||
case LOCK_STATE_JAMMED:
|
||||
return LOG_STR("JAMMED");
|
||||
case LOCK_STATE_LOCKING:
|
||||
return LOG_STR("LOCKING");
|
||||
case LOCK_STATE_UNLOCKING:
|
||||
return LOG_STR("UNLOCKING");
|
||||
case LOCK_STATE_NONE:
|
||||
default:
|
||||
return LOG_STR("UNKNOWN");
|
||||
}
|
||||
}
|
||||
|
||||
Lock::Lock() : state(LOCK_STATE_NONE) {}
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
#include "esphome/core/application.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/progmem.h"
|
||||
|
||||
namespace esphome::logger {
|
||||
|
||||
@@ -242,20 +241,34 @@ UARTSelection Logger::get_uart() const { return this->uart_; }
|
||||
|
||||
float Logger::get_setup_priority() const { return setup_priority::BUS + 500.0f; }
|
||||
|
||||
// Log level strings - packed into flash on ESP8266, indexed by log level (0-7)
|
||||
PROGMEM_STRING_TABLE(LogLevelStrings, "NONE", "ERROR", "WARN", "INFO", "CONFIG", "DEBUG", "VERBOSE", "VERY_VERBOSE");
|
||||
#ifdef USE_STORE_LOG_STR_IN_FLASH
|
||||
// ESP8266: PSTR() cannot be used in array initializers, so we need to declare
|
||||
// each string separately as a global constant first
|
||||
static const char LOG_LEVEL_NONE[] PROGMEM = "NONE";
|
||||
static const char LOG_LEVEL_ERROR[] PROGMEM = "ERROR";
|
||||
static const char LOG_LEVEL_WARN[] PROGMEM = "WARN";
|
||||
static const char LOG_LEVEL_INFO[] PROGMEM = "INFO";
|
||||
static const char LOG_LEVEL_CONFIG[] PROGMEM = "CONFIG";
|
||||
static const char LOG_LEVEL_DEBUG[] PROGMEM = "DEBUG";
|
||||
static const char LOG_LEVEL_VERBOSE[] PROGMEM = "VERBOSE";
|
||||
static const char LOG_LEVEL_VERY_VERBOSE[] PROGMEM = "VERY_VERBOSE";
|
||||
|
||||
static const LogString *get_log_level_str(uint8_t level) {
|
||||
return LogLevelStrings::get_log_str(level, LogLevelStrings::LAST_INDEX);
|
||||
}
|
||||
static const LogString *const LOG_LEVELS[] = {
|
||||
reinterpret_cast<const LogString *>(LOG_LEVEL_NONE), reinterpret_cast<const LogString *>(LOG_LEVEL_ERROR),
|
||||
reinterpret_cast<const LogString *>(LOG_LEVEL_WARN), reinterpret_cast<const LogString *>(LOG_LEVEL_INFO),
|
||||
reinterpret_cast<const LogString *>(LOG_LEVEL_CONFIG), reinterpret_cast<const LogString *>(LOG_LEVEL_DEBUG),
|
||||
reinterpret_cast<const LogString *>(LOG_LEVEL_VERBOSE), reinterpret_cast<const LogString *>(LOG_LEVEL_VERY_VERBOSE),
|
||||
};
|
||||
#else
|
||||
static const char *const LOG_LEVELS[] = {"NONE", "ERROR", "WARN", "INFO", "CONFIG", "DEBUG", "VERBOSE", "VERY_VERBOSE"};
|
||||
#endif
|
||||
|
||||
void Logger::dump_config() {
|
||||
ESP_LOGCONFIG(TAG,
|
||||
"Logger:\n"
|
||||
" Max Level: %s\n"
|
||||
" Initial Level: %s",
|
||||
LOG_STR_ARG(get_log_level_str(ESPHOME_LOG_LEVEL)),
|
||||
LOG_STR_ARG(get_log_level_str(this->current_level_)));
|
||||
LOG_STR_ARG(LOG_LEVELS[ESPHOME_LOG_LEVEL]), LOG_STR_ARG(LOG_LEVELS[this->current_level_]));
|
||||
#ifndef USE_HOST
|
||||
ESP_LOGCONFIG(TAG,
|
||||
" Log Baud Rate: %" PRIu32 "\n"
|
||||
@@ -274,7 +287,7 @@ void Logger::dump_config() {
|
||||
|
||||
#ifdef USE_LOGGER_RUNTIME_TAG_LEVELS
|
||||
for (auto &it : this->log_levels_) {
|
||||
ESP_LOGCONFIG(TAG, " Level for '%s': %s", it.first, LOG_STR_ARG(get_log_level_str(it.second)));
|
||||
ESP_LOGCONFIG(TAG, " Level for '%s': %s", it.first, LOG_STR_ARG(LOG_LEVELS[it.second]));
|
||||
}
|
||||
#endif
|
||||
}
|
||||
@@ -282,8 +295,7 @@ void Logger::dump_config() {
|
||||
void Logger::set_log_level(uint8_t level) {
|
||||
if (level > ESPHOME_LOG_LEVEL) {
|
||||
level = ESPHOME_LOG_LEVEL;
|
||||
ESP_LOGW(TAG, "Cannot set log level higher than pre-compiled %s",
|
||||
LOG_STR_ARG(get_log_level_str(ESPHOME_LOG_LEVEL)));
|
||||
ESP_LOGW(TAG, "Cannot set log level higher than pre-compiled %s", LOG_STR_ARG(LOG_LEVELS[ESPHOME_LOG_LEVEL]));
|
||||
}
|
||||
this->current_level_ = level;
|
||||
#ifdef USE_LOGGER_LEVEL_LISTENERS
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -56,23 +56,17 @@ void PylontechComponent::setup() {
|
||||
void PylontechComponent::update() { this->write_str("pwr\n"); }
|
||||
|
||||
void PylontechComponent::loop() {
|
||||
int avail = this->available();
|
||||
if (avail > 0) {
|
||||
if (this->available() > 0) {
|
||||
// pylontech sends a lot of data very suddenly
|
||||
// we need to quickly put it all into our own buffer, otherwise the uart's buffer will overflow
|
||||
uint8_t data;
|
||||
int recv = 0;
|
||||
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;
|
||||
recv += to_read;
|
||||
|
||||
for (size_t i = 0; i < to_read; i++) {
|
||||
buffer_[buffer_index_write_] += (char) buf[i];
|
||||
if (buf[i] == ASCII_LF || buffer_[buffer_index_write_].length() >= MAX_DATA_LENGTH_BYTES) {
|
||||
while (this->available() > 0) {
|
||||
if (this->read_byte(&data)) {
|
||||
buffer_[buffer_index_write_] += (char) data;
|
||||
recv++;
|
||||
if (buffer_[buffer_index_write_].back() == static_cast<char>(ASCII_LF) ||
|
||||
buffer_[buffer_index_write_].length() >= MAX_DATA_LENGTH_BYTES) {
|
||||
// complete line received
|
||||
buffer_index_write_ = (buffer_index_write_ + 1) % NUM_BUFFERS;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
#include "esphome/core/defines.h"
|
||||
#include "esphome/core/controller_registry.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/progmem.h"
|
||||
|
||||
namespace esphome::sensor {
|
||||
|
||||
@@ -31,13 +30,20 @@ void log_sensor(const char *tag, const char *prefix, const char *type, Sensor *o
|
||||
}
|
||||
}
|
||||
|
||||
// State class strings indexed by StateClass enum (0-4): NONE, MEASUREMENT, TOTAL_INCREASING, TOTAL, MEASUREMENT_ANGLE
|
||||
PROGMEM_STRING_TABLE(StateClassStrings, "", "measurement", "total_increasing", "total", "measurement_angle");
|
||||
static_assert(StateClassStrings::COUNT == STATE_CLASS_LAST + 1, "StateClassStrings must match StateClass enum");
|
||||
|
||||
const LogString *state_class_to_string(StateClass state_class) {
|
||||
// Fallback to index 0 (empty string for STATE_CLASS_NONE) if out of range
|
||||
return StateClassStrings::get_log_str(static_cast<uint8_t>(state_class), 0);
|
||||
switch (state_class) {
|
||||
case STATE_CLASS_MEASUREMENT:
|
||||
return LOG_STR("measurement");
|
||||
case STATE_CLASS_TOTAL_INCREASING:
|
||||
return LOG_STR("total_increasing");
|
||||
case STATE_CLASS_TOTAL:
|
||||
return LOG_STR("total");
|
||||
case STATE_CLASS_MEASUREMENT_ANGLE:
|
||||
return LOG_STR("measurement_angle");
|
||||
case STATE_CLASS_NONE:
|
||||
default:
|
||||
return LOG_STR("");
|
||||
}
|
||||
}
|
||||
|
||||
Sensor::Sensor() : state(NAN), raw_state(NAN) {}
|
||||
|
||||
@@ -32,7 +32,6 @@ enum StateClass : uint8_t {
|
||||
STATE_CLASS_TOTAL = 3,
|
||||
STATE_CLASS_MEASUREMENT_ANGLE = 4
|
||||
};
|
||||
constexpr uint8_t STATE_CLASS_LAST = static_cast<uint8_t>(STATE_CLASS_MEASUREMENT_ANGLE);
|
||||
|
||||
const LogString *state_class_to_string(StateClass state_class);
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -2,21 +2,12 @@
|
||||
#include "esphome/core/defines.h"
|
||||
#include "esphome/core/controller_registry.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/progmem.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace update {
|
||||
|
||||
static const char *const TAG = "update";
|
||||
|
||||
// Update state strings indexed by UpdateState enum (0-3): UNKNOWN, NO UPDATE, UPDATE AVAILABLE, INSTALLING
|
||||
PROGMEM_STRING_TABLE(UpdateStateStrings, "UNKNOWN", "NO UPDATE", "UPDATE AVAILABLE", "INSTALLING");
|
||||
|
||||
const LogString *update_state_to_string(UpdateState state) {
|
||||
return UpdateStateStrings::get_log_str(static_cast<uint8_t>(state),
|
||||
static_cast<uint8_t>(UpdateState::UPDATE_STATE_UNKNOWN));
|
||||
}
|
||||
|
||||
void UpdateEntity::publish_state() {
|
||||
ESP_LOGD(TAG,
|
||||
"'%s' >>\n"
|
||||
|
||||
@@ -27,8 +27,6 @@ enum UpdateState : uint8_t {
|
||||
UPDATE_STATE_INSTALLING,
|
||||
};
|
||||
|
||||
const LogString *update_state_to_string(UpdateState state);
|
||||
|
||||
class UpdateEntity : public EntityBase, public EntityBase_DeviceClass {
|
||||
public:
|
||||
void publish_state();
|
||||
|
||||
@@ -23,11 +23,17 @@ const LogString *valve_command_to_str(float pos) {
|
||||
return LOG_STR("UNKNOWN");
|
||||
}
|
||||
}
|
||||
// Valve operation strings indexed by ValveOperation enum (0-2): IDLE, OPENING, CLOSING, plus UNKNOWN
|
||||
PROGMEM_STRING_TABLE(ValveOperationStrings, "IDLE", "OPENING", "CLOSING", "UNKNOWN");
|
||||
|
||||
const LogString *valve_operation_to_str(ValveOperation op) {
|
||||
return ValveOperationStrings::get_log_str(static_cast<uint8_t>(op), ValveOperationStrings::LAST_INDEX);
|
||||
switch (op) {
|
||||
case VALVE_OPERATION_IDLE:
|
||||
return LOG_STR("IDLE");
|
||||
case VALVE_OPERATION_OPENING:
|
||||
return LOG_STR("OPENING");
|
||||
case VALVE_OPERATION_CLOSING:
|
||||
return LOG_STR("CLOSING");
|
||||
default:
|
||||
return LOG_STR("UNKNOWN");
|
||||
}
|
||||
}
|
||||
|
||||
Valve::Valve() : position{VALVE_OPEN} {}
|
||||
|
||||
@@ -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 {
|
||||
std::string to_string() const { // NOLINT
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -239,13 +233,25 @@ void WaterHeater::set_visual_target_temperature_step_override(float visual_targe
|
||||
}
|
||||
#endif
|
||||
|
||||
// Water heater mode strings indexed by WaterHeaterMode enum (0-6): OFF, ECO, ELECTRIC, PERFORMANCE, HIGH_DEMAND,
|
||||
// HEAT_PUMP, GAS
|
||||
PROGMEM_STRING_TABLE(WaterHeaterModeStrings, "OFF", "ECO", "ELECTRIC", "PERFORMANCE", "HIGH_DEMAND", "HEAT_PUMP", "GAS",
|
||||
"UNKNOWN");
|
||||
|
||||
const LogString *water_heater_mode_to_string(WaterHeaterMode mode) {
|
||||
return WaterHeaterModeStrings::get_log_str(static_cast<uint8_t>(mode), WaterHeaterModeStrings::LAST_INDEX);
|
||||
switch (mode) {
|
||||
case WATER_HEATER_MODE_OFF:
|
||||
return LOG_STR("OFF");
|
||||
case WATER_HEATER_MODE_ECO:
|
||||
return LOG_STR("ECO");
|
||||
case WATER_HEATER_MODE_ELECTRIC:
|
||||
return LOG_STR("ELECTRIC");
|
||||
case WATER_HEATER_MODE_PERFORMANCE:
|
||||
return LOG_STR("PERFORMANCE");
|
||||
case WATER_HEATER_MODE_HIGH_DEMAND:
|
||||
return LOG_STR("HIGH_DEMAND");
|
||||
case WATER_HEATER_MODE_HEAT_PUMP:
|
||||
return LOG_STR("HEAT_PUMP");
|
||||
case WATER_HEATER_MODE_GAS:
|
||||
return LOG_STR("GAS");
|
||||
default:
|
||||
return LOG_STR("UNKNOWN");
|
||||
}
|
||||
}
|
||||
|
||||
void WaterHeater::dump_traits_(const char *tag) {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user