mirror of
https://github.com/esphome/esphome.git
synced 2026-02-10 09:42:01 +00:00
Compare commits
117 Commits
http_reque
...
integratio
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
86c1803538 | ||
|
|
eb8bb260e5 | ||
|
|
ef079f9113 | ||
|
|
647f39504a | ||
|
|
b6c98f0586 | ||
|
|
77b46ba90f | ||
|
|
8cc6915558 | ||
|
|
10e71255a0 | ||
|
|
30521f7de6 | ||
|
|
c97ae656e4 | ||
|
|
f09e72766e | ||
|
|
a320e87a17 | ||
|
|
45932fabea | ||
|
|
f62ea6bdc2 | ||
|
|
675625cf25 | ||
|
|
6baeaf5b7b | ||
|
|
ca8617cf10 | ||
|
|
a0bc6a9922 | ||
|
|
d5295a894b | ||
|
|
fb6b96ff58 | ||
|
|
e07144ef74 | ||
|
|
1c4cf1a3e8 | ||
|
|
a198df34ee | ||
|
|
15904ab583 | ||
|
|
9c9e8ac388 | ||
|
|
04f4636d36 | ||
|
|
3cbadfe42a | ||
|
|
277a11f0ea | ||
|
|
08cca414e7 | ||
|
|
4db5835b6f | ||
|
|
527003e16b | ||
|
|
e9a0d06880 | ||
|
|
b2879f7f99 | ||
|
|
44e9346e9c | ||
|
|
6670c2b6c4 | ||
|
|
6013b473ca | ||
|
|
cc1f83ac35 | ||
|
|
1f1405364d | ||
|
|
5d3ae8cbec | ||
|
|
59a2f6f538 | ||
|
|
a9c37cae26 | ||
|
|
c8a93f31e9 | ||
|
|
f79448a09a | ||
|
|
5e096826c3 | ||
|
|
457d68256d | ||
|
|
a9029fb67a | ||
|
|
cd891d4b16 | ||
|
|
2784059a64 | ||
|
|
4827f53156 | ||
|
|
8dff0ee449 | ||
|
|
a7f04a6cf9 | ||
|
|
53bde863f5 | ||
|
|
dfb0c8670d | ||
|
|
7490efedd7 | ||
|
|
c28c97fbaf | ||
|
|
3cde3daceb | ||
|
|
be4e573cc4 | ||
|
|
66af998098 | ||
|
|
938a11595d | ||
|
|
c812ac8b29 | ||
|
|
248fc06dac | ||
|
|
8b8acb3b27 | ||
|
|
1c60efa4b6 | ||
|
|
4ef238eb7b | ||
|
|
22c77866d8 | ||
|
|
790ac620ab | ||
|
|
a0f736b7aa | ||
|
|
21f270677b | ||
|
|
d6e692e302 | ||
|
|
991ce396a9 | ||
|
|
68dfb844bd | ||
|
|
9742880bf7 | ||
|
|
13f9726534 | ||
|
|
dd07e25a8f | ||
|
|
a875a2fb9b | ||
|
|
fb93283720 | ||
|
|
bed01da345 | ||
|
|
422f413680 | ||
|
|
c3c0c40524 | ||
|
|
46f8302d8f | ||
|
|
e24528c842 | ||
|
|
5370687001 | ||
|
|
6ee185c58a | ||
|
|
eb6a6f8d0d | ||
|
|
140ec0639c | ||
|
|
756f1c6b7e | ||
|
|
28b9487b25 | ||
|
|
41fedaedb3 | ||
|
|
7b40e8afcb | ||
|
|
a43e3e5948 | ||
|
|
9de91539e6 | ||
|
|
836bfc625d | ||
|
|
2a17592d57 | ||
|
|
04697ac223 | ||
|
|
3f3cf83aab | ||
|
|
39013388dd | ||
|
|
cfbeea9983 | ||
|
|
8f6e1abbce | ||
|
|
c77d70c093 | ||
|
|
25762c62f8 | ||
|
|
441ec35d9f | ||
|
|
33c831dbb8 | ||
|
|
38aeb9be37 | ||
|
|
6b7c52799d | ||
|
|
f19bb2cd0a | ||
|
|
26c98a1e25 | ||
|
|
b544cf2ffe | ||
|
|
6d1281301f | ||
|
|
901192cca1 | ||
|
|
67e7ba4812 | ||
|
|
572376091e | ||
|
|
e7c9808b87 | ||
|
|
eb7aa3420f | ||
|
|
86f91eed2f | ||
|
|
41cecbfb0f | ||
|
|
9315da79bc | ||
|
|
155447f541 |
@@ -1 +1 @@
|
|||||||
069fa9526c52f7c580a9ec17c7678d12f142221387e9b561c18f95394d4629a3
|
37ec8d5a343c8d0a485fd2118cbdabcbccd7b9bca197e4a392be75087974dced
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ _READELF_SECTION_PATTERN = re.compile(
|
|||||||
# Component category prefixes
|
# Component category prefixes
|
||||||
_COMPONENT_PREFIX_ESPHOME = "[esphome]"
|
_COMPONENT_PREFIX_ESPHOME = "[esphome]"
|
||||||
_COMPONENT_PREFIX_EXTERNAL = "[external]"
|
_COMPONENT_PREFIX_EXTERNAL = "[external]"
|
||||||
|
_COMPONENT_PREFIX_LIB = "[lib]"
|
||||||
_COMPONENT_CORE = f"{_COMPONENT_PREFIX_ESPHOME}core"
|
_COMPONENT_CORE = f"{_COMPONENT_PREFIX_ESPHOME}core"
|
||||||
_COMPONENT_API = f"{_COMPONENT_PREFIX_ESPHOME}api"
|
_COMPONENT_API = f"{_COMPONENT_PREFIX_ESPHOME}api"
|
||||||
|
|
||||||
@@ -56,6 +57,16 @@ SymbolInfoType = tuple[str, int, str]
|
|||||||
# RAM sections - symbols in these sections consume RAM
|
# RAM sections - symbols in these sections consume RAM
|
||||||
RAM_SECTIONS = frozenset([".data", ".bss"])
|
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
|
@dataclass
|
||||||
class MemorySection:
|
class MemorySection:
|
||||||
@@ -179,11 +190,19 @@ class MemoryAnalyzer:
|
|||||||
self._sdk_symbols: list[SDKSymbol] = []
|
self._sdk_symbols: list[SDKSymbol] = []
|
||||||
# CSWTCH symbols: list of (name, size, source_file, component)
|
# CSWTCH symbols: list of (name, size, source_file, component)
|
||||||
self._cswtch_symbols: list[tuple[str, int, str, str]] = []
|
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]:
|
def analyze(self) -> dict[str, ComponentMemory]:
|
||||||
"""Analyze the ELF file and return component memory usage."""
|
"""Analyze the ELF file and return component memory usage."""
|
||||||
self._parse_sections()
|
self._parse_sections()
|
||||||
self._parse_symbols()
|
self._parse_symbols()
|
||||||
|
self._scan_libraries()
|
||||||
self._categorize_symbols()
|
self._categorize_symbols()
|
||||||
self._analyze_cswtch_symbols()
|
self._analyze_cswtch_symbols()
|
||||||
self._analyze_sdk_libraries()
|
self._analyze_sdk_libraries()
|
||||||
@@ -328,15 +347,19 @@ class MemoryAnalyzer:
|
|||||||
# If no component match found, it's core
|
# If no component match found, it's core
|
||||||
return _COMPONENT_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
|
# Check against symbol patterns
|
||||||
for component, patterns in SYMBOL_PATTERNS.items():
|
for component, patterns in SYMBOL_PATTERNS.items():
|
||||||
if any(pattern in symbol_name for pattern in patterns):
|
if any(pattern in symbol_name for pattern in patterns):
|
||||||
return component
|
return self._heuristic_to_lib.get(component, component)
|
||||||
|
|
||||||
# Check against demangled patterns
|
# Check against demangled patterns
|
||||||
for component, patterns in DEMANGLED_PATTERNS.items():
|
for component, patterns in DEMANGLED_PATTERNS.items():
|
||||||
if any(pattern in demangled for pattern in patterns):
|
if any(pattern in demangled for pattern in patterns):
|
||||||
return component
|
return self._heuristic_to_lib.get(component, component)
|
||||||
|
|
||||||
# Special cases that need more complex logic
|
# Special cases that need more complex logic
|
||||||
|
|
||||||
@@ -384,6 +407,327 @@ class MemoryAnalyzer:
|
|||||||
|
|
||||||
return "Other Core"
|
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:
|
def _find_object_files_dir(self) -> Path | None:
|
||||||
"""Find the directory containing object files for this build.
|
"""Find the directory containing object files for this build.
|
||||||
|
|
||||||
@@ -397,47 +741,38 @@ class MemoryAnalyzer:
|
|||||||
return pioenvs_dir
|
return pioenvs_dir
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def _scan_cswtch_in_objects(
|
@staticmethod
|
||||||
self, obj_dir: Path
|
def _parse_nm_cswtch_output(
|
||||||
) -> dict[str, list[tuple[str, int]]]:
|
output: str,
|
||||||
"""Scan object files for CSWTCH symbols using a single nm invocation.
|
base_dir: Path | None,
|
||||||
|
cswtch_map: dict[str, list[tuple[str, int]]],
|
||||||
|
) -> None:
|
||||||
|
"""Parse nm output for CSWTCH symbols and add to cswtch_map.
|
||||||
|
|
||||||
Uses ``nm --print-file-name -S`` on all ``.o`` files at once.
|
Handles both ``.o`` files and ``.a`` archives.
|
||||||
Output format: ``/path/to/file.o:address size type name``
|
|
||||||
|
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:
|
Args:
|
||||||
obj_dir: Directory containing object files (.pioenvs/<env>/)
|
output: Raw stdout from ``nm --print-file-name -S``.
|
||||||
|
base_dir: Base directory for computing relative paths of ``.o`` files.
|
||||||
Returns:
|
Pass ``None`` when scanning archives outside the build tree.
|
||||||
Dict mapping "CSWTCH$NNN:size" to list of (source_file, size) tuples.
|
cswtch_map: Dict to populate, mapping ``"CSWTCH$N:size"`` to source list.
|
||||||
"""
|
"""
|
||||||
cswtch_map: dict[str, list[tuple[str, int]]] = defaultdict(list)
|
for line in output.splitlines():
|
||||||
|
|
||||||
if not self.nm_path:
|
|
||||||
return cswtch_map
|
|
||||||
|
|
||||||
# Find all .o files recursively, sorted for deterministic output
|
|
||||||
obj_files = sorted(obj_dir.rglob("*.o"))
|
|
||||||
if not obj_files:
|
|
||||||
return cswtch_map
|
|
||||||
|
|
||||||
_LOGGER.debug("Scanning %d object files for CSWTCH symbols", len(obj_files))
|
|
||||||
|
|
||||||
# Single nm call with --print-file-name for all object files
|
|
||||||
result = run_tool(
|
|
||||||
[self.nm_path, "--print-file-name", "-S"] + [str(f) for f in obj_files],
|
|
||||||
timeout=30,
|
|
||||||
)
|
|
||||||
if result is None or result.returncode != 0:
|
|
||||||
return cswtch_map
|
|
||||||
|
|
||||||
for line in result.stdout.splitlines():
|
|
||||||
if "CSWTCH$" not in line:
|
if "CSWTCH$" not in line:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Split on last ":" that precedes a hex address.
|
# Split on last ":" that precedes a hex address.
|
||||||
# nm --print-file-name format: filepath:hex_addr hex_size type name
|
# For .o: "filepath.o" : "hex_addr hex_size type name"
|
||||||
# We split from the right: find the last colon followed by hex digits.
|
# For .a: "filepath.a:member.o" : "hex_addr hex_size type name"
|
||||||
parts_after_colon = line.rsplit(":", 1)
|
parts_after_colon = line.rsplit(":", 1)
|
||||||
if len(parts_after_colon) != 2:
|
if len(parts_after_colon) != 2:
|
||||||
continue
|
continue
|
||||||
@@ -457,16 +792,89 @@ class MemoryAnalyzer:
|
|||||||
except ValueError:
|
except ValueError:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Get relative path from obj_dir for readability
|
# Determine readable source path
|
||||||
try:
|
# Use ".a:" to detect archive format (not bare ":" which matches
|
||||||
rel_path = str(Path(file_path).relative_to(obj_dir))
|
# Windows drive letters like "C:\...\file.o").
|
||||||
except ValueError:
|
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
|
rel_path = file_path
|
||||||
|
|
||||||
key = f"{sym_name}:{size}"
|
key = f"{sym_name}:{size}"
|
||||||
cswtch_map[key].append((rel_path, size))
|
cswtch_map[key].append((rel_path, size))
|
||||||
|
|
||||||
return cswtch_map
|
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:
|
def _source_file_to_component(self, source_file: str) -> str:
|
||||||
"""Map a source object file path to its component name.
|
"""Map a source object file path to its component name.
|
||||||
@@ -495,9 +903,21 @@ class MemoryAnalyzer:
|
|||||||
if "esphome" in parts and "components" not in parts:
|
if "esphome" in parts and "components" not in parts:
|
||||||
return _COMPONENT_CORE
|
return _COMPONENT_CORE
|
||||||
|
|
||||||
# Framework/library files - return the first path component
|
# Framework/library files - check for PlatformIO library hash dirs
|
||||||
# e.g., lib65b/ESPAsyncTCP/... -> lib65b
|
# e.g., lib65b/ESPAsyncTCP/... -> [lib]espasynctcp
|
||||||
# FrameworkArduino/... -> FrameworkArduino
|
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
|
return parts[0] if parts else source_file
|
||||||
|
|
||||||
def _analyze_cswtch_symbols(self) -> None:
|
def _analyze_cswtch_symbols(self) -> None:
|
||||||
@@ -505,17 +925,25 @@ class MemoryAnalyzer:
|
|||||||
|
|
||||||
CSWTCH symbols are compiler-generated lookup tables for switch statements.
|
CSWTCH symbols are compiler-generated lookup tables for switch statements.
|
||||||
They are local symbols, so the same name can appear in different object files.
|
They are local symbols, so the same name can appear in different object files.
|
||||||
This method scans .o files to attribute them to their source components.
|
This method scans .o files and SDK archives to attribute them to their
|
||||||
|
source components.
|
||||||
"""
|
"""
|
||||||
obj_dir = self._find_object_files_dir()
|
obj_dir = self._find_object_files_dir()
|
||||||
if obj_dir is None:
|
if obj_dir is None:
|
||||||
_LOGGER.debug("No object files directory found, skipping CSWTCH analysis")
|
_LOGGER.debug("No object files directory found, skipping CSWTCH analysis")
|
||||||
return
|
return
|
||||||
|
|
||||||
# Scan object files for CSWTCH symbols
|
# Scan build-dir object files for CSWTCH symbols
|
||||||
cswtch_map = self._scan_cswtch_in_objects(obj_dir)
|
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:
|
if not cswtch_map:
|
||||||
_LOGGER.debug("No CSWTCH symbols found in object files")
|
_LOGGER.debug("No CSWTCH symbols found in object files or SDK archives")
|
||||||
return
|
return
|
||||||
|
|
||||||
# Collect CSWTCH symbols from the ELF (already parsed in sections)
|
# Collect CSWTCH symbols from the ELF (already parsed in sections)
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ from . import (
|
|||||||
_COMPONENT_CORE,
|
_COMPONENT_CORE,
|
||||||
_COMPONENT_PREFIX_ESPHOME,
|
_COMPONENT_PREFIX_ESPHOME,
|
||||||
_COMPONENT_PREFIX_EXTERNAL,
|
_COMPONENT_PREFIX_EXTERNAL,
|
||||||
|
_COMPONENT_PREFIX_LIB,
|
||||||
RAM_SECTIONS,
|
RAM_SECTIONS,
|
||||||
MemoryAnalyzer,
|
MemoryAnalyzer,
|
||||||
)
|
)
|
||||||
@@ -407,6 +408,11 @@ class MemoryAnalyzerCLI(MemoryAnalyzer):
|
|||||||
for name, mem in components
|
for name, mem in components
|
||||||
if name.startswith(_COMPONENT_PREFIX_EXTERNAL)
|
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(
|
top_esphome_components = sorted(
|
||||||
esphome_components, key=lambda x: x[1].flash_total, reverse=True
|
esphome_components, key=lambda x: x[1].flash_total, reverse=True
|
||||||
@@ -417,6 +423,11 @@ class MemoryAnalyzerCLI(MemoryAnalyzer):
|
|||||||
external_components, key=lambda x: x[1].flash_total, reverse=True
|
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
|
# Check if API component exists and ensure it's included
|
||||||
api_component = None
|
api_component = None
|
||||||
for name, mem in components:
|
for name, mem in components:
|
||||||
@@ -435,10 +446,11 @@ class MemoryAnalyzerCLI(MemoryAnalyzer):
|
|||||||
if name in system_components_to_include
|
if name in system_components_to_include
|
||||||
]
|
]
|
||||||
|
|
||||||
# Combine all components to analyze: top ESPHome + all external + API if not already included + system components
|
# Combine all components to analyze: top ESPHome + all external + libraries + API if not already included + system components
|
||||||
components_to_analyze = (
|
components_to_analyze = (
|
||||||
list(top_esphome_components)
|
list(top_esphome_components)
|
||||||
+ list(top_external_components)
|
+ list(top_external_components)
|
||||||
|
+ list(top_library_components)
|
||||||
+ system_components
|
+ system_components
|
||||||
)
|
)
|
||||||
if api_component and api_component not in components_to_analyze:
|
if api_component and api_component not in components_to_analyze:
|
||||||
|
|||||||
@@ -87,6 +87,7 @@ from esphome.cpp_types import ( # noqa: F401
|
|||||||
size_t,
|
size_t,
|
||||||
std_ns,
|
std_ns,
|
||||||
std_shared_ptr,
|
std_shared_ptr,
|
||||||
|
std_span,
|
||||||
std_string,
|
std_string,
|
||||||
std_string_ref,
|
std_string_ref,
|
||||||
std_vector,
|
std_vector,
|
||||||
|
|||||||
@@ -283,7 +283,7 @@ void APIConnection::loop() {
|
|||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
bool APIConnection::send_disconnect_response(const DisconnectRequest &msg) {
|
bool APIConnection::send_disconnect_response_() {
|
||||||
// remote initiated disconnect_client
|
// remote initiated disconnect_client
|
||||||
// don't close yet, we still need to send the disconnect response
|
// don't close yet, we still need to send the disconnect response
|
||||||
// close will happen on next loop
|
// close will happen on next loop
|
||||||
@@ -292,7 +292,7 @@ bool APIConnection::send_disconnect_response(const DisconnectRequest &msg) {
|
|||||||
DisconnectResponse resp;
|
DisconnectResponse resp;
|
||||||
return this->send_message(resp, DisconnectResponse::MESSAGE_TYPE);
|
return this->send_message(resp, DisconnectResponse::MESSAGE_TYPE);
|
||||||
}
|
}
|
||||||
void APIConnection::on_disconnect_response(const DisconnectResponse &value) {
|
void APIConnection::on_disconnect_response() {
|
||||||
this->helper_->close();
|
this->helper_->close();
|
||||||
this->flags_.remove = true;
|
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();
|
msg.device_class = cover->get_device_class_ref();
|
||||||
return fill_and_encode_entity_info(cover, msg, ListEntitiesCoverResponse::MESSAGE_TYPE, conn, remaining_size);
|
return fill_and_encode_entity_info(cover, msg, ListEntitiesCoverResponse::MESSAGE_TYPE, conn, remaining_size);
|
||||||
}
|
}
|
||||||
void APIConnection::cover_command(const CoverCommandRequest &msg) {
|
void APIConnection::on_cover_command_request(const CoverCommandRequest &msg) {
|
||||||
ENTITY_COMMAND_MAKE_CALL(cover::Cover, cover, cover)
|
ENTITY_COMMAND_MAKE_CALL(cover::Cover, cover, cover)
|
||||||
if (msg.has_position)
|
if (msg.has_position)
|
||||||
call.set_position(msg.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();
|
msg.supported_preset_modes = &traits.supported_preset_modes();
|
||||||
return fill_and_encode_entity_info(fan, msg, ListEntitiesFanResponse::MESSAGE_TYPE, conn, remaining_size);
|
return fill_and_encode_entity_info(fan, msg, ListEntitiesFanResponse::MESSAGE_TYPE, conn, remaining_size);
|
||||||
}
|
}
|
||||||
void APIConnection::fan_command(const FanCommandRequest &msg) {
|
void APIConnection::on_fan_command_request(const FanCommandRequest &msg) {
|
||||||
ENTITY_COMMAND_MAKE_CALL(fan::Fan, fan, fan)
|
ENTITY_COMMAND_MAKE_CALL(fan::Fan, fan, fan)
|
||||||
if (msg.has_state)
|
if (msg.has_state)
|
||||||
call.set_state(msg.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;
|
msg.effects = &effects_list;
|
||||||
return fill_and_encode_entity_info(light, msg, ListEntitiesLightResponse::MESSAGE_TYPE, conn, remaining_size);
|
return fill_and_encode_entity_info(light, msg, ListEntitiesLightResponse::MESSAGE_TYPE, conn, remaining_size);
|
||||||
}
|
}
|
||||||
void APIConnection::light_command(const LightCommandRequest &msg) {
|
void APIConnection::on_light_command_request(const LightCommandRequest &msg) {
|
||||||
ENTITY_COMMAND_MAKE_CALL(light::LightState, light, light)
|
ENTITY_COMMAND_MAKE_CALL(light::LightState, light, light)
|
||||||
if (msg.has_state)
|
if (msg.has_state)
|
||||||
call.set_state(msg.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();
|
msg.device_class = a_switch->get_device_class_ref();
|
||||||
return fill_and_encode_entity_info(a_switch, msg, ListEntitiesSwitchResponse::MESSAGE_TYPE, conn, remaining_size);
|
return fill_and_encode_entity_info(a_switch, msg, ListEntitiesSwitchResponse::MESSAGE_TYPE, conn, remaining_size);
|
||||||
}
|
}
|
||||||
void APIConnection::switch_command(const SwitchCommandRequest &msg) {
|
void APIConnection::on_switch_command_request(const SwitchCommandRequest &msg) {
|
||||||
ENTITY_COMMAND_GET(switch_::Switch, a_switch, switch)
|
ENTITY_COMMAND_GET(switch_::Switch, a_switch, switch)
|
||||||
|
|
||||||
if (msg.state) {
|
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();
|
msg.supported_swing_modes = &traits.get_supported_swing_modes();
|
||||||
return fill_and_encode_entity_info(climate, msg, ListEntitiesClimateResponse::MESSAGE_TYPE, conn, remaining_size);
|
return fill_and_encode_entity_info(climate, msg, ListEntitiesClimateResponse::MESSAGE_TYPE, conn, remaining_size);
|
||||||
}
|
}
|
||||||
void APIConnection::climate_command(const ClimateCommandRequest &msg) {
|
void APIConnection::on_climate_command_request(const ClimateCommandRequest &msg) {
|
||||||
ENTITY_COMMAND_MAKE_CALL(climate::Climate, climate, climate)
|
ENTITY_COMMAND_MAKE_CALL(climate::Climate, climate, climate)
|
||||||
if (msg.has_mode)
|
if (msg.has_mode)
|
||||||
call.set_mode(static_cast<climate::ClimateMode>(msg.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();
|
msg.step = number->traits.get_step();
|
||||||
return fill_and_encode_entity_info(number, msg, ListEntitiesNumberResponse::MESSAGE_TYPE, conn, remaining_size);
|
return fill_and_encode_entity_info(number, msg, ListEntitiesNumberResponse::MESSAGE_TYPE, conn, remaining_size);
|
||||||
}
|
}
|
||||||
void APIConnection::number_command(const NumberCommandRequest &msg) {
|
void APIConnection::on_number_command_request(const NumberCommandRequest &msg) {
|
||||||
ENTITY_COMMAND_MAKE_CALL(number::Number, number, number)
|
ENTITY_COMMAND_MAKE_CALL(number::Number, number, number)
|
||||||
call.set_value(msg.state);
|
call.set_value(msg.state);
|
||||||
call.perform();
|
call.perform();
|
||||||
@@ -767,7 +767,7 @@ uint16_t APIConnection::try_send_date_info(EntityBase *entity, APIConnection *co
|
|||||||
ListEntitiesDateResponse msg;
|
ListEntitiesDateResponse msg;
|
||||||
return fill_and_encode_entity_info(date, msg, ListEntitiesDateResponse::MESSAGE_TYPE, conn, remaining_size);
|
return fill_and_encode_entity_info(date, msg, ListEntitiesDateResponse::MESSAGE_TYPE, conn, remaining_size);
|
||||||
}
|
}
|
||||||
void APIConnection::date_command(const DateCommandRequest &msg) {
|
void APIConnection::on_date_command_request(const DateCommandRequest &msg) {
|
||||||
ENTITY_COMMAND_MAKE_CALL(datetime::DateEntity, date, date)
|
ENTITY_COMMAND_MAKE_CALL(datetime::DateEntity, date, date)
|
||||||
call.set_date(msg.year, msg.month, msg.day);
|
call.set_date(msg.year, msg.month, msg.day);
|
||||||
call.perform();
|
call.perform();
|
||||||
@@ -792,7 +792,7 @@ uint16_t APIConnection::try_send_time_info(EntityBase *entity, APIConnection *co
|
|||||||
ListEntitiesTimeResponse msg;
|
ListEntitiesTimeResponse msg;
|
||||||
return fill_and_encode_entity_info(time, msg, ListEntitiesTimeResponse::MESSAGE_TYPE, conn, remaining_size);
|
return fill_and_encode_entity_info(time, msg, ListEntitiesTimeResponse::MESSAGE_TYPE, conn, remaining_size);
|
||||||
}
|
}
|
||||||
void APIConnection::time_command(const TimeCommandRequest &msg) {
|
void APIConnection::on_time_command_request(const TimeCommandRequest &msg) {
|
||||||
ENTITY_COMMAND_MAKE_CALL(datetime::TimeEntity, time, time)
|
ENTITY_COMMAND_MAKE_CALL(datetime::TimeEntity, time, time)
|
||||||
call.set_time(msg.hour, msg.minute, msg.second);
|
call.set_time(msg.hour, msg.minute, msg.second);
|
||||||
call.perform();
|
call.perform();
|
||||||
@@ -819,7 +819,7 @@ uint16_t APIConnection::try_send_datetime_info(EntityBase *entity, APIConnection
|
|||||||
ListEntitiesDateTimeResponse msg;
|
ListEntitiesDateTimeResponse msg;
|
||||||
return fill_and_encode_entity_info(datetime, msg, ListEntitiesDateTimeResponse::MESSAGE_TYPE, conn, remaining_size);
|
return fill_and_encode_entity_info(datetime, msg, ListEntitiesDateTimeResponse::MESSAGE_TYPE, conn, remaining_size);
|
||||||
}
|
}
|
||||||
void APIConnection::datetime_command(const DateTimeCommandRequest &msg) {
|
void APIConnection::on_date_time_command_request(const DateTimeCommandRequest &msg) {
|
||||||
ENTITY_COMMAND_MAKE_CALL(datetime::DateTimeEntity, datetime, datetime)
|
ENTITY_COMMAND_MAKE_CALL(datetime::DateTimeEntity, datetime, datetime)
|
||||||
call.set_datetime(msg.epoch_seconds);
|
call.set_datetime(msg.epoch_seconds);
|
||||||
call.perform();
|
call.perform();
|
||||||
@@ -848,7 +848,7 @@ uint16_t APIConnection::try_send_text_info(EntityBase *entity, APIConnection *co
|
|||||||
msg.pattern = text->traits.get_pattern_ref();
|
msg.pattern = text->traits.get_pattern_ref();
|
||||||
return fill_and_encode_entity_info(text, msg, ListEntitiesTextResponse::MESSAGE_TYPE, conn, remaining_size);
|
return fill_and_encode_entity_info(text, msg, ListEntitiesTextResponse::MESSAGE_TYPE, conn, remaining_size);
|
||||||
}
|
}
|
||||||
void APIConnection::text_command(const TextCommandRequest &msg) {
|
void APIConnection::on_text_command_request(const TextCommandRequest &msg) {
|
||||||
ENTITY_COMMAND_MAKE_CALL(text::Text, text, text)
|
ENTITY_COMMAND_MAKE_CALL(text::Text, text, text)
|
||||||
call.set_value(msg.state);
|
call.set_value(msg.state);
|
||||||
call.perform();
|
call.perform();
|
||||||
@@ -874,7 +874,7 @@ uint16_t APIConnection::try_send_select_info(EntityBase *entity, APIConnection *
|
|||||||
msg.options = &select->traits.get_options();
|
msg.options = &select->traits.get_options();
|
||||||
return fill_and_encode_entity_info(select, msg, ListEntitiesSelectResponse::MESSAGE_TYPE, conn, remaining_size);
|
return fill_and_encode_entity_info(select, msg, ListEntitiesSelectResponse::MESSAGE_TYPE, conn, remaining_size);
|
||||||
}
|
}
|
||||||
void APIConnection::select_command(const SelectCommandRequest &msg) {
|
void APIConnection::on_select_command_request(const SelectCommandRequest &msg) {
|
||||||
ENTITY_COMMAND_MAKE_CALL(select::Select, select, select)
|
ENTITY_COMMAND_MAKE_CALL(select::Select, select, select)
|
||||||
call.set_option(msg.state.c_str(), msg.state.size());
|
call.set_option(msg.state.c_str(), msg.state.size());
|
||||||
call.perform();
|
call.perform();
|
||||||
@@ -888,7 +888,7 @@ uint16_t APIConnection::try_send_button_info(EntityBase *entity, APIConnection *
|
|||||||
msg.device_class = button->get_device_class_ref();
|
msg.device_class = button->get_device_class_ref();
|
||||||
return fill_and_encode_entity_info(button, msg, ListEntitiesButtonResponse::MESSAGE_TYPE, conn, remaining_size);
|
return fill_and_encode_entity_info(button, msg, ListEntitiesButtonResponse::MESSAGE_TYPE, conn, remaining_size);
|
||||||
}
|
}
|
||||||
void esphome::api::APIConnection::button_command(const ButtonCommandRequest &msg) {
|
void esphome::api::APIConnection::on_button_command_request(const ButtonCommandRequest &msg) {
|
||||||
ENTITY_COMMAND_GET(button::Button, button, button)
|
ENTITY_COMMAND_GET(button::Button, button, button)
|
||||||
button->press();
|
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();
|
msg.requires_code = a_lock->traits.get_requires_code();
|
||||||
return fill_and_encode_entity_info(a_lock, msg, ListEntitiesLockResponse::MESSAGE_TYPE, conn, remaining_size);
|
return fill_and_encode_entity_info(a_lock, msg, ListEntitiesLockResponse::MESSAGE_TYPE, conn, remaining_size);
|
||||||
}
|
}
|
||||||
void APIConnection::lock_command(const LockCommandRequest &msg) {
|
void APIConnection::on_lock_command_request(const LockCommandRequest &msg) {
|
||||||
ENTITY_COMMAND_GET(lock::Lock, a_lock, lock)
|
ENTITY_COMMAND_GET(lock::Lock, a_lock, lock)
|
||||||
|
|
||||||
switch (msg.command) {
|
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();
|
msg.supports_stop = traits.get_supports_stop();
|
||||||
return fill_and_encode_entity_info(valve, msg, ListEntitiesValveResponse::MESSAGE_TYPE, conn, remaining_size);
|
return fill_and_encode_entity_info(valve, msg, ListEntitiesValveResponse::MESSAGE_TYPE, conn, remaining_size);
|
||||||
}
|
}
|
||||||
void APIConnection::valve_command(const ValveCommandRequest &msg) {
|
void APIConnection::on_valve_command_request(const ValveCommandRequest &msg) {
|
||||||
ENTITY_COMMAND_MAKE_CALL(valve::Valve, valve, valve)
|
ENTITY_COMMAND_MAKE_CALL(valve::Valve, valve, valve)
|
||||||
if (msg.has_position)
|
if (msg.has_position)
|
||||||
call.set_position(msg.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,
|
return fill_and_encode_entity_info(media_player, msg, ListEntitiesMediaPlayerResponse::MESSAGE_TYPE, conn,
|
||||||
remaining_size);
|
remaining_size);
|
||||||
}
|
}
|
||||||
void APIConnection::media_player_command(const MediaPlayerCommandRequest &msg) {
|
void APIConnection::on_media_player_command_request(const MediaPlayerCommandRequest &msg) {
|
||||||
ENTITY_COMMAND_MAKE_CALL(media_player::MediaPlayer, media_player, media_player)
|
ENTITY_COMMAND_MAKE_CALL(media_player::MediaPlayer, media_player, media_player)
|
||||||
if (msg.has_command) {
|
if (msg.has_command) {
|
||||||
call.set_command(static_cast<media_player::MediaPlayerCommand>(msg.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;
|
ListEntitiesCameraResponse msg;
|
||||||
return fill_and_encode_entity_info(camera, msg, ListEntitiesCameraResponse::MESSAGE_TYPE, conn, remaining_size);
|
return fill_and_encode_entity_info(camera, msg, ListEntitiesCameraResponse::MESSAGE_TYPE, conn, remaining_size);
|
||||||
}
|
}
|
||||||
void APIConnection::camera_image(const CameraImageRequest &msg) {
|
void APIConnection::on_camera_image_request(const CameraImageRequest &msg) {
|
||||||
if (camera::Camera::instance() == nullptr)
|
if (camera::Camera::instance() == nullptr)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
@@ -1092,42 +1092,47 @@ void APIConnection::on_get_time_response(const GetTimeResponse &value) {
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
#ifdef USE_BLUETOOTH_PROXY
|
#ifdef USE_BLUETOOTH_PROXY
|
||||||
void APIConnection::subscribe_bluetooth_le_advertisements(const SubscribeBluetoothLEAdvertisementsRequest &msg) {
|
void APIConnection::on_subscribe_bluetooth_le_advertisements_request(
|
||||||
|
const SubscribeBluetoothLEAdvertisementsRequest &msg) {
|
||||||
bluetooth_proxy::global_bluetooth_proxy->subscribe_api_connection(this, msg.flags);
|
bluetooth_proxy::global_bluetooth_proxy->subscribe_api_connection(this, msg.flags);
|
||||||
}
|
}
|
||||||
void APIConnection::unsubscribe_bluetooth_le_advertisements(const UnsubscribeBluetoothLEAdvertisementsRequest &msg) {
|
void APIConnection::on_unsubscribe_bluetooth_le_advertisements_request() {
|
||||||
bluetooth_proxy::global_bluetooth_proxy->unsubscribe_api_connection(this);
|
bluetooth_proxy::global_bluetooth_proxy->unsubscribe_api_connection(this);
|
||||||
}
|
}
|
||||||
void APIConnection::bluetooth_device_request(const BluetoothDeviceRequest &msg) {
|
void APIConnection::on_bluetooth_device_request(const BluetoothDeviceRequest &msg) {
|
||||||
bluetooth_proxy::global_bluetooth_proxy->bluetooth_device_request(msg);
|
bluetooth_proxy::global_bluetooth_proxy->bluetooth_device_request(msg);
|
||||||
}
|
}
|
||||||
void APIConnection::bluetooth_gatt_read(const BluetoothGATTReadRequest &msg) {
|
void APIConnection::on_bluetooth_gatt_read_request(const BluetoothGATTReadRequest &msg) {
|
||||||
bluetooth_proxy::global_bluetooth_proxy->bluetooth_gatt_read(msg);
|
bluetooth_proxy::global_bluetooth_proxy->bluetooth_gatt_read(msg);
|
||||||
}
|
}
|
||||||
void APIConnection::bluetooth_gatt_write(const BluetoothGATTWriteRequest &msg) {
|
void APIConnection::on_bluetooth_gatt_write_request(const BluetoothGATTWriteRequest &msg) {
|
||||||
bluetooth_proxy::global_bluetooth_proxy->bluetooth_gatt_write(msg);
|
bluetooth_proxy::global_bluetooth_proxy->bluetooth_gatt_write(msg);
|
||||||
}
|
}
|
||||||
void APIConnection::bluetooth_gatt_read_descriptor(const BluetoothGATTReadDescriptorRequest &msg) {
|
void APIConnection::on_bluetooth_gatt_read_descriptor_request(const BluetoothGATTReadDescriptorRequest &msg) {
|
||||||
bluetooth_proxy::global_bluetooth_proxy->bluetooth_gatt_read_descriptor(msg);
|
bluetooth_proxy::global_bluetooth_proxy->bluetooth_gatt_read_descriptor(msg);
|
||||||
}
|
}
|
||||||
void APIConnection::bluetooth_gatt_write_descriptor(const BluetoothGATTWriteDescriptorRequest &msg) {
|
void APIConnection::on_bluetooth_gatt_write_descriptor_request(const BluetoothGATTWriteDescriptorRequest &msg) {
|
||||||
bluetooth_proxy::global_bluetooth_proxy->bluetooth_gatt_write_descriptor(msg);
|
bluetooth_proxy::global_bluetooth_proxy->bluetooth_gatt_write_descriptor(msg);
|
||||||
}
|
}
|
||||||
void APIConnection::bluetooth_gatt_get_services(const BluetoothGATTGetServicesRequest &msg) {
|
void APIConnection::on_bluetooth_gatt_get_services_request(const BluetoothGATTGetServicesRequest &msg) {
|
||||||
bluetooth_proxy::global_bluetooth_proxy->bluetooth_gatt_send_services(msg);
|
bluetooth_proxy::global_bluetooth_proxy->bluetooth_gatt_send_services(msg);
|
||||||
}
|
}
|
||||||
|
|
||||||
void APIConnection::bluetooth_gatt_notify(const BluetoothGATTNotifyRequest &msg) {
|
void APIConnection::on_bluetooth_gatt_notify_request(const BluetoothGATTNotifyRequest &msg) {
|
||||||
bluetooth_proxy::global_bluetooth_proxy->bluetooth_gatt_notify(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);
|
bluetooth_proxy::global_bluetooth_proxy->send_connections_free(this);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
void APIConnection::on_subscribe_bluetooth_connections_free_request() {
|
||||||
|
if (!this->send_subscribe_bluetooth_connections_free_response_()) {
|
||||||
|
this->on_fatal_error();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void APIConnection::bluetooth_scanner_set_mode(const BluetoothScannerSetModeRequest &msg) {
|
void APIConnection::on_bluetooth_scanner_set_mode_request(const BluetoothScannerSetModeRequest &msg) {
|
||||||
bluetooth_proxy::global_bluetooth_proxy->bluetooth_scanner_set_mode(
|
bluetooth_proxy::global_bluetooth_proxy->bluetooth_scanner_set_mode(
|
||||||
msg.mode == enums::BluetoothScannerMode::BLUETOOTH_SCANNER_MODE_ACTIVE);
|
msg.mode == enums::BluetoothScannerMode::BLUETOOTH_SCANNER_MODE_ACTIVE);
|
||||||
}
|
}
|
||||||
@@ -1139,7 +1144,7 @@ bool APIConnection::check_voice_assistant_api_connection_() const {
|
|||||||
voice_assistant::global_voice_assistant->get_api_connection() == this;
|
voice_assistant::global_voice_assistant->get_api_connection() == this;
|
||||||
}
|
}
|
||||||
|
|
||||||
void APIConnection::subscribe_voice_assistant(const SubscribeVoiceAssistantRequest &msg) {
|
void APIConnection::on_subscribe_voice_assistant_request(const SubscribeVoiceAssistantRequest &msg) {
|
||||||
if (voice_assistant::global_voice_assistant != nullptr) {
|
if (voice_assistant::global_voice_assistant != nullptr) {
|
||||||
voice_assistant::global_voice_assistant->client_subscription(this, msg.subscribe);
|
voice_assistant::global_voice_assistant->client_subscription(this, msg.subscribe);
|
||||||
}
|
}
|
||||||
@@ -1185,7 +1190,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;
|
VoiceAssistantConfigurationResponse resp;
|
||||||
if (!this->check_voice_assistant_api_connection_()) {
|
if (!this->check_voice_assistant_api_connection_()) {
|
||||||
return this->send_message(resp, VoiceAssistantConfigurationResponse::MESSAGE_TYPE);
|
return this->send_message(resp, VoiceAssistantConfigurationResponse::MESSAGE_TYPE);
|
||||||
@@ -1222,8 +1227,13 @@ bool APIConnection::send_voice_assistant_get_configuration_response(const VoiceA
|
|||||||
resp.max_active_wake_words = config.max_active_wake_words;
|
resp.max_active_wake_words = config.max_active_wake_words;
|
||||||
return this->send_message(resp, VoiceAssistantConfigurationResponse::MESSAGE_TYPE);
|
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::voice_assistant_set_configuration(const VoiceAssistantSetConfiguration &msg) {
|
void APIConnection::on_voice_assistant_set_configuration(const VoiceAssistantSetConfiguration &msg) {
|
||||||
if (this->check_voice_assistant_api_connection_()) {
|
if (this->check_voice_assistant_api_connection_()) {
|
||||||
voice_assistant::global_voice_assistant->on_set_configuration(msg.active_wake_words);
|
voice_assistant::global_voice_assistant->on_set_configuration(msg.active_wake_words);
|
||||||
}
|
}
|
||||||
@@ -1231,11 +1241,11 @@ void APIConnection::voice_assistant_set_configuration(const VoiceAssistantSetCon
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
#ifdef USE_ZWAVE_PROXY
|
#ifdef USE_ZWAVE_PROXY
|
||||||
void APIConnection::zwave_proxy_frame(const ZWaveProxyFrame &msg) {
|
void APIConnection::on_z_wave_proxy_frame(const ZWaveProxyFrame &msg) {
|
||||||
zwave_proxy::global_zwave_proxy->send_frame(msg.data, msg.data_len);
|
zwave_proxy::global_zwave_proxy->send_frame(msg.data, msg.data_len);
|
||||||
}
|
}
|
||||||
|
|
||||||
void APIConnection::zwave_proxy_request(const ZWaveProxyRequest &msg) {
|
void APIConnection::on_z_wave_proxy_request(const ZWaveProxyRequest &msg) {
|
||||||
zwave_proxy::global_zwave_proxy->zwave_proxy_request(this, msg.type);
|
zwave_proxy::global_zwave_proxy->zwave_proxy_request(this, msg.type);
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
@@ -1263,7 +1273,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,
|
return fill_and_encode_entity_info(a_alarm_control_panel, msg, ListEntitiesAlarmControlPanelResponse::MESSAGE_TYPE,
|
||||||
conn, remaining_size);
|
conn, remaining_size);
|
||||||
}
|
}
|
||||||
void APIConnection::alarm_control_panel_command(const AlarmControlPanelCommandRequest &msg) {
|
void APIConnection::on_alarm_control_panel_command_request(const AlarmControlPanelCommandRequest &msg) {
|
||||||
ENTITY_COMMAND_MAKE_CALL(alarm_control_panel::AlarmControlPanel, a_alarm_control_panel, alarm_control_panel)
|
ENTITY_COMMAND_MAKE_CALL(alarm_control_panel::AlarmControlPanel, a_alarm_control_panel, alarm_control_panel)
|
||||||
switch (msg.command) {
|
switch (msg.command) {
|
||||||
case enums::ALARM_CONTROL_PANEL_DISARM:
|
case enums::ALARM_CONTROL_PANEL_DISARM:
|
||||||
@@ -1323,7 +1333,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);
|
return fill_and_encode_entity_info(wh, msg, ListEntitiesWaterHeaterResponse::MESSAGE_TYPE, conn, remaining_size);
|
||||||
}
|
}
|
||||||
|
|
||||||
void APIConnection::water_heater_command(const WaterHeaterCommandRequest &msg) {
|
void APIConnection::on_water_heater_command_request(const WaterHeaterCommandRequest &msg) {
|
||||||
ENTITY_COMMAND_MAKE_CALL(water_heater::WaterHeater, water_heater, water_heater)
|
ENTITY_COMMAND_MAKE_CALL(water_heater::WaterHeater, water_heater, water_heater)
|
||||||
if (msg.has_fields & enums::WATER_HEATER_COMMAND_HAS_MODE)
|
if (msg.has_fields & enums::WATER_HEATER_COMMAND_HAS_MODE)
|
||||||
call.set_mode(static_cast<water_heater::WaterHeaterMode>(msg.mode));
|
call.set_mode(static_cast<water_heater::WaterHeaterMode>(msg.mode));
|
||||||
@@ -1365,7 +1375,7 @@ uint16_t APIConnection::try_send_event_info(EntityBase *entity, APIConnection *c
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
#ifdef USE_IR_RF
|
#ifdef USE_IR_RF
|
||||||
void APIConnection::infrared_rf_transmit_raw_timings(const InfraredRFTransmitRawTimingsRequest &msg) {
|
void APIConnection::on_infrared_rf_transmit_raw_timings_request(const InfraredRFTransmitRawTimingsRequest &msg) {
|
||||||
// TODO: When RF is implemented, add a field to the message to distinguish IR vs RF
|
// 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.
|
// and dispatch to the appropriate entity type based on that field.
|
||||||
#ifdef USE_INFRARED
|
#ifdef USE_INFRARED
|
||||||
@@ -1419,7 +1429,7 @@ uint16_t APIConnection::try_send_update_info(EntityBase *entity, APIConnection *
|
|||||||
msg.device_class = update->get_device_class_ref();
|
msg.device_class = update->get_device_class_ref();
|
||||||
return fill_and_encode_entity_info(update, msg, ListEntitiesUpdateResponse::MESSAGE_TYPE, conn, remaining_size);
|
return fill_and_encode_entity_info(update, msg, ListEntitiesUpdateResponse::MESSAGE_TYPE, conn, remaining_size);
|
||||||
}
|
}
|
||||||
void APIConnection::update_command(const UpdateCommandRequest &msg) {
|
void APIConnection::on_update_command_request(const UpdateCommandRequest &msg) {
|
||||||
ENTITY_COMMAND_GET(update::UpdateEntity, update, update)
|
ENTITY_COMMAND_GET(update::UpdateEntity, update, update)
|
||||||
|
|
||||||
switch (msg.command) {
|
switch (msg.command) {
|
||||||
@@ -1470,7 +1480,7 @@ void APIConnection::complete_authentication_() {
|
|||||||
#endif
|
#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)
|
// 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->helper_->set_client_name(msg.client_info.c_str(), msg.client_info.size());
|
||||||
this->client_api_version_major_ = msg.api_version_major;
|
this->client_api_version_major_ = msg.api_version_major;
|
||||||
@@ -1491,12 +1501,12 @@ bool APIConnection::send_hello_response(const HelloRequest &msg) {
|
|||||||
return this->send_message(resp, HelloResponse::MESSAGE_TYPE);
|
return this->send_message(resp, HelloResponse::MESSAGE_TYPE);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool APIConnection::send_ping_response(const PingRequest &msg) {
|
bool APIConnection::send_ping_response_() {
|
||||||
PingResponse resp;
|
PingResponse resp;
|
||||||
return this->send_message(resp, PingResponse::MESSAGE_TYPE);
|
return this->send_message(resp, PingResponse::MESSAGE_TYPE);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool APIConnection::send_device_info_response(const DeviceInfoRequest &msg) {
|
bool APIConnection::send_device_info_response_() {
|
||||||
DeviceInfoResponse resp{};
|
DeviceInfoResponse resp{};
|
||||||
resp.name = StringRef(App.get_name());
|
resp.name = StringRef(App.get_name());
|
||||||
resp.friendly_name = StringRef(App.get_friendly_name());
|
resp.friendly_name = StringRef(App.get_friendly_name());
|
||||||
@@ -1619,6 +1629,26 @@ bool APIConnection::send_device_info_response(const DeviceInfoRequest &msg) {
|
|||||||
|
|
||||||
return this->send_message(resp, DeviceInfoResponse::MESSAGE_TYPE);
|
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
|
#ifdef USE_API_HOMEASSISTANT_STATES
|
||||||
void APIConnection::on_home_assistant_state_response(const HomeAssistantStateResponse &msg) {
|
void APIConnection::on_home_assistant_state_response(const HomeAssistantStateResponse &msg) {
|
||||||
@@ -1657,7 +1687,7 @@ void APIConnection::on_home_assistant_state_response(const HomeAssistantStateRes
|
|||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
#ifdef USE_API_USER_DEFINED_ACTIONS
|
#ifdef USE_API_USER_DEFINED_ACTIONS
|
||||||
void APIConnection::execute_service(const ExecuteServiceRequest &msg) {
|
void APIConnection::on_execute_service_request(const ExecuteServiceRequest &msg) {
|
||||||
bool found = false;
|
bool found = false;
|
||||||
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES
|
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES
|
||||||
// Register the call and get a unique server-generated action_call_id
|
// Register the call and get a unique server-generated action_call_id
|
||||||
@@ -1723,7 +1753,7 @@ void APIConnection::on_homeassistant_action_response(const HomeassistantActionRe
|
|||||||
};
|
};
|
||||||
#endif
|
#endif
|
||||||
#ifdef USE_API_NOISE
|
#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;
|
NoiseEncryptionSetKeyResponse resp;
|
||||||
resp.success = false;
|
resp.success = false;
|
||||||
|
|
||||||
@@ -1744,11 +1774,14 @@ bool APIConnection::send_noise_encryption_set_key_response(const NoiseEncryption
|
|||||||
|
|
||||||
return this->send_message(resp, NoiseEncryptionSetKeyResponse::MESSAGE_TYPE);
|
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
|
#endif
|
||||||
#ifdef USE_API_HOMEASSISTANT_STATES
|
#ifdef USE_API_HOMEASSISTANT_STATES
|
||||||
void APIConnection::subscribe_home_assistant_states(const SubscribeHomeAssistantStatesRequest &msg) {
|
void APIConnection::on_subscribe_home_assistant_states_request() { state_subs_at_ = 0; }
|
||||||
state_subs_at_ = 0;
|
|
||||||
}
|
|
||||||
#endif
|
#endif
|
||||||
bool APIConnection::try_to_clear_buffer(bool log_out_of_space) {
|
bool APIConnection::try_to_clear_buffer(bool log_out_of_space) {
|
||||||
if (this->flags_.remove)
|
if (this->flags_.remove)
|
||||||
|
|||||||
@@ -47,72 +47,72 @@ class APIConnection final : public APIServerConnection {
|
|||||||
#endif
|
#endif
|
||||||
#ifdef USE_COVER
|
#ifdef USE_COVER
|
||||||
bool send_cover_state(cover::Cover *cover);
|
bool send_cover_state(cover::Cover *cover);
|
||||||
void cover_command(const CoverCommandRequest &msg) override;
|
void on_cover_command_request(const CoverCommandRequest &msg) override;
|
||||||
#endif
|
#endif
|
||||||
#ifdef USE_FAN
|
#ifdef USE_FAN
|
||||||
bool send_fan_state(fan::Fan *fan);
|
bool send_fan_state(fan::Fan *fan);
|
||||||
void fan_command(const FanCommandRequest &msg) override;
|
void on_fan_command_request(const FanCommandRequest &msg) override;
|
||||||
#endif
|
#endif
|
||||||
#ifdef USE_LIGHT
|
#ifdef USE_LIGHT
|
||||||
bool send_light_state(light::LightState *light);
|
bool send_light_state(light::LightState *light);
|
||||||
void light_command(const LightCommandRequest &msg) override;
|
void on_light_command_request(const LightCommandRequest &msg) override;
|
||||||
#endif
|
#endif
|
||||||
#ifdef USE_SENSOR
|
#ifdef USE_SENSOR
|
||||||
bool send_sensor_state(sensor::Sensor *sensor);
|
bool send_sensor_state(sensor::Sensor *sensor);
|
||||||
#endif
|
#endif
|
||||||
#ifdef USE_SWITCH
|
#ifdef USE_SWITCH
|
||||||
bool send_switch_state(switch_::Switch *a_switch);
|
bool send_switch_state(switch_::Switch *a_switch);
|
||||||
void switch_command(const SwitchCommandRequest &msg) override;
|
void on_switch_command_request(const SwitchCommandRequest &msg) override;
|
||||||
#endif
|
#endif
|
||||||
#ifdef USE_TEXT_SENSOR
|
#ifdef USE_TEXT_SENSOR
|
||||||
bool send_text_sensor_state(text_sensor::TextSensor *text_sensor);
|
bool send_text_sensor_state(text_sensor::TextSensor *text_sensor);
|
||||||
#endif
|
#endif
|
||||||
#ifdef USE_CAMERA
|
#ifdef USE_CAMERA
|
||||||
void set_camera_state(std::shared_ptr<camera::CameraImage> image);
|
void set_camera_state(std::shared_ptr<camera::CameraImage> image);
|
||||||
void camera_image(const CameraImageRequest &msg) override;
|
void on_camera_image_request(const CameraImageRequest &msg) override;
|
||||||
#endif
|
#endif
|
||||||
#ifdef USE_CLIMATE
|
#ifdef USE_CLIMATE
|
||||||
bool send_climate_state(climate::Climate *climate);
|
bool send_climate_state(climate::Climate *climate);
|
||||||
void climate_command(const ClimateCommandRequest &msg) override;
|
void on_climate_command_request(const ClimateCommandRequest &msg) override;
|
||||||
#endif
|
#endif
|
||||||
#ifdef USE_NUMBER
|
#ifdef USE_NUMBER
|
||||||
bool send_number_state(number::Number *number);
|
bool send_number_state(number::Number *number);
|
||||||
void number_command(const NumberCommandRequest &msg) override;
|
void on_number_command_request(const NumberCommandRequest &msg) override;
|
||||||
#endif
|
#endif
|
||||||
#ifdef USE_DATETIME_DATE
|
#ifdef USE_DATETIME_DATE
|
||||||
bool send_date_state(datetime::DateEntity *date);
|
bool send_date_state(datetime::DateEntity *date);
|
||||||
void date_command(const DateCommandRequest &msg) override;
|
void on_date_command_request(const DateCommandRequest &msg) override;
|
||||||
#endif
|
#endif
|
||||||
#ifdef USE_DATETIME_TIME
|
#ifdef USE_DATETIME_TIME
|
||||||
bool send_time_state(datetime::TimeEntity *time);
|
bool send_time_state(datetime::TimeEntity *time);
|
||||||
void time_command(const TimeCommandRequest &msg) override;
|
void on_time_command_request(const TimeCommandRequest &msg) override;
|
||||||
#endif
|
#endif
|
||||||
#ifdef USE_DATETIME_DATETIME
|
#ifdef USE_DATETIME_DATETIME
|
||||||
bool send_datetime_state(datetime::DateTimeEntity *datetime);
|
bool send_datetime_state(datetime::DateTimeEntity *datetime);
|
||||||
void datetime_command(const DateTimeCommandRequest &msg) override;
|
void on_date_time_command_request(const DateTimeCommandRequest &msg) override;
|
||||||
#endif
|
#endif
|
||||||
#ifdef USE_TEXT
|
#ifdef USE_TEXT
|
||||||
bool send_text_state(text::Text *text);
|
bool send_text_state(text::Text *text);
|
||||||
void text_command(const TextCommandRequest &msg) override;
|
void on_text_command_request(const TextCommandRequest &msg) override;
|
||||||
#endif
|
#endif
|
||||||
#ifdef USE_SELECT
|
#ifdef USE_SELECT
|
||||||
bool send_select_state(select::Select *select);
|
bool send_select_state(select::Select *select);
|
||||||
void select_command(const SelectCommandRequest &msg) override;
|
void on_select_command_request(const SelectCommandRequest &msg) override;
|
||||||
#endif
|
#endif
|
||||||
#ifdef USE_BUTTON
|
#ifdef USE_BUTTON
|
||||||
void button_command(const ButtonCommandRequest &msg) override;
|
void on_button_command_request(const ButtonCommandRequest &msg) override;
|
||||||
#endif
|
#endif
|
||||||
#ifdef USE_LOCK
|
#ifdef USE_LOCK
|
||||||
bool send_lock_state(lock::Lock *a_lock);
|
bool send_lock_state(lock::Lock *a_lock);
|
||||||
void lock_command(const LockCommandRequest &msg) override;
|
void on_lock_command_request(const LockCommandRequest &msg) override;
|
||||||
#endif
|
#endif
|
||||||
#ifdef USE_VALVE
|
#ifdef USE_VALVE
|
||||||
bool send_valve_state(valve::Valve *valve);
|
bool send_valve_state(valve::Valve *valve);
|
||||||
void valve_command(const ValveCommandRequest &msg) override;
|
void on_valve_command_request(const ValveCommandRequest &msg) override;
|
||||||
#endif
|
#endif
|
||||||
#ifdef USE_MEDIA_PLAYER
|
#ifdef USE_MEDIA_PLAYER
|
||||||
bool send_media_player_state(media_player::MediaPlayer *media_player);
|
bool send_media_player_state(media_player::MediaPlayer *media_player);
|
||||||
void media_player_command(const MediaPlayerCommandRequest &msg) override;
|
void on_media_player_command_request(const MediaPlayerCommandRequest &msg) override;
|
||||||
#endif
|
#endif
|
||||||
bool try_send_log_message(int level, const char *tag, const char *line, size_t message_len);
|
bool try_send_log_message(int level, const char *tag, const char *line, size_t message_len);
|
||||||
#ifdef USE_API_HOMEASSISTANT_SERVICES
|
#ifdef USE_API_HOMEASSISTANT_SERVICES
|
||||||
@@ -126,18 +126,18 @@ class APIConnection final : public APIServerConnection {
|
|||||||
#endif // USE_API_HOMEASSISTANT_ACTION_RESPONSES
|
#endif // USE_API_HOMEASSISTANT_ACTION_RESPONSES
|
||||||
#endif // USE_API_HOMEASSISTANT_SERVICES
|
#endif // USE_API_HOMEASSISTANT_SERVICES
|
||||||
#ifdef USE_BLUETOOTH_PROXY
|
#ifdef USE_BLUETOOTH_PROXY
|
||||||
void subscribe_bluetooth_le_advertisements(const SubscribeBluetoothLEAdvertisementsRequest &msg) override;
|
void on_subscribe_bluetooth_le_advertisements_request(const SubscribeBluetoothLEAdvertisementsRequest &msg) override;
|
||||||
void unsubscribe_bluetooth_le_advertisements(const UnsubscribeBluetoothLEAdvertisementsRequest &msg) override;
|
void on_unsubscribe_bluetooth_le_advertisements_request() override;
|
||||||
|
|
||||||
void bluetooth_device_request(const BluetoothDeviceRequest &msg) override;
|
void on_bluetooth_device_request(const BluetoothDeviceRequest &msg) override;
|
||||||
void bluetooth_gatt_read(const BluetoothGATTReadRequest &msg) override;
|
void on_bluetooth_gatt_read_request(const BluetoothGATTReadRequest &msg) override;
|
||||||
void bluetooth_gatt_write(const BluetoothGATTWriteRequest &msg) override;
|
void on_bluetooth_gatt_write_request(const BluetoothGATTWriteRequest &msg) override;
|
||||||
void bluetooth_gatt_read_descriptor(const BluetoothGATTReadDescriptorRequest &msg) override;
|
void on_bluetooth_gatt_read_descriptor_request(const BluetoothGATTReadDescriptorRequest &msg) override;
|
||||||
void bluetooth_gatt_write_descriptor(const BluetoothGATTWriteDescriptorRequest &msg) override;
|
void on_bluetooth_gatt_write_descriptor_request(const BluetoothGATTWriteDescriptorRequest &msg) override;
|
||||||
void bluetooth_gatt_get_services(const BluetoothGATTGetServicesRequest &msg) override;
|
void on_bluetooth_gatt_get_services_request(const BluetoothGATTGetServicesRequest &msg) override;
|
||||||
void bluetooth_gatt_notify(const BluetoothGATTNotifyRequest &msg) override;
|
void on_bluetooth_gatt_notify_request(const BluetoothGATTNotifyRequest &msg) override;
|
||||||
bool send_subscribe_bluetooth_connections_free_response(const SubscribeBluetoothConnectionsFreeRequest &msg) override;
|
void on_subscribe_bluetooth_connections_free_request() override;
|
||||||
void bluetooth_scanner_set_mode(const BluetoothScannerSetModeRequest &msg) override;
|
void on_bluetooth_scanner_set_mode_request(const BluetoothScannerSetModeRequest &msg) override;
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
#ifdef USE_HOMEASSISTANT_TIME
|
#ifdef USE_HOMEASSISTANT_TIME
|
||||||
@@ -148,33 +148,33 @@ class APIConnection final : public APIServerConnection {
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
#ifdef USE_VOICE_ASSISTANT
|
#ifdef USE_VOICE_ASSISTANT
|
||||||
void subscribe_voice_assistant(const SubscribeVoiceAssistantRequest &msg) override;
|
void on_subscribe_voice_assistant_request(const SubscribeVoiceAssistantRequest &msg) override;
|
||||||
void on_voice_assistant_response(const VoiceAssistantResponse &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_event_response(const VoiceAssistantEventResponse &msg) override;
|
||||||
void on_voice_assistant_audio(const VoiceAssistantAudio &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_timer_event_response(const VoiceAssistantTimerEventResponse &msg) override;
|
||||||
void on_voice_assistant_announce_request(const VoiceAssistantAnnounceRequest &msg) override;
|
void on_voice_assistant_announce_request(const VoiceAssistantAnnounceRequest &msg) override;
|
||||||
bool send_voice_assistant_get_configuration_response(const VoiceAssistantConfigurationRequest &msg) override;
|
void on_voice_assistant_configuration_request(const VoiceAssistantConfigurationRequest &msg) override;
|
||||||
void voice_assistant_set_configuration(const VoiceAssistantSetConfiguration &msg) override;
|
void on_voice_assistant_set_configuration(const VoiceAssistantSetConfiguration &msg) override;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#ifdef USE_ZWAVE_PROXY
|
#ifdef USE_ZWAVE_PROXY
|
||||||
void zwave_proxy_frame(const ZWaveProxyFrame &msg) override;
|
void on_z_wave_proxy_frame(const ZWaveProxyFrame &msg) override;
|
||||||
void zwave_proxy_request(const ZWaveProxyRequest &msg) override;
|
void on_z_wave_proxy_request(const ZWaveProxyRequest &msg) override;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#ifdef USE_ALARM_CONTROL_PANEL
|
#ifdef USE_ALARM_CONTROL_PANEL
|
||||||
bool send_alarm_control_panel_state(alarm_control_panel::AlarmControlPanel *a_alarm_control_panel);
|
bool send_alarm_control_panel_state(alarm_control_panel::AlarmControlPanel *a_alarm_control_panel);
|
||||||
void alarm_control_panel_command(const AlarmControlPanelCommandRequest &msg) override;
|
void on_alarm_control_panel_command_request(const AlarmControlPanelCommandRequest &msg) override;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#ifdef USE_WATER_HEATER
|
#ifdef USE_WATER_HEATER
|
||||||
bool send_water_heater_state(water_heater::WaterHeater *water_heater);
|
bool send_water_heater_state(water_heater::WaterHeater *water_heater);
|
||||||
void water_heater_command(const WaterHeaterCommandRequest &msg) override;
|
void on_water_heater_command_request(const WaterHeaterCommandRequest &msg) override;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#ifdef USE_IR_RF
|
#ifdef USE_IR_RF
|
||||||
void infrared_rf_transmit_raw_timings(const InfraredRFTransmitRawTimingsRequest &msg) override;
|
void on_infrared_rf_transmit_raw_timings_request(const InfraredRFTransmitRawTimingsRequest &msg) override;
|
||||||
void send_infrared_rf_receive_event(const InfraredRFReceiveEvent &msg);
|
void send_infrared_rf_receive_event(const InfraredRFReceiveEvent &msg);
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
@@ -184,11 +184,11 @@ class APIConnection final : public APIServerConnection {
|
|||||||
|
|
||||||
#ifdef USE_UPDATE
|
#ifdef USE_UPDATE
|
||||||
bool send_update_state(update::UpdateEntity *update);
|
bool send_update_state(update::UpdateEntity *update);
|
||||||
void update_command(const UpdateCommandRequest &msg) override;
|
void on_update_command_request(const UpdateCommandRequest &msg) override;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
void on_disconnect_response(const DisconnectResponse &value) override;
|
void on_disconnect_response() override;
|
||||||
void on_ping_response(const PingResponse &value) override {
|
void on_ping_response() override {
|
||||||
// we initiated ping
|
// we initiated ping
|
||||||
this->flags_.sent_ping = false;
|
this->flags_.sent_ping = false;
|
||||||
}
|
}
|
||||||
@@ -198,12 +198,12 @@ class APIConnection final : public APIServerConnection {
|
|||||||
#ifdef USE_HOMEASSISTANT_TIME
|
#ifdef USE_HOMEASSISTANT_TIME
|
||||||
void on_get_time_response(const GetTimeResponse &value) override;
|
void on_get_time_response(const GetTimeResponse &value) override;
|
||||||
#endif
|
#endif
|
||||||
bool send_hello_response(const HelloRequest &msg) override;
|
void on_hello_request(const HelloRequest &msg) override;
|
||||||
bool send_disconnect_response(const DisconnectRequest &msg) override;
|
void on_disconnect_request() override;
|
||||||
bool send_ping_response(const PingRequest &msg) override;
|
void on_ping_request() override;
|
||||||
bool send_device_info_response(const DeviceInfoRequest &msg) override;
|
void on_device_info_request() override;
|
||||||
void list_entities(const ListEntitiesRequest &msg) override { this->begin_iterator_(ActiveIterator::LIST_ENTITIES); }
|
void on_list_entities_request() override { this->begin_iterator_(ActiveIterator::LIST_ENTITIES); }
|
||||||
void subscribe_states(const SubscribeStatesRequest &msg) override {
|
void on_subscribe_states_request() override {
|
||||||
this->flags_.state_subscription = true;
|
this->flags_.state_subscription = true;
|
||||||
// Start initial state iterator only if no iterator is active
|
// Start initial state iterator only if no iterator is active
|
||||||
// If list_entities is running, we'll start initial_state when it completes
|
// If list_entities is running, we'll start initial_state when it completes
|
||||||
@@ -211,21 +211,19 @@ class APIConnection final : public APIServerConnection {
|
|||||||
this->begin_iterator_(ActiveIterator::INITIAL_STATE);
|
this->begin_iterator_(ActiveIterator::INITIAL_STATE);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
void subscribe_logs(const SubscribeLogsRequest &msg) override {
|
void on_subscribe_logs_request(const SubscribeLogsRequest &msg) override {
|
||||||
this->flags_.log_subscription = msg.level;
|
this->flags_.log_subscription = msg.level;
|
||||||
if (msg.dump_config)
|
if (msg.dump_config)
|
||||||
App.schedule_dump_config();
|
App.schedule_dump_config();
|
||||||
}
|
}
|
||||||
#ifdef USE_API_HOMEASSISTANT_SERVICES
|
#ifdef USE_API_HOMEASSISTANT_SERVICES
|
||||||
void subscribe_homeassistant_services(const SubscribeHomeassistantServicesRequest &msg) override {
|
void on_subscribe_homeassistant_services_request() override { this->flags_.service_call_subscription = true; }
|
||||||
this->flags_.service_call_subscription = true;
|
|
||||||
}
|
|
||||||
#endif
|
#endif
|
||||||
#ifdef USE_API_HOMEASSISTANT_STATES
|
#ifdef USE_API_HOMEASSISTANT_STATES
|
||||||
void subscribe_home_assistant_states(const SubscribeHomeAssistantStatesRequest &msg) override;
|
void on_subscribe_home_assistant_states_request() override;
|
||||||
#endif
|
#endif
|
||||||
#ifdef USE_API_USER_DEFINED_ACTIONS
|
#ifdef USE_API_USER_DEFINED_ACTIONS
|
||||||
void execute_service(const ExecuteServiceRequest &msg) override;
|
void on_execute_service_request(const ExecuteServiceRequest &msg) override;
|
||||||
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES
|
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES
|
||||||
void send_execute_service_response(uint32_t call_id, bool success, StringRef error_message);
|
void send_execute_service_response(uint32_t call_id, bool success, StringRef error_message);
|
||||||
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES_JSON
|
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES_JSON
|
||||||
@@ -235,7 +233,7 @@ class APIConnection final : public APIServerConnection {
|
|||||||
#endif // USE_API_USER_DEFINED_ACTION_RESPONSES
|
#endif // USE_API_USER_DEFINED_ACTION_RESPONSES
|
||||||
#endif
|
#endif
|
||||||
#ifdef USE_API_NOISE
|
#ifdef USE_API_NOISE
|
||||||
bool send_noise_encryption_set_key_response(const NoiseEncryptionSetKeyRequest &msg) override;
|
void on_noise_encryption_set_key_request(const NoiseEncryptionSetKeyRequest &msg) override;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
bool is_authenticated() override {
|
bool is_authenticated() override {
|
||||||
@@ -285,6 +283,21 @@ class APIConnection final : public APIServerConnection {
|
|||||||
// Helper function to handle authentication completion
|
// Helper function to handle authentication completion
|
||||||
void complete_authentication_();
|
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
|
#ifdef USE_CAMERA
|
||||||
void try_send_camera_image_();
|
void try_send_camera_image_();
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -15,6 +15,9 @@ void APIServerConnectionBase::log_receive_message_(const LogString *name, const
|
|||||||
DumpBuffer dump_buf;
|
DumpBuffer dump_buf;
|
||||||
ESP_LOGVV(TAG, "%s: %s", LOG_STR_ARG(name), msg.dump_to(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
|
#endif
|
||||||
|
|
||||||
void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, const uint8_t *msg_data) {
|
void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, const uint8_t *msg_data) {
|
||||||
@@ -29,66 +32,52 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case DisconnectRequest::MESSAGE_TYPE: {
|
case DisconnectRequest::MESSAGE_TYPE: {
|
||||||
DisconnectRequest msg;
|
|
||||||
// Empty message: no decode needed
|
|
||||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||||
this->log_receive_message_(LOG_STR("on_disconnect_request"), msg);
|
this->log_receive_message_(LOG_STR("on_disconnect_request"));
|
||||||
#endif
|
#endif
|
||||||
this->on_disconnect_request(msg);
|
this->on_disconnect_request();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case DisconnectResponse::MESSAGE_TYPE: {
|
case DisconnectResponse::MESSAGE_TYPE: {
|
||||||
DisconnectResponse msg;
|
|
||||||
// Empty message: no decode needed
|
|
||||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||||
this->log_receive_message_(LOG_STR("on_disconnect_response"), msg);
|
this->log_receive_message_(LOG_STR("on_disconnect_response"));
|
||||||
#endif
|
#endif
|
||||||
this->on_disconnect_response(msg);
|
this->on_disconnect_response();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case PingRequest::MESSAGE_TYPE: {
|
case PingRequest::MESSAGE_TYPE: {
|
||||||
PingRequest msg;
|
|
||||||
// Empty message: no decode needed
|
|
||||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||||
this->log_receive_message_(LOG_STR("on_ping_request"), msg);
|
this->log_receive_message_(LOG_STR("on_ping_request"));
|
||||||
#endif
|
#endif
|
||||||
this->on_ping_request(msg);
|
this->on_ping_request();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case PingResponse::MESSAGE_TYPE: {
|
case PingResponse::MESSAGE_TYPE: {
|
||||||
PingResponse msg;
|
|
||||||
// Empty message: no decode needed
|
|
||||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||||
this->log_receive_message_(LOG_STR("on_ping_response"), msg);
|
this->log_receive_message_(LOG_STR("on_ping_response"));
|
||||||
#endif
|
#endif
|
||||||
this->on_ping_response(msg);
|
this->on_ping_response();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case DeviceInfoRequest::MESSAGE_TYPE: {
|
case DeviceInfoRequest::MESSAGE_TYPE: {
|
||||||
DeviceInfoRequest msg;
|
|
||||||
// Empty message: no decode needed
|
|
||||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||||
this->log_receive_message_(LOG_STR("on_device_info_request"), msg);
|
this->log_receive_message_(LOG_STR("on_device_info_request"));
|
||||||
#endif
|
#endif
|
||||||
this->on_device_info_request(msg);
|
this->on_device_info_request();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case ListEntitiesRequest::MESSAGE_TYPE: {
|
case ListEntitiesRequest::MESSAGE_TYPE: {
|
||||||
ListEntitiesRequest msg;
|
|
||||||
// Empty message: no decode needed
|
|
||||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||||
this->log_receive_message_(LOG_STR("on_list_entities_request"), msg);
|
this->log_receive_message_(LOG_STR("on_list_entities_request"));
|
||||||
#endif
|
#endif
|
||||||
this->on_list_entities_request(msg);
|
this->on_list_entities_request();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case SubscribeStatesRequest::MESSAGE_TYPE: {
|
case SubscribeStatesRequest::MESSAGE_TYPE: {
|
||||||
SubscribeStatesRequest msg;
|
|
||||||
// Empty message: no decode needed
|
|
||||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||||
this->log_receive_message_(LOG_STR("on_subscribe_states_request"), msg);
|
this->log_receive_message_(LOG_STR("on_subscribe_states_request"));
|
||||||
#endif
|
#endif
|
||||||
this->on_subscribe_states_request(msg);
|
this->on_subscribe_states_request();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case SubscribeLogsRequest::MESSAGE_TYPE: {
|
case SubscribeLogsRequest::MESSAGE_TYPE: {
|
||||||
@@ -146,12 +135,10 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
|
|||||||
#endif
|
#endif
|
||||||
#ifdef USE_API_HOMEASSISTANT_SERVICES
|
#ifdef USE_API_HOMEASSISTANT_SERVICES
|
||||||
case SubscribeHomeassistantServicesRequest::MESSAGE_TYPE: {
|
case SubscribeHomeassistantServicesRequest::MESSAGE_TYPE: {
|
||||||
SubscribeHomeassistantServicesRequest msg;
|
|
||||||
// Empty message: no decode needed
|
|
||||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||||
this->log_receive_message_(LOG_STR("on_subscribe_homeassistant_services_request"), msg);
|
this->log_receive_message_(LOG_STR("on_subscribe_homeassistant_services_request"));
|
||||||
#endif
|
#endif
|
||||||
this->on_subscribe_homeassistant_services_request(msg);
|
this->on_subscribe_homeassistant_services_request();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
@@ -166,12 +153,10 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
|
|||||||
}
|
}
|
||||||
#ifdef USE_API_HOMEASSISTANT_STATES
|
#ifdef USE_API_HOMEASSISTANT_STATES
|
||||||
case SubscribeHomeAssistantStatesRequest::MESSAGE_TYPE: {
|
case SubscribeHomeAssistantStatesRequest::MESSAGE_TYPE: {
|
||||||
SubscribeHomeAssistantStatesRequest msg;
|
|
||||||
// Empty message: no decode needed
|
|
||||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||||
this->log_receive_message_(LOG_STR("on_subscribe_home_assistant_states_request"), msg);
|
this->log_receive_message_(LOG_STR("on_subscribe_home_assistant_states_request"));
|
||||||
#endif
|
#endif
|
||||||
this->on_subscribe_home_assistant_states_request(msg);
|
this->on_subscribe_home_assistant_states_request();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
@@ -375,23 +360,19 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
|
|||||||
#endif
|
#endif
|
||||||
#ifdef USE_BLUETOOTH_PROXY
|
#ifdef USE_BLUETOOTH_PROXY
|
||||||
case SubscribeBluetoothConnectionsFreeRequest::MESSAGE_TYPE: {
|
case SubscribeBluetoothConnectionsFreeRequest::MESSAGE_TYPE: {
|
||||||
SubscribeBluetoothConnectionsFreeRequest msg;
|
|
||||||
// Empty message: no decode needed
|
|
||||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||||
this->log_receive_message_(LOG_STR("on_subscribe_bluetooth_connections_free_request"), msg);
|
this->log_receive_message_(LOG_STR("on_subscribe_bluetooth_connections_free_request"));
|
||||||
#endif
|
#endif
|
||||||
this->on_subscribe_bluetooth_connections_free_request(msg);
|
this->on_subscribe_bluetooth_connections_free_request();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
#ifdef USE_BLUETOOTH_PROXY
|
#ifdef USE_BLUETOOTH_PROXY
|
||||||
case UnsubscribeBluetoothLEAdvertisementsRequest::MESSAGE_TYPE: {
|
case UnsubscribeBluetoothLEAdvertisementsRequest::MESSAGE_TYPE: {
|
||||||
UnsubscribeBluetoothLEAdvertisementsRequest msg;
|
|
||||||
// Empty message: no decode needed
|
|
||||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||||
this->log_receive_message_(LOG_STR("on_unsubscribe_bluetooth_le_advertisements_request"), msg);
|
this->log_receive_message_(LOG_STR("on_unsubscribe_bluetooth_le_advertisements_request"));
|
||||||
#endif
|
#endif
|
||||||
this->on_unsubscribe_bluetooth_le_advertisements_request(msg);
|
this->on_unsubscribe_bluetooth_le_advertisements_request();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
@@ -642,209 +623,6 @@ 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) {
|
void APIServerConnection::read_message(uint32_t msg_size, uint32_t msg_type, const uint8_t *msg_data) {
|
||||||
// Check authentication/connection requirements for messages
|
// Check authentication/connection requirements for messages
|
||||||
switch (msg_type) {
|
switch (msg_type) {
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ class APIServerConnectionBase : public ProtoService {
|
|||||||
protected:
|
protected:
|
||||||
void log_send_message_(const char *name, const char *dump);
|
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, const ProtoMessage &msg);
|
||||||
|
void log_receive_message_(const LogString *name);
|
||||||
|
|
||||||
public:
|
public:
|
||||||
#endif
|
#endif
|
||||||
@@ -28,15 +29,15 @@ class APIServerConnectionBase : public ProtoService {
|
|||||||
|
|
||||||
virtual void on_hello_request(const HelloRequest &value){};
|
virtual void on_hello_request(const HelloRequest &value){};
|
||||||
|
|
||||||
virtual void on_disconnect_request(const DisconnectRequest &value){};
|
virtual void on_disconnect_request(){};
|
||||||
virtual void on_disconnect_response(const DisconnectResponse &value){};
|
virtual void on_disconnect_response(){};
|
||||||
virtual void on_ping_request(const PingRequest &value){};
|
virtual void on_ping_request(){};
|
||||||
virtual void on_ping_response(const PingResponse &value){};
|
virtual void on_ping_response(){};
|
||||||
virtual void on_device_info_request(const DeviceInfoRequest &value){};
|
virtual void on_device_info_request(){};
|
||||||
|
|
||||||
virtual void on_list_entities_request(const ListEntitiesRequest &value){};
|
virtual void on_list_entities_request(){};
|
||||||
|
|
||||||
virtual void on_subscribe_states_request(const SubscribeStatesRequest &value){};
|
virtual void on_subscribe_states_request(){};
|
||||||
|
|
||||||
#ifdef USE_COVER
|
#ifdef USE_COVER
|
||||||
virtual void on_cover_command_request(const CoverCommandRequest &value){};
|
virtual void on_cover_command_request(const CoverCommandRequest &value){};
|
||||||
@@ -61,14 +62,14 @@ class APIServerConnectionBase : public ProtoService {
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
#ifdef USE_API_HOMEASSISTANT_SERVICES
|
#ifdef USE_API_HOMEASSISTANT_SERVICES
|
||||||
virtual void on_subscribe_homeassistant_services_request(const SubscribeHomeassistantServicesRequest &value){};
|
virtual void on_subscribe_homeassistant_services_request(){};
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
|
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
|
||||||
virtual void on_homeassistant_action_response(const HomeassistantActionResponse &value){};
|
virtual void on_homeassistant_action_response(const HomeassistantActionResponse &value){};
|
||||||
#endif
|
#endif
|
||||||
#ifdef USE_API_HOMEASSISTANT_STATES
|
#ifdef USE_API_HOMEASSISTANT_STATES
|
||||||
virtual void on_subscribe_home_assistant_states_request(const SubscribeHomeAssistantStatesRequest &value){};
|
virtual void on_subscribe_home_assistant_states_request(){};
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#ifdef USE_API_HOMEASSISTANT_STATES
|
#ifdef USE_API_HOMEASSISTANT_STATES
|
||||||
@@ -147,12 +148,11 @@ class APIServerConnectionBase : public ProtoService {
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
#ifdef USE_BLUETOOTH_PROXY
|
#ifdef USE_BLUETOOTH_PROXY
|
||||||
virtual void on_subscribe_bluetooth_connections_free_request(const SubscribeBluetoothConnectionsFreeRequest &value){};
|
virtual void on_subscribe_bluetooth_connections_free_request(){};
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#ifdef USE_BLUETOOTH_PROXY
|
#ifdef USE_BLUETOOTH_PROXY
|
||||||
virtual void on_unsubscribe_bluetooth_le_advertisements_request(
|
virtual void on_unsubscribe_bluetooth_le_advertisements_request(){};
|
||||||
const UnsubscribeBluetoothLEAdvertisementsRequest &value){};
|
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#ifdef USE_BLUETOOTH_PROXY
|
#ifdef USE_BLUETOOTH_PROXY
|
||||||
@@ -229,270 +229,7 @@ class APIServerConnectionBase : public ProtoService {
|
|||||||
};
|
};
|
||||||
|
|
||||||
class APIServerConnection : public APIServerConnectionBase {
|
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:
|
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;
|
void read_message(uint32_t msg_size, uint32_t msg_type, const uint8_t *msg_data) override;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -112,8 +112,12 @@ class ProtoVarInt {
|
|||||||
uint64_t result = buffer[0] & 0x7F;
|
uint64_t result = buffer[0] & 0x7F;
|
||||||
uint8_t bitpos = 7;
|
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
|
// Start from the second byte since we've already processed the first
|
||||||
for (uint32_t i = 1; i < len; i++) {
|
for (uint32_t i = 1; i < max_len; i++) {
|
||||||
uint8_t val = buffer[i];
|
uint8_t val = buffer[i];
|
||||||
result |= uint64_t(val & 0x7F) << uint64_t(bitpos);
|
result |= uint64_t(val & 0x7F) << uint64_t(bitpos);
|
||||||
bitpos += 7;
|
bitpos += 7;
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ namespace esphome {
|
|||||||
namespace cse7766 {
|
namespace cse7766 {
|
||||||
|
|
||||||
static const char *const TAG = "cse7766";
|
static const char *const TAG = "cse7766";
|
||||||
static constexpr size_t CSE7766_RAW_DATA_SIZE = 24;
|
|
||||||
|
|
||||||
void CSE7766Component::loop() {
|
void CSE7766Component::loop() {
|
||||||
const uint32_t now = App.get_loop_component_start_time();
|
const uint32_t now = App.get_loop_component_start_time();
|
||||||
@@ -16,25 +15,39 @@ void CSE7766Component::loop() {
|
|||||||
this->raw_data_index_ = 0;
|
this->raw_data_index_ = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this->available() == 0) {
|
// Early return prevents updating last_transmission_ when no data is available.
|
||||||
|
int avail = this->available();
|
||||||
|
if (avail <= 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this->last_transmission_ = now;
|
this->last_transmission_ = now;
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this->raw_data_index_ == 23) {
|
// Read all available bytes in batches to reduce UART call overhead.
|
||||||
this->parse_data_();
|
// At 4800 baud (~480 bytes/sec) with ~122 Hz loop rate, typically ~4 bytes per call.
|
||||||
this->status_clear_warning();
|
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;
|
||||||
}
|
}
|
||||||
|
avail -= to_read;
|
||||||
|
|
||||||
this->raw_data_index_ = (this->raw_data_index_ + 1) % 24;
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -53,14 +66,15 @@ bool CSE7766Component::check_byte_() {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (index == 23) {
|
if (index == CSE7766_RAW_DATA_SIZE - 1) {
|
||||||
uint8_t checksum = 0;
|
uint8_t checksum = 0;
|
||||||
for (uint8_t i = 2; i < 23; i++) {
|
for (uint8_t i = 2; i < CSE7766_RAW_DATA_SIZE - 1; i++) {
|
||||||
checksum += this->raw_data_[i];
|
checksum += this->raw_data_[i];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (checksum != this->raw_data_[23]) {
|
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_[23]);
|
ESP_LOGW(TAG, "Invalid checksum from CSE7766: 0x%02X != 0x%02X", checksum,
|
||||||
|
this->raw_data_[CSE7766_RAW_DATA_SIZE - 1]);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
@@ -8,6 +8,8 @@
|
|||||||
namespace esphome {
|
namespace esphome {
|
||||||
namespace cse7766 {
|
namespace cse7766 {
|
||||||
|
|
||||||
|
static constexpr size_t CSE7766_RAW_DATA_SIZE = 24;
|
||||||
|
|
||||||
class CSE7766Component : public Component, public uart::UARTDevice {
|
class CSE7766Component : public Component, public uart::UARTDevice {
|
||||||
public:
|
public:
|
||||||
void set_voltage_sensor(sensor::Sensor *voltage_sensor) { voltage_sensor_ = voltage_sensor; }
|
void set_voltage_sensor(sensor::Sensor *voltage_sensor) { voltage_sensor_ = voltage_sensor; }
|
||||||
@@ -33,7 +35,7 @@ class CSE7766Component : public Component, public uart::UARTDevice {
|
|||||||
this->raw_data_[start_index + 2]);
|
this->raw_data_[start_index + 2]);
|
||||||
}
|
}
|
||||||
|
|
||||||
uint8_t raw_data_[24];
|
uint8_t raw_data_[CSE7766_RAW_DATA_SIZE];
|
||||||
uint8_t raw_data_index_{0};
|
uint8_t raw_data_index_{0};
|
||||||
uint32_t last_transmission_{0};
|
uint32_t last_transmission_{0};
|
||||||
sensor::Sensor *voltage_sensor_{nullptr};
|
sensor::Sensor *voltage_sensor_{nullptr};
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
#include "dfplayer.h"
|
#include "dfplayer.h"
|
||||||
|
#include "esphome/core/helpers.h"
|
||||||
#include "esphome/core/log.h"
|
#include "esphome/core/log.h"
|
||||||
|
|
||||||
namespace esphome {
|
namespace esphome {
|
||||||
@@ -131,140 +132,149 @@ void DFPlayer::send_cmd_(uint8_t cmd, uint16_t argument) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void DFPlayer::loop() {
|
void DFPlayer::loop() {
|
||||||
// Read message
|
// Read all available bytes in batches to reduce UART call overhead.
|
||||||
while (this->available()) {
|
int avail = this->available();
|
||||||
uint8_t byte;
|
uint8_t buf[64];
|
||||||
this->read_byte(&byte);
|
while (avail > 0) {
|
||||||
|
size_t to_read = std::min(static_cast<size_t>(avail), sizeof(buf));
|
||||||
if (this->read_pos_ == DFPLAYER_READ_BUFFER_LENGTH)
|
if (!this->read_array(buf, to_read)) {
|
||||||
this->read_pos_ = 0;
|
break;
|
||||||
|
}
|
||||||
switch (this->read_pos_) {
|
avail -= to_read;
|
||||||
case 0: // Start mark
|
for (size_t bi = 0; bi < to_read; bi++) {
|
||||||
if (byte != 0x7E)
|
uint8_t byte = buf[bi];
|
||||||
continue;
|
|
||||||
break;
|
if (this->read_pos_ == DFPLAYER_READ_BUFFER_LENGTH)
|
||||||
case 1: // Version
|
this->read_pos_ = 0;
|
||||||
if (byte != 0xFF) {
|
|
||||||
ESP_LOGW(TAG, "Expected Version 0xFF, got %#02x", byte);
|
switch (this->read_pos_) {
|
||||||
this->read_pos_ = 0;
|
case 0: // Start mark
|
||||||
continue;
|
if (byte != 0x7E)
|
||||||
}
|
continue;
|
||||||
break;
|
break;
|
||||||
case 2: // Buffer length
|
case 1: // Version
|
||||||
if (byte != 0x06) {
|
if (byte != 0xFF) {
|
||||||
ESP_LOGW(TAG, "Expected Buffer length 0x06, got %#02x", byte);
|
ESP_LOGW(TAG, "Expected Version 0xFF, got %#02x", byte);
|
||||||
this->read_pos_ = 0;
|
this->read_pos_ = 0;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 9: // End byte
|
case 2: // Buffer length
|
||||||
#ifdef ESPHOME_LOG_HAS_VERY_VERBOSE
|
if (byte != 0x06) {
|
||||||
char byte_sequence[100];
|
ESP_LOGW(TAG, "Expected Buffer length 0x06, got %#02x", byte);
|
||||||
byte_sequence[0] = '\0';
|
this->read_pos_ = 0;
|
||||||
for (size_t i = 0; i < this->read_pos_ + 1; ++i) {
|
continue;
|
||||||
snprintf(byte_sequence + strlen(byte_sequence), sizeof(byte_sequence) - strlen(byte_sequence), "%02X ",
|
}
|
||||||
this->read_buffer_[i]);
|
break;
|
||||||
}
|
case 9: // End byte
|
||||||
ESP_LOGVV(TAG, "Received byte sequence: %s", byte_sequence);
|
#ifdef ESPHOME_LOG_HAS_VERY_VERBOSE
|
||||||
#endif
|
char byte_sequence[100];
|
||||||
if (byte != 0xEF) {
|
byte_sequence[0] = '\0';
|
||||||
ESP_LOGW(TAG, "Expected end byte 0xEF, got %#02x", byte);
|
for (size_t i = 0; i < this->read_pos_ + 1; ++i) {
|
||||||
this->read_pos_ = 0;
|
snprintf(byte_sequence + strlen(byte_sequence), sizeof(byte_sequence) - strlen(byte_sequence), "%02X ",
|
||||||
continue;
|
this->read_buffer_[i]);
|
||||||
}
|
}
|
||||||
// Parse valid received command
|
ESP_LOGVV(TAG, "Received byte sequence: %s", byte_sequence);
|
||||||
uint8_t cmd = this->read_buffer_[3];
|
#endif
|
||||||
uint16_t argument = (this->read_buffer_[5] << 8) | this->read_buffer_[6];
|
if (byte != 0xEF) {
|
||||||
|
ESP_LOGW(TAG, "Expected end byte 0xEF, got %#02x", byte);
|
||||||
ESP_LOGV(TAG, "Received message cmd: %#02x arg %#04x", cmd, argument);
|
this->read_pos_ = 0;
|
||||||
|
continue;
|
||||||
switch (cmd) {
|
}
|
||||||
case 0x3A:
|
// Parse valid received command
|
||||||
if (argument == 1) {
|
uint8_t cmd = this->read_buffer_[3];
|
||||||
ESP_LOGI(TAG, "USB loaded");
|
uint16_t argument = (this->read_buffer_[5] << 8) | this->read_buffer_[6];
|
||||||
} else if (argument == 2) {
|
|
||||||
ESP_LOGI(TAG, "TF Card loaded");
|
ESP_LOGV(TAG, "Received message cmd: %#02x arg %#04x", cmd, argument);
|
||||||
}
|
|
||||||
break;
|
switch (cmd) {
|
||||||
case 0x3B:
|
case 0x3A:
|
||||||
if (argument == 1) {
|
if (argument == 1) {
|
||||||
ESP_LOGI(TAG, "USB unloaded");
|
ESP_LOGI(TAG, "USB loaded");
|
||||||
} else if (argument == 2) {
|
} else if (argument == 2) {
|
||||||
ESP_LOGI(TAG, "TF Card unloaded");
|
ESP_LOGI(TAG, "TF Card loaded");
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 0x3F:
|
case 0x3B:
|
||||||
if (argument == 1) {
|
if (argument == 1) {
|
||||||
ESP_LOGI(TAG, "USB available");
|
ESP_LOGI(TAG, "USB unloaded");
|
||||||
} else if (argument == 2) {
|
} else if (argument == 2) {
|
||||||
ESP_LOGI(TAG, "TF Card available");
|
ESP_LOGI(TAG, "TF Card unloaded");
|
||||||
} else if (argument == 3) {
|
}
|
||||||
ESP_LOGI(TAG, "USB, TF Card available");
|
break;
|
||||||
}
|
case 0x3F:
|
||||||
break;
|
if (argument == 1) {
|
||||||
case 0x40:
|
ESP_LOGI(TAG, "USB available");
|
||||||
ESP_LOGV(TAG, "Nack");
|
} else if (argument == 2) {
|
||||||
this->ack_set_is_playing_ = false;
|
ESP_LOGI(TAG, "TF Card available");
|
||||||
this->ack_reset_is_playing_ = false;
|
} else if (argument == 3) {
|
||||||
switch (argument) {
|
ESP_LOGI(TAG, "USB, TF Card available");
|
||||||
case 0x01:
|
}
|
||||||
ESP_LOGE(TAG, "Module is busy or uninitialized");
|
break;
|
||||||
break;
|
case 0x40:
|
||||||
case 0x02:
|
ESP_LOGV(TAG, "Nack");
|
||||||
ESP_LOGE(TAG, "Module is in sleep mode");
|
this->ack_set_is_playing_ = false;
|
||||||
break;
|
this->ack_reset_is_playing_ = false;
|
||||||
case 0x03:
|
switch (argument) {
|
||||||
ESP_LOGE(TAG, "Serial receive error");
|
case 0x01:
|
||||||
break;
|
ESP_LOGE(TAG, "Module is busy or uninitialized");
|
||||||
case 0x04:
|
break;
|
||||||
ESP_LOGE(TAG, "Checksum incorrect");
|
case 0x02:
|
||||||
break;
|
ESP_LOGE(TAG, "Module is in sleep mode");
|
||||||
case 0x05:
|
break;
|
||||||
ESP_LOGE(TAG, "Specified track is out of current track scope");
|
case 0x03:
|
||||||
this->is_playing_ = false;
|
ESP_LOGE(TAG, "Serial receive error");
|
||||||
break;
|
break;
|
||||||
case 0x06:
|
case 0x04:
|
||||||
ESP_LOGE(TAG, "Specified track is not found");
|
ESP_LOGE(TAG, "Checksum incorrect");
|
||||||
this->is_playing_ = false;
|
break;
|
||||||
break;
|
case 0x05:
|
||||||
case 0x07:
|
ESP_LOGE(TAG, "Specified track is out of current track scope");
|
||||||
ESP_LOGE(TAG, "Insertion error (an inserting operation only can be done when a track is being played)");
|
this->is_playing_ = false;
|
||||||
break;
|
break;
|
||||||
case 0x08:
|
case 0x06:
|
||||||
ESP_LOGE(TAG, "SD card reading failed (SD card pulled out or damaged)");
|
ESP_LOGE(TAG, "Specified track is not found");
|
||||||
break;
|
this->is_playing_ = false;
|
||||||
case 0x09:
|
break;
|
||||||
ESP_LOGE(TAG, "Entered into sleep mode");
|
case 0x07:
|
||||||
this->is_playing_ = false;
|
ESP_LOGE(TAG,
|
||||||
break;
|
"Insertion error (an inserting operation only can be done when a track is being played)");
|
||||||
}
|
break;
|
||||||
break;
|
case 0x08:
|
||||||
case 0x41:
|
ESP_LOGE(TAG, "SD card reading failed (SD card pulled out or damaged)");
|
||||||
ESP_LOGV(TAG, "Ack ok");
|
break;
|
||||||
this->is_playing_ |= this->ack_set_is_playing_;
|
case 0x09:
|
||||||
this->is_playing_ &= !this->ack_reset_is_playing_;
|
ESP_LOGE(TAG, "Entered into sleep mode");
|
||||||
this->ack_set_is_playing_ = false;
|
this->is_playing_ = false;
|
||||||
this->ack_reset_is_playing_ = false;
|
break;
|
||||||
break;
|
}
|
||||||
case 0x3C:
|
break;
|
||||||
ESP_LOGV(TAG, "Playback finished (USB drive)");
|
case 0x41:
|
||||||
this->is_playing_ = false;
|
ESP_LOGV(TAG, "Ack ok");
|
||||||
this->on_finished_playback_callback_.call();
|
this->is_playing_ |= this->ack_set_is_playing_;
|
||||||
case 0x3D:
|
this->is_playing_ &= !this->ack_reset_is_playing_;
|
||||||
ESP_LOGV(TAG, "Playback finished (SD card)");
|
this->ack_set_is_playing_ = false;
|
||||||
this->is_playing_ = false;
|
this->ack_reset_is_playing_ = false;
|
||||||
this->on_finished_playback_callback_.call();
|
break;
|
||||||
break;
|
case 0x3C:
|
||||||
default:
|
ESP_LOGV(TAG, "Playback finished (USB drive)");
|
||||||
ESP_LOGE(TAG, "Received unknown cmd %#02x arg %#04x", cmd, argument);
|
this->is_playing_ = false;
|
||||||
}
|
this->on_finished_playback_callback_.call();
|
||||||
this->sent_cmd_ = 0;
|
case 0x3D:
|
||||||
this->read_pos_ = 0;
|
ESP_LOGV(TAG, "Playback finished (SD card)");
|
||||||
continue;
|
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_++;
|
||||||
}
|
}
|
||||||
this->read_buffer_[this->read_pos_] = byte;
|
|
||||||
this->read_pos_++;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
void DFPlayer::dump_config() {
|
void DFPlayer::dump_config() {
|
||||||
|
|||||||
@@ -28,15 +28,28 @@ void DlmsMeterComponent::dump_config() {
|
|||||||
|
|
||||||
void DlmsMeterComponent::loop() {
|
void DlmsMeterComponent::loop() {
|
||||||
// Read while data is available, netznoe uses two frames so allow 2x max frame length
|
// Read while data is available, netznoe uses two frames so allow 2x max frame length
|
||||||
while (this->available()) {
|
int avail = this->available();
|
||||||
if (this->receive_buffer_.size() >= MBUS_MAX_FRAME_LENGTH * 2) {
|
if (avail > 0) {
|
||||||
|
size_t remaining = MBUS_MAX_FRAME_LENGTH * 2 - this->receive_buffer_.size();
|
||||||
|
if (remaining == 0) {
|
||||||
ESP_LOGW(TAG, "Receive buffer full, dropping remaining bytes");
|
ESP_LOGW(TAG, "Receive buffer full, dropping remaining bytes");
|
||||||
break;
|
} else {
|
||||||
|
// Read all available bytes in batches to reduce UART call overhead.
|
||||||
|
// Cap reads to remaining buffer capacity.
|
||||||
|
if (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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
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_) {
|
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]))
|
cg.add_build_flag("-DDSMR_WATER_MBUS_ID=" + str(config[CONF_WATER_MBUS_ID]))
|
||||||
|
|
||||||
# DSMR Parser
|
# DSMR Parser
|
||||||
cg.add_library("esphome/dsmr_parser", "1.0.0")
|
cg.add_library("esphome/dsmr_parser", "1.1.0")
|
||||||
|
|
||||||
# Crypto
|
# Crypto
|
||||||
cg.add_library("polargoose/Crypto-no-arduino", "0.4.0")
|
cg.add_library("polargoose/Crypto-no-arduino", "0.4.0")
|
||||||
|
|||||||
@@ -40,9 +40,7 @@ bool Dsmr::ready_to_request_data_() {
|
|||||||
this->start_requesting_data_();
|
this->start_requesting_data_();
|
||||||
}
|
}
|
||||||
if (!this->requesting_data_) {
|
if (!this->requesting_data_) {
|
||||||
while (this->available()) {
|
this->drain_rx_buffer_();
|
||||||
this->read();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return this->requesting_data_;
|
return this->requesting_data_;
|
||||||
@@ -115,13 +113,21 @@ void Dsmr::stop_requesting_data_() {
|
|||||||
} else {
|
} else {
|
||||||
ESP_LOGV(TAG, "Stop reading data from P1 port");
|
ESP_LOGV(TAG, "Stop reading data from P1 port");
|
||||||
}
|
}
|
||||||
while (this->available()) {
|
this->drain_rx_buffer_();
|
||||||
this->read();
|
|
||||||
}
|
|
||||||
this->requesting_data_ = false;
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void Dsmr::reset_telegram_() {
|
void Dsmr::reset_telegram_() {
|
||||||
this->header_found_ = false;
|
this->header_found_ = false;
|
||||||
this->footer_found_ = false;
|
this->footer_found_ = false;
|
||||||
@@ -133,120 +139,144 @@ void Dsmr::reset_telegram_() {
|
|||||||
|
|
||||||
void Dsmr::receive_telegram_() {
|
void Dsmr::receive_telegram_() {
|
||||||
while (this->available_within_timeout_()) {
|
while (this->available_within_timeout_()) {
|
||||||
const char c = this->read();
|
// Read all available bytes in batches to reduce UART call overhead.
|
||||||
|
uint8_t buf[64];
|
||||||
|
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;
|
||||||
|
|
||||||
// Find a new telegram header, i.e. forward slash.
|
for (size_t i = 0; i < to_read; i++) {
|
||||||
if (c == '/') {
|
const char c = static_cast<char>(buf[i]);
|
||||||
ESP_LOGV(TAG, "Header of telegram found");
|
|
||||||
this->reset_telegram_();
|
|
||||||
this->header_found_ = true;
|
|
||||||
}
|
|
||||||
if (!this->header_found_)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
// Check for buffer overflow.
|
// Find a new telegram header, i.e. forward slash.
|
||||||
if (this->bytes_read_ >= this->max_telegram_len_) {
|
if (c == '/') {
|
||||||
this->reset_telegram_();
|
ESP_LOGV(TAG, "Header of telegram found");
|
||||||
ESP_LOGE(TAG, "Error: telegram larger than buffer (%d bytes)", this->max_telegram_len_);
|
this->reset_telegram_();
|
||||||
return;
|
this->header_found_ = true;
|
||||||
}
|
}
|
||||||
|
if (!this->header_found_)
|
||||||
|
continue;
|
||||||
|
|
||||||
// Some v2.2 or v3 meters will send a new value which starts with '('
|
// Check for buffer overflow.
|
||||||
// in a new line, while the value belongs to the previous ObisId. For
|
if (this->bytes_read_ >= this->max_telegram_len_) {
|
||||||
// proper parsing, remove these new line characters.
|
this->reset_telegram_();
|
||||||
if (c == '(') {
|
ESP_LOGE(TAG, "Error: telegram larger than buffer (%d bytes)", this->max_telegram_len_);
|
||||||
while (true) {
|
return;
|
||||||
auto previous_char = this->telegram_[this->bytes_read_ - 1];
|
}
|
||||||
if (previous_char == '\n' || previous_char == '\r') {
|
|
||||||
this->bytes_read_--;
|
// Some v2.2 or v3 meters will send a new value which starts with '('
|
||||||
} else {
|
// in a new line, while the value belongs to the previous ObisId. For
|
||||||
break;
|
// proper parsing, remove these new line characters.
|
||||||
|
if (c == '(') {
|
||||||
|
while (true) {
|
||||||
|
auto previous_char = this->telegram_[this->bytes_read_ - 1];
|
||||||
|
if (previous_char == '\n' || previous_char == '\r') {
|
||||||
|
this->bytes_read_--;
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store the byte in the buffer.
|
||||||
|
this->telegram_[this->bytes_read_] = c;
|
||||||
|
this->bytes_read_++;
|
||||||
|
|
||||||
|
// Check for a footer, i.e. exclamation mark, followed by a hex checksum.
|
||||||
|
if (c == '!') {
|
||||||
|
ESP_LOGV(TAG, "Footer of telegram found");
|
||||||
|
this->footer_found_ = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// Check for the end of the hex checksum, i.e. a newline.
|
||||||
|
if (this->footer_found_ && c == '\n') {
|
||||||
|
// Parse the telegram and publish sensor values.
|
||||||
|
this->parse_telegram();
|
||||||
|
this->reset_telegram_();
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Store the byte in the buffer.
|
|
||||||
this->telegram_[this->bytes_read_] = c;
|
|
||||||
this->bytes_read_++;
|
|
||||||
|
|
||||||
// Check for a footer, i.e. exclamation mark, followed by a hex checksum.
|
|
||||||
if (c == '!') {
|
|
||||||
ESP_LOGV(TAG, "Footer of telegram found");
|
|
||||||
this->footer_found_ = true;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
// Check for the end of the hex checksum, i.e. a newline.
|
|
||||||
if (this->footer_found_ && c == '\n') {
|
|
||||||
// Parse the telegram and publish sensor values.
|
|
||||||
this->parse_telegram();
|
|
||||||
this->reset_telegram_();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void Dsmr::receive_encrypted_telegram_() {
|
void Dsmr::receive_encrypted_telegram_() {
|
||||||
while (this->available_within_timeout_()) {
|
while (this->available_within_timeout_()) {
|
||||||
const char c = this->read();
|
// Read all available bytes in batches to reduce UART call overhead.
|
||||||
|
uint8_t buf[64];
|
||||||
|
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;
|
||||||
|
|
||||||
// Find a new telegram start byte.
|
for (size_t i = 0; i < to_read; i++) {
|
||||||
if (!this->header_found_) {
|
const char c = static_cast<char>(buf[i]);
|
||||||
if ((uint8_t) c != 0xDB) {
|
|
||||||
continue;
|
// Find a new telegram start byte.
|
||||||
|
if (!this->header_found_) {
|
||||||
|
if ((uint8_t) c != 0xDB) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
ESP_LOGV(TAG, "Start byte 0xDB of encrypted telegram found");
|
||||||
|
this->reset_telegram_();
|
||||||
|
this->header_found_ = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for buffer overflow.
|
||||||
|
if (this->crypt_bytes_read_ >= this->max_telegram_len_) {
|
||||||
|
this->reset_telegram_();
|
||||||
|
ESP_LOGE(TAG, "Error: encrypted telegram larger than buffer (%d bytes)", this->max_telegram_len_);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store the byte in the buffer.
|
||||||
|
this->crypt_telegram_[this->crypt_bytes_read_] = c;
|
||||||
|
this->crypt_bytes_read_++;
|
||||||
|
|
||||||
|
// Read the length of the incoming encrypted telegram.
|
||||||
|
if (this->crypt_telegram_len_ == 0 && this->crypt_bytes_read_ > 20) {
|
||||||
|
// Complete header + data bytes
|
||||||
|
this->crypt_telegram_len_ = 13 + (this->crypt_telegram_[11] << 8 | this->crypt_telegram_[12]);
|
||||||
|
ESP_LOGV(TAG, "Encrypted telegram length: %d bytes", this->crypt_telegram_len_);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for the end of the encrypted telegram.
|
||||||
|
if (this->crypt_telegram_len_ == 0 || this->crypt_bytes_read_ != this->crypt_telegram_len_) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
ESP_LOGV(TAG, "End of encrypted telegram found");
|
||||||
|
|
||||||
|
// Decrypt the encrypted telegram.
|
||||||
|
GCM<AES128> *gcmaes128{new GCM<AES128>()};
|
||||||
|
gcmaes128->setKey(this->decryption_key_.data(), gcmaes128->keySize());
|
||||||
|
// the iv is 8 bytes of the system title + 4 bytes frame counter
|
||||||
|
// system title is at byte 2 and frame counter at byte 15
|
||||||
|
for (int i = 10; i < 14; i++)
|
||||||
|
this->crypt_telegram_[i] = this->crypt_telegram_[i + 4];
|
||||||
|
constexpr uint16_t iv_size{12};
|
||||||
|
gcmaes128->setIV(&this->crypt_telegram_[2], iv_size);
|
||||||
|
gcmaes128->decrypt(reinterpret_cast<uint8_t *>(this->telegram_),
|
||||||
|
// the ciphertext start at byte 18
|
||||||
|
&this->crypt_telegram_[18],
|
||||||
|
// cipher size
|
||||||
|
this->crypt_bytes_read_ - 17);
|
||||||
|
delete gcmaes128; // NOLINT(cppcoreguidelines-owning-memory)
|
||||||
|
|
||||||
|
this->bytes_read_ = strnlen(this->telegram_, this->max_telegram_len_);
|
||||||
|
ESP_LOGV(TAG, "Decrypted telegram size: %d bytes", this->bytes_read_);
|
||||||
|
ESP_LOGVV(TAG, "Decrypted telegram: %s", this->telegram_);
|
||||||
|
|
||||||
|
// Parse the decrypted telegram and publish sensor values.
|
||||||
|
this->parse_telegram();
|
||||||
|
this->reset_telegram_();
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
ESP_LOGV(TAG, "Start byte 0xDB of encrypted telegram found");
|
|
||||||
this->reset_telegram_();
|
|
||||||
this->header_found_ = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for buffer overflow.
|
|
||||||
if (this->crypt_bytes_read_ >= this->max_telegram_len_) {
|
|
||||||
this->reset_telegram_();
|
|
||||||
ESP_LOGE(TAG, "Error: encrypted telegram larger than buffer (%d bytes)", this->max_telegram_len_);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Store the byte in the buffer.
|
|
||||||
this->crypt_telegram_[this->crypt_bytes_read_] = c;
|
|
||||||
this->crypt_bytes_read_++;
|
|
||||||
|
|
||||||
// Read the length of the incoming encrypted telegram.
|
|
||||||
if (this->crypt_telegram_len_ == 0 && this->crypt_bytes_read_ > 20) {
|
|
||||||
// Complete header + data bytes
|
|
||||||
this->crypt_telegram_len_ = 13 + (this->crypt_telegram_[11] << 8 | this->crypt_telegram_[12]);
|
|
||||||
ESP_LOGV(TAG, "Encrypted telegram length: %d bytes", this->crypt_telegram_len_);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for the end of the encrypted telegram.
|
|
||||||
if (this->crypt_telegram_len_ == 0 || this->crypt_bytes_read_ != this->crypt_telegram_len_) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
ESP_LOGV(TAG, "End of encrypted telegram found");
|
|
||||||
|
|
||||||
// Decrypt the encrypted telegram.
|
|
||||||
GCM<AES128> *gcmaes128{new GCM<AES128>()};
|
|
||||||
gcmaes128->setKey(this->decryption_key_.data(), gcmaes128->keySize());
|
|
||||||
// the iv is 8 bytes of the system title + 4 bytes frame counter
|
|
||||||
// system title is at byte 2 and frame counter at byte 15
|
|
||||||
for (int i = 10; i < 14; i++)
|
|
||||||
this->crypt_telegram_[i] = this->crypt_telegram_[i + 4];
|
|
||||||
constexpr uint16_t iv_size{12};
|
|
||||||
gcmaes128->setIV(&this->crypt_telegram_[2], iv_size);
|
|
||||||
gcmaes128->decrypt(reinterpret_cast<uint8_t *>(this->telegram_),
|
|
||||||
// the ciphertext start at byte 18
|
|
||||||
&this->crypt_telegram_[18],
|
|
||||||
// cipher size
|
|
||||||
this->crypt_bytes_read_ - 17);
|
|
||||||
delete gcmaes128; // NOLINT(cppcoreguidelines-owning-memory)
|
|
||||||
|
|
||||||
this->bytes_read_ = strnlen(this->telegram_, this->max_telegram_len_);
|
|
||||||
ESP_LOGV(TAG, "Decrypted telegram size: %d bytes", this->bytes_read_);
|
|
||||||
ESP_LOGVV(TAG, "Decrypted telegram: %s", this->telegram_);
|
|
||||||
|
|
||||||
// Parse the decrypted telegram and publish sensor values.
|
|
||||||
this->parse_telegram();
|
|
||||||
this->reset_telegram_();
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -85,6 +85,7 @@ class Dsmr : public Component, public uart::UARTDevice {
|
|||||||
void receive_telegram_();
|
void receive_telegram_();
|
||||||
void receive_encrypted_telegram_();
|
void receive_encrypted_telegram_();
|
||||||
void reset_telegram_();
|
void reset_telegram_();
|
||||||
|
void drain_rx_buffer_();
|
||||||
|
|
||||||
/// Wait for UART data to become available within the read timeout.
|
/// Wait for UART data to become available within the read timeout.
|
||||||
///
|
///
|
||||||
|
|||||||
@@ -718,14 +718,6 @@ CONFIG_SCHEMA = cv.Schema(
|
|||||||
device_class=DEVICE_CLASS_POWER,
|
device_class=DEVICE_CLASS_POWER,
|
||||||
state_class=STATE_CLASS_MEASUREMENT,
|
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)
|
).extend(cv.COMPONENT_SCHEMA)
|
||||||
|
|
||||||
|
|||||||
@@ -26,7 +26,9 @@ CONFIG_SCHEMA = cv.Schema(
|
|||||||
cv.Optional("sub_equipment_id"): text_sensor.text_sensor_schema(),
|
cv.Optional("sub_equipment_id"): text_sensor.text_sensor_schema(),
|
||||||
cv.Optional("gas_delivered_text"): 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_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_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("telegram"): text_sensor.text_sensor_schema().extend(
|
||||||
{cv.Optional(CONF_INTERNAL, default=True): cv.boolean}
|
{cv.Optional(CONF_INTERNAL, default=True): cv.boolean}
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -55,7 +55,6 @@ void E131Component::setup() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void E131Component::loop() {
|
void E131Component::loop() {
|
||||||
std::vector<uint8_t> payload;
|
|
||||||
E131Packet packet;
|
E131Packet packet;
|
||||||
int universe = 0;
|
int universe = 0;
|
||||||
uint8_t buf[1460];
|
uint8_t buf[1460];
|
||||||
@@ -64,11 +63,9 @@ void E131Component::loop() {
|
|||||||
if (len == -1) {
|
if (len == -1) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
payload.resize(len);
|
|
||||||
memmove(&payload[0], buf, len);
|
|
||||||
|
|
||||||
if (!this->packet_(payload, universe, packet)) {
|
if (!this->packet_(buf, (size_t) len, universe, packet)) {
|
||||||
ESP_LOGV(TAG, "Invalid packet received of size %zu.", payload.size());
|
ESP_LOGV(TAG, "Invalid packet received of size %zd.", len);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ class E131Component : public esphome::Component {
|
|||||||
void set_method(E131ListenMethod listen_method) { this->listen_method_ = listen_method; }
|
void set_method(E131ListenMethod listen_method) { this->listen_method_ = listen_method; }
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
bool packet_(const std::vector<uint8_t> &data, int &universe, E131Packet &packet);
|
bool packet_(const uint8_t *data, size_t len, int &universe, E131Packet &packet);
|
||||||
bool process_(int universe, const E131Packet &packet);
|
bool process_(int universe, const E131Packet &packet);
|
||||||
bool join_igmp_groups_();
|
bool join_igmp_groups_();
|
||||||
void join_(int universe);
|
void join_(int universe);
|
||||||
|
|||||||
@@ -116,11 +116,11 @@ void E131Component::leave_(int universe) {
|
|||||||
ESP_LOGD(TAG, "Left %d universe for E1.31.", universe);
|
ESP_LOGD(TAG, "Left %d universe for E1.31.", universe);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool E131Component::packet_(const std::vector<uint8_t> &data, int &universe, E131Packet &packet) {
|
bool E131Component::packet_(const uint8_t *data, size_t len, int &universe, E131Packet &packet) {
|
||||||
if (data.size() < E131_MIN_PACKET_SIZE)
|
if (len < E131_MIN_PACKET_SIZE)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
auto *sbuff = reinterpret_cast<const E131RawPacket *>(&data[0]);
|
auto *sbuff = reinterpret_cast<const E131RawPacket *>(data);
|
||||||
|
|
||||||
if (memcmp(sbuff->acn_id, ACN_ID, sizeof(sbuff->acn_id)) != 0)
|
if (memcmp(sbuff->acn_id, ACN_ID, sizeof(sbuff->acn_id)) != 0)
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
67
esphome/components/epaper_spi/colorconv.h
Normal file
67
esphome/components/epaper_spi/colorconv.h
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
#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
|
||||||
227
esphome/components/epaper_spi/epaper_spi_jd79660.cpp
Normal file
227
esphome/components/epaper_spi/epaper_spi_jd79660.cpp
Normal file
@@ -0,0 +1,227 @@
|
|||||||
|
#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
|
||||||
145
esphome/components/epaper_spi/epaper_spi_jd79660.h
Normal file
145
esphome/components/epaper_spi/epaper_spi_jd79660.h
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
#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 {
|
namespace esphome::epaper_spi {
|
||||||
|
|
||||||
class EPaperSpectraE6 : public EPaperBase {
|
class EPaperSpectraE6 final : public EPaperBase {
|
||||||
public:
|
public:
|
||||||
EPaperSpectraE6(const char *name, uint16_t width, uint16_t height, const uint8_t *init_sequence,
|
EPaperSpectraE6(const char *name, uint16_t width, uint16_t height, const uint8_t *init_sequence,
|
||||||
size_t init_sequence_length)
|
size_t init_sequence_length)
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ namespace esphome::epaper_spi {
|
|||||||
/**
|
/**
|
||||||
* An epaper display that needs LUTs to be sent to it.
|
* An epaper display that needs LUTs to be sent to it.
|
||||||
*/
|
*/
|
||||||
class EpaperWaveshare : public EPaperMono {
|
class EpaperWaveshare final : public EPaperMono {
|
||||||
public:
|
public:
|
||||||
EpaperWaveshare(const char *name, uint16_t width, uint16_t height, const uint8_t *init_sequence,
|
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,
|
size_t init_sequence_length, const uint8_t *lut, size_t lut_length, const uint8_t *partial_lut,
|
||||||
|
|||||||
86
esphome/components/epaper_spi/models/jd79660.py
Normal file
86
esphome/components/epaper_spi/models/jd79660.py
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
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,),
|
||||||
|
),
|
||||||
|
)
|
||||||
@@ -27,6 +27,11 @@ static const char *const TAG = "esp32_hosted.update";
|
|||||||
// Older coprocessor firmware versions have a 1500-byte limit per RPC call
|
// Older coprocessor firmware versions have a 1500-byte limit per RPC call
|
||||||
constexpr size_t CHUNK_SIZE = 1500;
|
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
|
// Compile-time version string from esp_hosted_host_fw_ver.h macros
|
||||||
#define STRINGIFY_(x) #x
|
#define STRINGIFY_(x) #x
|
||||||
#define STRINGIFY(x) STRINGIFY_(x)
|
#define STRINGIFY(x) STRINGIFY_(x)
|
||||||
@@ -127,15 +132,18 @@ void Esp32HostedUpdate::setup() {
|
|||||||
this->status_clear_error();
|
this->status_clear_error();
|
||||||
this->publish_state();
|
this->publish_state();
|
||||||
#else
|
#else
|
||||||
// HTTP mode: retry initial check every 10s until network is ready (max 6 attempts)
|
// HTTP mode: check every 10s until network is ready (max 6 attempts)
|
||||||
// Only if update interval is > 1 minute to avoid redundant checks
|
// Only if update interval is > 1 minute to avoid redundant checks
|
||||||
if (this->get_update_interval() > 60000) {
|
if (this->get_update_interval() > 60000) {
|
||||||
this->set_retry("initial_check", 10000, 6, [this](uint8_t) {
|
this->initial_check_remaining_ = 6;
|
||||||
if (!network::is_connected()) {
|
this->set_interval(INITIAL_CHECK_INTERVAL_ID, 10000, [this]() {
|
||||||
return RetryResult::RETRY;
|
bool connected = network::is_connected();
|
||||||
|
if (--this->initial_check_remaining_ == 0 || connected) {
|
||||||
|
this->cancel_interval(INITIAL_CHECK_INTERVAL_ID);
|
||||||
|
if (connected) {
|
||||||
|
this->check();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
this->check();
|
|
||||||
return RetryResult::DONE;
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ class Esp32HostedUpdate : public update::UpdateEntity, public PollingComponent {
|
|||||||
// HTTP mode helpers
|
// HTTP mode helpers
|
||||||
bool fetch_manifest_();
|
bool fetch_manifest_();
|
||||||
bool stream_firmware_to_coprocessor_();
|
bool stream_firmware_to_coprocessor_();
|
||||||
|
uint8_t initial_check_remaining_{0};
|
||||||
#else
|
#else
|
||||||
// Embedded mode members
|
// Embedded mode members
|
||||||
const uint8_t *firmware_data_{nullptr};
|
const uint8_t *firmware_data_{nullptr};
|
||||||
|
|||||||
@@ -275,8 +275,19 @@ void LD2410Component::restart_and_read_all_info() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void LD2410Component::loop() {
|
void LD2410Component::loop() {
|
||||||
while (this->available()) {
|
// Read all available bytes in batches to reduce UART call overhead.
|
||||||
this->readline_(this->read());
|
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]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -310,8 +310,19 @@ void LD2412Component::restart_and_read_all_info() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void LD2412Component::loop() {
|
void LD2412Component::loop() {
|
||||||
while (this->available()) {
|
// Read all available bytes in batches to reduce UART call overhead.
|
||||||
this->readline_(this->read());
|
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]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -335,9 +335,10 @@ void LD2420Component::revert_config_action() {
|
|||||||
|
|
||||||
void LD2420Component::loop() {
|
void LD2420Component::loop() {
|
||||||
// If there is a active send command do not process it here, the send command call will handle it.
|
// If there is a active send command do not process it here, the send command call will handle it.
|
||||||
while (!this->cmd_active_ && this->available()) {
|
if (this->cmd_active_) {
|
||||||
this->readline_(this->read(), this->buffer_data_, MAX_LINE_LENGTH);
|
return;
|
||||||
}
|
}
|
||||||
|
this->read_batch_(this->buffer_data_);
|
||||||
}
|
}
|
||||||
|
|
||||||
void LD2420Component::update_radar_data(uint16_t const *gate_energy, uint8_t sample_number) {
|
void LD2420Component::update_radar_data(uint16_t const *gate_energy, uint8_t sample_number) {
|
||||||
@@ -539,6 +540,23 @@ void LD2420Component::handle_simple_mode_(const uint8_t *inbuf, int len) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void LD2420Component::read_batch_(std::span<uint8_t, MAX_LINE_LENGTH> buffer) {
|
||||||
|
// Read all available bytes in batches to reduce UART call overhead.
|
||||||
|
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) {
|
void LD2420Component::handle_ack_data_(uint8_t *buffer, int len) {
|
||||||
this->cmd_reply_.command = buffer[CMD_FRAME_COMMAND];
|
this->cmd_reply_.command = buffer[CMD_FRAME_COMMAND];
|
||||||
this->cmd_reply_.length = buffer[CMD_FRAME_DATA_LENGTH];
|
this->cmd_reply_.length = buffer[CMD_FRAME_DATA_LENGTH];
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
#include "esphome/components/uart/uart.h"
|
#include "esphome/components/uart/uart.h"
|
||||||
#include "esphome/core/automation.h"
|
#include "esphome/core/automation.h"
|
||||||
#include "esphome/core/helpers.h"
|
#include "esphome/core/helpers.h"
|
||||||
|
#include <span>
|
||||||
#ifdef USE_TEXT_SENSOR
|
#ifdef USE_TEXT_SENSOR
|
||||||
#include "esphome/components/text_sensor/text_sensor.h"
|
#include "esphome/components/text_sensor/text_sensor.h"
|
||||||
#endif
|
#endif
|
||||||
@@ -165,6 +166,7 @@ class LD2420Component : public Component, public uart::UARTDevice {
|
|||||||
void handle_energy_mode_(uint8_t *buffer, int len);
|
void handle_energy_mode_(uint8_t *buffer, int len);
|
||||||
void handle_ack_data_(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 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; };
|
void set_calibration_(bool state) { this->calibration_ = state; };
|
||||||
bool get_calibration_() { return this->calibration_; };
|
bool get_calibration_() { return this->calibration_; };
|
||||||
|
|
||||||
|
|||||||
@@ -276,8 +276,19 @@ void LD2450Component::dump_config() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void LD2450Component::loop() {
|
void LD2450Component::loop() {
|
||||||
while (this->available()) {
|
// Read all available bytes in batches to reduce UART call overhead.
|
||||||
this->readline_(this->read());
|
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]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ void HOT Logger::write_msg_(const char *msg, uint16_t len) {
|
|||||||
#ifdef CONFIG_PRINTK
|
#ifdef CONFIG_PRINTK
|
||||||
// Requires the debug component and an active SWD connection.
|
// Requires the debug component and an active SWD connection.
|
||||||
// It is used for pyocd rtt -t nrf52840
|
// It is used for pyocd rtt -t nrf52840
|
||||||
k_str_out(const_cast<char *>(msg), len);
|
printk("%.*s", static_cast<int>(len), msg);
|
||||||
#endif
|
#endif
|
||||||
if (this->uart_dev_ == nullptr) {
|
if (this->uart_dev_ == nullptr) {
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -38,22 +38,29 @@ void LPS22Component::dump_config() {
|
|||||||
LOG_UPDATE_INTERVAL(this);
|
LOG_UPDATE_INTERVAL(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static constexpr uint32_t INTERVAL_READ = 0;
|
||||||
|
|
||||||
void LPS22Component::update() {
|
void LPS22Component::update() {
|
||||||
uint8_t value = 0x00;
|
uint8_t value = 0x00;
|
||||||
this->read_register(CTRL_REG2, &value, 1);
|
this->read_register(CTRL_REG2, &value, 1);
|
||||||
value |= CTRL_REG2_ONE_SHOT_MASK;
|
value |= CTRL_REG2_ONE_SHOT_MASK;
|
||||||
this->write_register(CTRL_REG2, &value, 1);
|
this->write_register(CTRL_REG2, &value, 1);
|
||||||
this->set_retry(READ_INTERVAL, READ_ATTEMPTS, [this](uint8_t _) { return this->try_read_(); });
|
this->read_attempts_remaining_ = READ_ATTEMPTS;
|
||||||
|
this->set_interval(INTERVAL_READ, READ_INTERVAL, [this]() { this->try_read_(); });
|
||||||
}
|
}
|
||||||
|
|
||||||
RetryResult LPS22Component::try_read_() {
|
void LPS22Component::try_read_() {
|
||||||
uint8_t value = 0x00;
|
uint8_t value = 0x00;
|
||||||
this->read_register(STATUS, &value, 1);
|
this->read_register(STATUS, &value, 1);
|
||||||
const uint8_t expected_status_mask = STATUS_T_DA_MASK | STATUS_P_DA_MASK;
|
const uint8_t expected_status_mask = STATUS_T_DA_MASK | STATUS_P_DA_MASK;
|
||||||
if ((value & expected_status_mask) != expected_status_mask) {
|
if ((value & expected_status_mask) != expected_status_mask) {
|
||||||
ESP_LOGD(TAG, "STATUS not ready: %x", value);
|
ESP_LOGD(TAG, "STATUS not ready: %x", value);
|
||||||
return RetryResult::RETRY;
|
if (--this->read_attempts_remaining_ == 0) {
|
||||||
|
this->cancel_interval(INTERVAL_READ);
|
||||||
|
}
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
this->cancel_interval(INTERVAL_READ);
|
||||||
|
|
||||||
if (this->temperature_sensor_ != nullptr) {
|
if (this->temperature_sensor_ != nullptr) {
|
||||||
uint8_t t_buf[2]{0};
|
uint8_t t_buf[2]{0};
|
||||||
@@ -68,7 +75,6 @@ RetryResult LPS22Component::try_read_() {
|
|||||||
uint32_t p_lsb = encode_uint24(p_buf[2], p_buf[1], p_buf[0]);
|
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));
|
this->pressure_sensor_->publish_state(PRESSURE_SCALE * static_cast<float>(p_lsb));
|
||||||
}
|
}
|
||||||
return RetryResult::DONE;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace lps22
|
} // namespace lps22
|
||||||
|
|||||||
@@ -17,10 +17,11 @@ class LPS22Component : public sensor::Sensor, public PollingComponent, public i2
|
|||||||
void dump_config() override;
|
void dump_config() override;
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
|
void try_read_();
|
||||||
|
|
||||||
sensor::Sensor *temperature_sensor_{nullptr};
|
sensor::Sensor *temperature_sensor_{nullptr};
|
||||||
sensor::Sensor *pressure_sensor_{nullptr};
|
sensor::Sensor *pressure_sensor_{nullptr};
|
||||||
|
uint8_t read_attempts_remaining_{0};
|
||||||
RetryResult try_read_();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace lps22
|
} // namespace lps22
|
||||||
|
|||||||
@@ -436,6 +436,7 @@ def container_schema(widget_type: WidgetType, extras=None):
|
|||||||
schema = schema.extend(widget_type.schema)
|
schema = schema.extend(widget_type.schema)
|
||||||
|
|
||||||
def validator(value):
|
def validator(value):
|
||||||
|
value = value or {}
|
||||||
return append_layout_schema(schema, value)(value)
|
return append_layout_schema(schema, value)(value)
|
||||||
|
|
||||||
return validator
|
return validator
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
from esphome import automation
|
from esphome import automation
|
||||||
import esphome.codegen as cg
|
import esphome.codegen as cg
|
||||||
from esphome.components import audio, esp32, speaker
|
from esphome.components import audio, esp32, socket, speaker
|
||||||
import esphome.config_validation as cv
|
import esphome.config_validation as cv
|
||||||
from esphome.const import (
|
from esphome.const import (
|
||||||
CONF_BITS_PER_SAMPLE,
|
CONF_BITS_PER_SAMPLE,
|
||||||
@@ -61,7 +61,7 @@ def _set_stream_limits(config):
|
|||||||
def _validate_source_speaker(config):
|
def _validate_source_speaker(config):
|
||||||
fconf = fv.full_config.get()
|
fconf = fv.full_config.get()
|
||||||
|
|
||||||
# Get ID for the output speaker and add it to the source speakrs config to easily inherit properties
|
# Get ID for the output speaker and add it to the source speakers config to easily inherit properties
|
||||||
path = fconf.get_path_for_id(config[CONF_ID])[:-3]
|
path = fconf.get_path_for_id(config[CONF_ID])[:-3]
|
||||||
path.append(CONF_OUTPUT_SPEAKER)
|
path.append(CONF_OUTPUT_SPEAKER)
|
||||||
output_speaker_id = fconf.get_config_for_path(path)
|
output_speaker_id = fconf.get_config_for_path(path)
|
||||||
@@ -111,6 +111,9 @@ FINAL_VALIDATE_SCHEMA = cv.All(
|
|||||||
|
|
||||||
|
|
||||||
async def to_code(config):
|
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])
|
var = cg.new_Pvariable(config[CONF_ID])
|
||||||
await cg.register_component(var, config)
|
await cg.register_component(var, config)
|
||||||
|
|
||||||
@@ -127,6 +130,9 @@ async def to_code(config):
|
|||||||
"CONFIG_SPIRAM_ALLOW_STACK_EXTERNAL_MEMORY", True
|
"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]:
|
for speaker_config in config[CONF_SOURCE_SPEAKERS]:
|
||||||
source_speaker = cg.new_Pvariable(speaker_config[CONF_ID])
|
source_speaker = cg.new_Pvariable(speaker_config[CONF_ID])
|
||||||
|
|
||||||
|
|||||||
@@ -8,8 +8,8 @@
|
|||||||
namespace esphome {
|
namespace esphome {
|
||||||
namespace mixer_speaker {
|
namespace mixer_speaker {
|
||||||
template<typename... Ts> class DuckingApplyAction : public Action<Ts...>, public Parented<SourceSpeaker> {
|
template<typename... Ts> class DuckingApplyAction : public Action<Ts...>, public Parented<SourceSpeaker> {
|
||||||
TEMPLATABLE_VALUE(uint8_t, decibel_reduction)
|
TEMPLATABLE_VALUE(uint8_t, decibel_reduction);
|
||||||
TEMPLATABLE_VALUE(uint32_t, duration)
|
TEMPLATABLE_VALUE(uint32_t, duration);
|
||||||
void play(const Ts &...x) override {
|
void play(const Ts &...x) override {
|
||||||
this->parent_->apply_ducking(this->decibel_reduction_.value(x...), this->duration_.value(x...));
|
this->parent_->apply_ducking(this->decibel_reduction_.value(x...), this->duration_.value(x...));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,11 +2,13 @@
|
|||||||
|
|
||||||
#ifdef USE_ESP32
|
#ifdef USE_ESP32
|
||||||
|
|
||||||
|
#include "esphome/core/application.h"
|
||||||
#include "esphome/core/hal.h"
|
#include "esphome/core/hal.h"
|
||||||
#include "esphome/core/helpers.h"
|
#include "esphome/core/helpers.h"
|
||||||
#include "esphome/core/log.h"
|
#include "esphome/core/log.h"
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
|
#include <array>
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
|
|
||||||
namespace esphome {
|
namespace esphome {
|
||||||
@@ -14,6 +16,7 @@ namespace mixer_speaker {
|
|||||||
|
|
||||||
static const UBaseType_t MIXER_TASK_PRIORITY = 10;
|
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 TRANSFER_BUFFER_DURATION_MS = 50;
|
||||||
static const uint32_t TASK_DELAY_MS = 25;
|
static const uint32_t TASK_DELAY_MS = 25;
|
||||||
|
|
||||||
@@ -27,21 +30,53 @@ static const char *const TAG = "speaker_mixer";
|
|||||||
// Gives the Q15 fixed point scaling factor to reduce by 0 dB, 1dB, ..., 50 dB
|
// 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)
|
// 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)
|
// float to Q15 fixed point formula: q15_scale_factor = floating_point_scale_factor * 2^(15)
|
||||||
static const std::vector<int16_t> DECIBEL_REDUCTION_TABLE = {
|
static const std::array<int16_t, 51> DECIBEL_REDUCTION_TABLE = {
|
||||||
32767, 29201, 26022, 23189, 20665, 18415, 16410, 14624, 13032, 11613, 10349, 9222, 8218, 7324, 6527, 5816, 5183,
|
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,
|
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};
|
651, 580, 517, 461, 411, 366, 326, 291, 259, 231, 206, 183, 163, 146, 130, 116, 103};
|
||||||
|
|
||||||
enum MixerEventGroupBits : uint32_t {
|
// Event bits for SourceSpeaker command processing
|
||||||
COMMAND_STOP = (1 << 0), // stops the mixer task
|
enum SourceSpeakerEventBits : uint32_t {
|
||||||
STATE_STARTING = (1 << 10),
|
SOURCE_SPEAKER_COMMAND_START = (1 << 0),
|
||||||
STATE_RUNNING = (1 << 11),
|
SOURCE_SPEAKER_COMMAND_STOP = (1 << 1),
|
||||||
STATE_STOPPING = (1 << 12),
|
SOURCE_SPEAKER_COMMAND_FINISH = (1 << 2),
|
||||||
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() {
|
void SourceSpeaker::dump_config() {
|
||||||
ESP_LOGCONFIG(TAG,
|
ESP_LOGCONFIG(TAG,
|
||||||
"Mixer Source Speaker\n"
|
"Mixer Source Speaker\n"
|
||||||
@@ -55,22 +90,70 @@ void SourceSpeaker::dump_config() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void SourceSpeaker::setup() {
|
void SourceSpeaker::setup() {
|
||||||
this->parent_->get_output_speaker()->add_audio_output_callback([this](uint32_t new_frames, int64_t write_timestamp) {
|
if (!create_event_group(this->event_group_, this)) {
|
||||||
// The SourceSpeaker may not have included any audio in the mixed output, so verify there were pending frames
|
return;
|
||||||
uint32_t speakers_playback_frames = std::min(new_frames, this->pending_playback_frames_);
|
}
|
||||||
this->pending_playback_frames_ -= speakers_playback_frames;
|
|
||||||
|
|
||||||
if (speakers_playback_frames > 0) {
|
// Start with loop disabled since we begin in STATE_STOPPED with no pending commands
|
||||||
this->audio_output_callback_(speakers_playback_frames, write_timestamp);
|
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;
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void SourceSpeaker::loop() {
|
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_) {
|
switch (this->state_) {
|
||||||
case speaker::STATE_STARTING: {
|
case speaker::STATE_STARTING: {
|
||||||
esp_err_t err = this->start_();
|
esp_err_t err = this->start_();
|
||||||
if (err == ESP_OK) {
|
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->state_ = speaker::STATE_RUNNING;
|
||||||
this->stop_gracefully_ = false;
|
this->stop_gracefully_ = false;
|
||||||
this->last_seen_data_ms_ = millis();
|
this->last_seen_data_ms_ = millis();
|
||||||
@@ -78,41 +161,62 @@ void SourceSpeaker::loop() {
|
|||||||
} else {
|
} else {
|
||||||
switch (err) {
|
switch (err) {
|
||||||
case ESP_ERR_NO_MEM:
|
case ESP_ERR_NO_MEM:
|
||||||
this->status_set_error(LOG_STR("Failed to start mixer: not enough memory"));
|
this->status_set_error(LOG_STR("Not enough memory"));
|
||||||
break;
|
break;
|
||||||
case ESP_ERR_NOT_SUPPORTED:
|
case ESP_ERR_NOT_SUPPORTED:
|
||||||
this->status_set_error(LOG_STR("Failed to start mixer: unsupported bits per sample"));
|
this->status_set_error(LOG_STR("Unsupported bit depth"));
|
||||||
break;
|
break;
|
||||||
case ESP_ERR_INVALID_ARG:
|
case ESP_ERR_INVALID_ARG:
|
||||||
this->status_set_error(
|
this->status_set_error(LOG_STR("Incompatible audio streams"));
|
||||||
LOG_STR("Failed to start mixer: audio stream isn't compatible with the other audio stream."));
|
|
||||||
break;
|
break;
|
||||||
case ESP_ERR_INVALID_STATE:
|
case ESP_ERR_INVALID_STATE:
|
||||||
this->status_set_error(LOG_STR("Failed to start mixer: mixer task failed to start"));
|
this->status_set_error(LOG_STR("Task failed"));
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
this->status_set_error(LOG_STR("Failed to start mixer"));
|
this->status_set_error(LOG_STR("Failed"));
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
this->state_ = speaker::STATE_STOPPING;
|
this->enter_stopping_state_();
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case speaker::STATE_RUNNING:
|
case speaker::STATE_RUNNING:
|
||||||
if (!this->transfer_buffer_->has_buffered_data()) {
|
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->timeout_ms_.has_value() && ((millis() - this->last_seen_data_ms_) > this->timeout_ms_.value())) ||
|
if ((this->timeout_ms_.has_value() && ((millis() - this->last_seen_data_ms_) > this->timeout_ms_.value())) ||
|
||||||
this->stop_gracefully_) {
|
this->stop_gracefully_) {
|
||||||
this->state_ = speaker::STATE_STOPPING;
|
// Timeout exceeded or graceful stop requested
|
||||||
|
this->enter_stopping_state_();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case speaker::STATE_STOPPING:
|
case speaker::STATE_STOPPING: {
|
||||||
this->stop_();
|
if ((this->parent_->get_output_speaker()->get_pause_state()) ||
|
||||||
this->stop_gracefully_ = false;
|
((millis() - this->stopping_start_ms_) > STOPPING_TIMEOUT_MS)) {
|
||||||
this->state_ = speaker::STATE_STOPPED;
|
// 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;
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
case speaker::STATE_STOPPED:
|
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;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -122,17 +226,34 @@ size_t SourceSpeaker::play(const uint8_t *data, size_t length, TickType_t ticks_
|
|||||||
this->start();
|
this->start();
|
||||||
}
|
}
|
||||||
size_t bytes_written = 0;
|
size_t bytes_written = 0;
|
||||||
if (this->ring_buffer_.use_count() == 1) {
|
std::shared_ptr<RingBuffer> temp_ring_buffer = this->ring_buffer_.lock();
|
||||||
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
|
||||||
bytes_written = temp_ring_buffer->write_without_replacement(data, length, ticks_to_wait);
|
bytes_written = temp_ring_buffer->write_without_replacement(data, length, ticks_to_wait);
|
||||||
if (bytes_written > 0) {
|
if (bytes_written > 0) {
|
||||||
this->last_seen_data_ms_ = millis();
|
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;
|
return bytes_written;
|
||||||
}
|
}
|
||||||
|
|
||||||
void SourceSpeaker::start() { this->state_ = speaker::STATE_STARTING; }
|
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); }
|
||||||
|
|
||||||
esp_err_t SourceSpeaker::start_() {
|
esp_err_t SourceSpeaker::start_() {
|
||||||
const size_t ring_buffer_size = this->audio_stream_info_.ms_to_bytes(this->buffer_duration_ms_);
|
const size_t ring_buffer_size = this->audio_stream_info_.ms_to_bytes(this->buffer_duration_ms_);
|
||||||
@@ -143,35 +264,26 @@ esp_err_t SourceSpeaker::start_() {
|
|||||||
if (this->transfer_buffer_ == nullptr) {
|
if (this->transfer_buffer_ == nullptr) {
|
||||||
return ESP_ERR_NO_MEM;
|
return ESP_ERR_NO_MEM;
|
||||||
}
|
}
|
||||||
std::shared_ptr<RingBuffer> temp_ring_buffer;
|
|
||||||
|
|
||||||
if (!this->ring_buffer_.use_count()) {
|
std::shared_ptr<RingBuffer> temp_ring_buffer = this->ring_buffer_.lock();
|
||||||
|
if (!temp_ring_buffer) {
|
||||||
temp_ring_buffer = RingBuffer::create(ring_buffer_size);
|
temp_ring_buffer = RingBuffer::create(ring_buffer_size);
|
||||||
this->ring_buffer_ = temp_ring_buffer;
|
this->ring_buffer_ = temp_ring_buffer;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this->ring_buffer_.use_count()) {
|
if (!temp_ring_buffer) {
|
||||||
return ESP_ERR_NO_MEM;
|
return ESP_ERR_NO_MEM;
|
||||||
} else {
|
} else {
|
||||||
this->transfer_buffer_->set_source(temp_ring_buffer);
|
this->transfer_buffer_->set_source(temp_ring_buffer);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this->pending_playback_frames_ = 0; // reset
|
|
||||||
return this->parent_->start(this->audio_stream_info_);
|
return this->parent_->start(this->audio_stream_info_);
|
||||||
}
|
}
|
||||||
|
|
||||||
void SourceSpeaker::stop() {
|
void SourceSpeaker::stop() { this->send_command_(SOURCE_SPEAKER_COMMAND_STOP); }
|
||||||
if (this->state_ != speaker::STATE_STOPPED) {
|
|
||||||
this->state_ = speaker::STATE_STOPPING;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void SourceSpeaker::stop_() {
|
void SourceSpeaker::finish() { this->send_command_(SOURCE_SPEAKER_COMMAND_FINISH); }
|
||||||
this->transfer_buffer_.reset(); // deallocates the transfer buffer
|
|
||||||
}
|
|
||||||
|
|
||||||
void SourceSpeaker::finish() { this->stop_gracefully_ = true; }
|
|
||||||
|
|
||||||
bool SourceSpeaker::has_buffered_data() const {
|
bool SourceSpeaker::has_buffered_data() const {
|
||||||
return ((this->transfer_buffer_.use_count() > 0) && this->transfer_buffer_->has_buffered_data());
|
return ((this->transfer_buffer_.use_count() > 0) && this->transfer_buffer_->has_buffered_data());
|
||||||
@@ -191,19 +303,16 @@ void SourceSpeaker::set_volume(float volume) {
|
|||||||
|
|
||||||
float SourceSpeaker::get_volume() { return this->parent_->get_output_speaker()->get_volume(); }
|
float SourceSpeaker::get_volume() { return this->parent_->get_output_speaker()->get_volume(); }
|
||||||
|
|
||||||
size_t SourceSpeaker::process_data_from_source(TickType_t ticks_to_wait) {
|
size_t SourceSpeaker::process_data_from_source(std::shared_ptr<audio::AudioSourceTransferBuffer> &transfer_buffer,
|
||||||
if (!this->transfer_buffer_.use_count()) {
|
TickType_t ticks_to_wait) {
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Store current offset, as these samples are already ducked
|
// Store current offset, as these samples are already ducked
|
||||||
const size_t current_length = this->transfer_buffer_->available();
|
const size_t current_length = transfer_buffer->available();
|
||||||
|
|
||||||
size_t bytes_read = this->transfer_buffer_->transfer_data_from_source(ticks_to_wait);
|
size_t bytes_read = transfer_buffer->transfer_data_from_source(ticks_to_wait);
|
||||||
|
|
||||||
uint32_t samples_to_duck = this->audio_stream_info_.bytes_to_samples(bytes_read);
|
uint32_t samples_to_duck = this->audio_stream_info_.bytes_to_samples(bytes_read);
|
||||||
if (samples_to_duck > 0) {
|
if (samples_to_duck > 0) {
|
||||||
int16_t *current_buffer = reinterpret_cast<int16_t *>(this->transfer_buffer_->get_buffer_start() + current_length);
|
int16_t *current_buffer = reinterpret_cast<int16_t *>(transfer_buffer->get_buffer_start() + current_length);
|
||||||
|
|
||||||
duck_samples(current_buffer, samples_to_duck, &this->current_ducking_db_reduction_,
|
duck_samples(current_buffer, samples_to_duck, &this->current_ducking_db_reduction_,
|
||||||
&this->ducking_transition_samples_remaining_, this->samples_per_ducking_step_,
|
&this->ducking_transition_samples_remaining_, this->samples_per_ducking_step_,
|
||||||
@@ -215,10 +324,13 @@ size_t SourceSpeaker::process_data_from_source(TickType_t ticks_to_wait) {
|
|||||||
|
|
||||||
void SourceSpeaker::apply_ducking(uint8_t decibel_reduction, uint32_t duration) {
|
void SourceSpeaker::apply_ducking(uint8_t decibel_reduction, uint32_t duration) {
|
||||||
if (this->target_ducking_db_reduction_ != decibel_reduction) {
|
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->current_ducking_db_reduction_ = this->target_ducking_db_reduction_;
|
||||||
|
|
||||||
this->target_ducking_db_reduction_ = decibel_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;
|
uint8_t total_ducking_steps = 0;
|
||||||
if (this->target_ducking_db_reduction_ > this->current_ducking_db_reduction_) {
|
if (this->target_ducking_db_reduction_ > this->current_ducking_db_reduction_) {
|
||||||
// The dB reduction level is increasing (which results in quieter audio)
|
// The dB reduction level is increasing (which results in quieter audio)
|
||||||
@@ -234,7 +346,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->samples_per_ducking_step_ = this->ducking_transition_samples_remaining_ / total_ducking_steps;
|
||||||
this->ducking_transition_samples_remaining_ =
|
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_;
|
this->current_ducking_db_reduction_ += this->db_change_per_ducking_step_;
|
||||||
} else {
|
} else {
|
||||||
@@ -293,6 +405,12 @@ 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() {
|
void MixerSpeaker::dump_config() {
|
||||||
ESP_LOGCONFIG(TAG,
|
ESP_LOGCONFIG(TAG,
|
||||||
"Speaker Mixer:\n"
|
"Speaker Mixer:\n"
|
||||||
@@ -301,42 +419,74 @@ void MixerSpeaker::dump_config() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void MixerSpeaker::setup() {
|
void MixerSpeaker::setup() {
|
||||||
this->event_group_ = xEventGroupCreate();
|
if (!create_event_group(this->event_group_, this)) {
|
||||||
|
|
||||||
if (this->event_group_ == nullptr) {
|
|
||||||
ESP_LOGE(TAG, "Failed to create event group");
|
|
||||||
this->mark_failed();
|
|
||||||
return;
|
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() {
|
void MixerSpeaker::loop() {
|
||||||
uint32_t event_group_bits = xEventGroupGetBits(this->event_group_);
|
uint32_t event_group_bits = xEventGroupGetBits(this->event_group_);
|
||||||
|
|
||||||
if (event_group_bits & MixerEventGroupBits::STATE_STARTING) {
|
// Handle pending start request
|
||||||
ESP_LOGD(TAG, "Starting speaker mixer");
|
if (event_group_bits & MIXER_TASK_COMMAND_START) {
|
||||||
xEventGroupClearBits(this->event_group_, MixerEventGroupBits::STATE_STARTING);
|
// 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::ERR_ESP_NO_MEM) {
|
|
||||||
this->status_set_error(LOG_STR("Failed to allocate the mixer's internal buffer"));
|
if (event_group_bits & MIXER_TASK_STATE_STARTING) {
|
||||||
xEventGroupClearBits(this->event_group_, MixerEventGroupBits::ERR_ESP_NO_MEM);
|
ESP_LOGD(TAG, "Starting");
|
||||||
|
xEventGroupClearBits(this->event_group_, MIXER_TASK_STATE_STARTING);
|
||||||
}
|
}
|
||||||
if (event_group_bits & MixerEventGroupBits::STATE_RUNNING) {
|
if (event_group_bits & MIXER_TASK_ERR_ESP_NO_MEM) {
|
||||||
ESP_LOGD(TAG, "Started speaker mixer");
|
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");
|
||||||
this->status_clear_error();
|
this->status_clear_error();
|
||||||
xEventGroupClearBits(this->event_group_, MixerEventGroupBits::STATE_RUNNING);
|
xEventGroupClearBits(this->event_group_, MIXER_TASK_STATE_RUNNING);
|
||||||
}
|
}
|
||||||
if (event_group_bits & MixerEventGroupBits::STATE_STOPPING) {
|
if (event_group_bits & MIXER_TASK_STATE_STOPPING) {
|
||||||
ESP_LOGD(TAG, "Stopping speaker mixer");
|
ESP_LOGV(TAG, "Stopping");
|
||||||
xEventGroupClearBits(this->event_group_, MixerEventGroupBits::STATE_STOPPING);
|
xEventGroupClearBits(this->event_group_, MIXER_TASK_STATE_STOPPING);
|
||||||
}
|
}
|
||||||
if (event_group_bits & MixerEventGroupBits::STATE_STOPPED) {
|
if (event_group_bits & MIXER_TASK_STATE_STOPPED) {
|
||||||
if (this->delete_task_() == ESP_OK) {
|
if (this->delete_task_() == ESP_OK) {
|
||||||
xEventGroupClearBits(this->event_group_, MixerEventGroupBits::ALL_BITS);
|
ESP_LOGD(TAG, "Stopped");
|
||||||
|
xEventGroupClearBits(this->event_group_, MIXER_TASK_ALL_BITS);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this->task_handle_ != nullptr) {
|
if (this->task_handle_ != nullptr) {
|
||||||
|
// If the mixer task is running, check if all source speakers are stopped
|
||||||
|
|
||||||
bool all_stopped = true;
|
bool all_stopped = true;
|
||||||
|
|
||||||
for (auto &speaker : this->source_speakers_) {
|
for (auto &speaker : this->source_speakers_) {
|
||||||
@@ -344,7 +494,15 @@ void MixerSpeaker::loop() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (all_stopped) {
|
if (all_stopped) {
|
||||||
this->stop();
|
// 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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -366,7 +524,18 @@ esp_err_t MixerSpeaker::start(audio::AudioStreamInfo &stream_info) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return this->start_task_();
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
esp_err_t MixerSpeaker::start_task_() {
|
esp_err_t MixerSpeaker::start_task_() {
|
||||||
@@ -397,28 +566,31 @@ esp_err_t MixerSpeaker::start_task_() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
esp_err_t MixerSpeaker::delete_task_() {
|
esp_err_t MixerSpeaker::delete_task_() {
|
||||||
if (!this->task_created_) {
|
if (this->task_handle_ != nullptr) {
|
||||||
|
// Delete the task
|
||||||
|
vTaskDelete(this->task_handle_);
|
||||||
this->task_handle_ = nullptr;
|
this->task_handle_ = nullptr;
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
return ESP_OK;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return ESP_ERR_INVALID_STATE;
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
void MixerSpeaker::stop() { xEventGroupSetBits(this->event_group_, MixerEventGroupBits::COMMAND_STOP); }
|
this->task_stack_buffer_ = nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((this->task_handle_ != nullptr) || (this->task_stack_buffer_ != nullptr)) {
|
||||||
|
return ESP_ERR_INVALID_STATE;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ESP_OK;
|
||||||
|
}
|
||||||
|
|
||||||
void MixerSpeaker::copy_frames(const int16_t *input_buffer, audio::AudioStreamInfo input_stream_info,
|
void MixerSpeaker::copy_frames(const int16_t *input_buffer, audio::AudioStreamInfo input_stream_info,
|
||||||
int16_t *output_buffer, audio::AudioStreamInfo output_stream_info,
|
int16_t *output_buffer, audio::AudioStreamInfo output_stream_info,
|
||||||
@@ -472,32 +644,34 @@ void MixerSpeaker::mix_audio_samples(const int16_t *primary_buffer, audio::Audio
|
|||||||
}
|
}
|
||||||
|
|
||||||
void MixerSpeaker::audio_mixer_task(void *params) {
|
void MixerSpeaker::audio_mixer_task(void *params) {
|
||||||
MixerSpeaker *this_mixer = (MixerSpeaker *) params;
|
MixerSpeaker *this_mixer = static_cast<MixerSpeaker *>(params);
|
||||||
|
|
||||||
xEventGroupSetBits(this_mixer->event_group_, MixerEventGroupBits::STATE_STARTING);
|
xEventGroupSetBits(this_mixer->event_group_, MIXER_TASK_STATE_STARTING);
|
||||||
|
|
||||||
this_mixer->task_created_ = true;
|
|
||||||
|
|
||||||
std::unique_ptr<audio::AudioSinkTransferBuffer> output_transfer_buffer = audio::AudioSinkTransferBuffer::create(
|
std::unique_ptr<audio::AudioSinkTransferBuffer> output_transfer_buffer = audio::AudioSinkTransferBuffer::create(
|
||||||
this_mixer->audio_stream_info_.value().ms_to_bytes(TRANSFER_BUFFER_DURATION_MS));
|
this_mixer->audio_stream_info_.value().ms_to_bytes(TRANSFER_BUFFER_DURATION_MS));
|
||||||
|
|
||||||
if (output_transfer_buffer == nullptr) {
|
if (output_transfer_buffer == nullptr) {
|
||||||
xEventGroupSetBits(this_mixer->event_group_,
|
xEventGroupSetBits(this_mixer->event_group_, MIXER_TASK_STATE_STOPPED | MIXER_TASK_ERR_ESP_NO_MEM);
|
||||||
MixerEventGroupBits::STATE_STOPPED | MixerEventGroupBits::ERR_ESP_NO_MEM);
|
|
||||||
|
|
||||||
this_mixer->task_created_ = false;
|
vTaskSuspend(nullptr); // Suspend this task indefinitely until the loop method deletes it
|
||||||
vTaskDelete(nullptr);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
output_transfer_buffer->set_sink(this_mixer->output_speaker_);
|
output_transfer_buffer->set_sink(this_mixer->output_speaker_);
|
||||||
|
|
||||||
xEventGroupSetBits(this_mixer->event_group_, MixerEventGroupBits::STATE_RUNNING);
|
xEventGroupSetBits(this_mixer->event_group_, MIXER_TASK_STATE_RUNNING);
|
||||||
|
|
||||||
bool sent_finished = false;
|
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) {
|
while (true) {
|
||||||
uint32_t event_group_bits = xEventGroupGetBits(this_mixer->event_group_);
|
uint32_t event_group_bits = xEventGroupGetBits(this_mixer->event_group_);
|
||||||
if (event_group_bits & MixerEventGroupBits::COMMAND_STOP) {
|
if (event_group_bits & MIXER_TASK_COMMAND_STOP) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -507,15 +681,20 @@ void MixerSpeaker::audio_mixer_task(void *params) {
|
|||||||
const uint32_t output_frames_free =
|
const uint32_t output_frames_free =
|
||||||
this_mixer->audio_stream_info_.value().bytes_to_frames(output_transfer_buffer->free());
|
this_mixer->audio_stream_info_.value().bytes_to_frames(output_transfer_buffer->free());
|
||||||
|
|
||||||
std::vector<SourceSpeaker *> speakers_with_data;
|
speakers_with_data.clear();
|
||||||
std::vector<std::shared_ptr<audio::AudioSourceTransferBuffer>> transfer_buffers_with_data;
|
transfer_buffers_with_data.clear();
|
||||||
|
|
||||||
for (auto &speaker : this_mixer->source_speakers_) {
|
for (auto &speaker : this_mixer->source_speakers_) {
|
||||||
if (speaker->get_transfer_buffer().use_count() > 0) {
|
if (speaker->is_running() && !speaker->get_pause_state()) {
|
||||||
|
// Speaker is running and not paused, so it possibly can provide audio data
|
||||||
std::shared_ptr<audio::AudioSourceTransferBuffer> transfer_buffer = speaker->get_transfer_buffer().lock();
|
std::shared_ptr<audio::AudioSourceTransferBuffer> transfer_buffer = speaker->get_transfer_buffer().lock();
|
||||||
speaker->process_data_from_source(0); // Transfers and ducks audio from source ring buffers
|
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
|
||||||
|
|
||||||
if ((transfer_buffer->available() > 0) && !speaker->get_pause_state()) {
|
if (transfer_buffer->available() > 0) {
|
||||||
// Store the locked transfer buffers in their own vector to avoid releasing ownership until after the loop
|
// 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);
|
transfer_buffers_with_data.push_back(transfer_buffer);
|
||||||
speakers_with_data.push_back(speaker);
|
speakers_with_data.push_back(speaker);
|
||||||
@@ -547,13 +726,21 @@ void MixerSpeaker::audio_mixer_task(void *params) {
|
|||||||
reinterpret_cast<int16_t *>(output_transfer_buffer->get_buffer_end()),
|
reinterpret_cast<int16_t *>(output_transfer_buffer->get_buffer_end()),
|
||||||
this_mixer->audio_stream_info_.value(), frames_to_mix);
|
this_mixer->audio_stream_info_.value(), frames_to_mix);
|
||||||
|
|
||||||
// Update source speaker buffer length
|
// Set playback delay for newly contributing source
|
||||||
transfer_buffers_with_data[0]->decrease_buffer_length(active_stream_info.frames_to_bytes(frames_to_mix));
|
if (!speakers_with_data[0]->has_contributed_.load(std::memory_order_acquire)) {
|
||||||
speakers_with_data[0]->pending_playback_frames_ += frames_to_mix;
|
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 output transfer buffer length
|
// Update source speaker pending frames
|
||||||
|
speakers_with_data[0]->pending_playback_frames_.fetch_add(frames_to_mix, std::memory_order_release);
|
||||||
|
transfer_buffers_with_data[0]->decrease_buffer_length(active_stream_info.frames_to_bytes(frames_to_mix));
|
||||||
|
|
||||||
|
// Update output transfer buffer length and pipeline frame count
|
||||||
output_transfer_buffer->increase_buffer_length(
|
output_transfer_buffer->increase_buffer_length(
|
||||||
this_mixer->audio_stream_info_.value().frames_to_bytes(frames_to_mix));
|
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 {
|
} else {
|
||||||
// Speaker's stream info doesn't match the output speaker's, so it's a new source speaker
|
// 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()) {
|
if (!this_mixer->output_speaker_->is_stopped()) {
|
||||||
@@ -568,6 +755,8 @@ void MixerSpeaker::audio_mixer_task(void *params) {
|
|||||||
active_stream_info.get_sample_rate());
|
active_stream_info.get_sample_rate());
|
||||||
this_mixer->output_speaker_->set_audio_stream_info(this_mixer->audio_stream_info_.value());
|
this_mixer->output_speaker_->set_audio_stream_info(this_mixer->audio_stream_info_.value());
|
||||||
this_mixer->output_speaker_->start();
|
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;
|
sent_finished = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -596,26 +785,39 @@ 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
|
// 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) {
|
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(
|
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]->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
|
// Update output transfer buffer length and pipeline frame count (once, not per source)
|
||||||
output_transfer_buffer->increase_buffer_length(
|
output_transfer_buffer->increase_buffer_length(
|
||||||
this_mixer->audio_stream_info_.value().frames_to_bytes(frames_to_mix));
|
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_, MixerEventGroupBits::STATE_STOPPING);
|
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);
|
||||||
|
|
||||||
output_transfer_buffer.reset();
|
output_transfer_buffer.reset();
|
||||||
|
|
||||||
xEventGroupSetBits(this_mixer->event_group_, MixerEventGroupBits::STATE_STOPPED);
|
xEventGroupSetBits(this_mixer->event_group_, MIXER_TASK_STATE_STOPPED);
|
||||||
this_mixer->task_created_ = false;
|
|
||||||
vTaskDelete(nullptr);
|
vTaskSuspend(nullptr); // Suspend this task indefinitely until the loop method deletes it
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace mixer_speaker
|
} // namespace mixer_speaker
|
||||||
|
|||||||
@@ -7,26 +7,31 @@
|
|||||||
#include "esphome/components/speaker/speaker.h"
|
#include "esphome/components/speaker/speaker.h"
|
||||||
|
|
||||||
#include "esphome/core/component.h"
|
#include "esphome/core/component.h"
|
||||||
|
#include "esphome/core/helpers.h"
|
||||||
|
|
||||||
#include <freertos/event_groups.h>
|
|
||||||
#include <freertos/FreeRTOS.h>
|
#include <freertos/FreeRTOS.h>
|
||||||
|
#include <freertos/event_groups.h>
|
||||||
|
|
||||||
|
#include <atomic>
|
||||||
|
|
||||||
namespace esphome {
|
namespace esphome {
|
||||||
namespace mixer_speaker {
|
namespace mixer_speaker {
|
||||||
|
|
||||||
/* Classes for mixing several source speaker audio streams and writing it to another speaker component.
|
/* Classes for mixing several source speaker audio streams and writing it to another speaker component.
|
||||||
* - Volume controls are passed through to the output speaker
|
* - 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.
|
* - Directly handles pausing at the SourceSpeaker level; pause state is not passed through to the output speaker.
|
||||||
* - Audio sent to the SourceSpeaker's must have 16 bits per sample.
|
* - Audio sent to the SourceSpeaker 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
|
* - 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.
|
* the number of channels required for the output speaker.
|
||||||
* - In queue mode, the audio sent to the SoureSpeakers can have different sample rates.
|
* - In queue mode, the audio sent to the SourceSpeakers can have different sample rates.
|
||||||
* - In non-queue mode, the audio sent to the SourceSpeakers must have the same 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.
|
* - SourceSpeaker has an internal ring buffer. It also allocates a shared_ptr for an AudioTranserBuffer object.
|
||||||
* - Audio Data Flow:
|
* - Audio Data Flow:
|
||||||
* - Audio data played on a SourceSpeaker first writes to its internal ring buffer.
|
* - 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 task temporarily takes shared ownership of each SourceSpeaker's AudioTransferBuffer.
|
||||||
* - MixerSpeaker calls SourceSpeaker's `process_data_from_source`, which tranfers audio from the SourceSpeaker's
|
* - MixerSpeaker calls SourceSpeaker's `process_data_from_source`, which transfers audio from the SourceSpeaker's
|
||||||
* ring buffer to its AudioTransferBuffer. Audio ducking is applied at this step.
|
* 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
|
* - In queue mode, MixerSpeaker prioritizes the earliest configured SourceSpeaker with audio data. Audio data is
|
||||||
* sent to the output speaker.
|
* sent to the output speaker.
|
||||||
@@ -63,13 +68,15 @@ class SourceSpeaker : public speaker::Speaker, public Component {
|
|||||||
bool get_pause_state() const override { return this->pause_state_; }
|
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.
|
/// @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.
|
/// @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.
|
/// @return Number of bytes transferred from the ring buffer.
|
||||||
size_t process_data_from_source(TickType_t ticks_to_wait);
|
size_t process_data_from_source(std::shared_ptr<audio::AudioSourceTransferBuffer> &transfer_buffer,
|
||||||
|
TickType_t ticks_to_wait);
|
||||||
|
|
||||||
/// @brief Sets the ducking level for the source speaker.
|
/// @brief Sets the ducking level for the source speaker.
|
||||||
/// @param decibel_reduction (uint8_t) The dB reduction level. For example, 0 is no change, 10 is a reduction by 10 dB
|
/// @param decibel_reduction 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
|
/// @param duration 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 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; }
|
void set_buffer_duration(uint32_t buffer_duration_ms) { this->buffer_duration_ms_ = buffer_duration_ms; }
|
||||||
@@ -81,14 +88,15 @@ class SourceSpeaker : public speaker::Speaker, public Component {
|
|||||||
protected:
|
protected:
|
||||||
friend class MixerSpeaker;
|
friend class MixerSpeaker;
|
||||||
esp_err_t start_();
|
esp_err_t start_();
|
||||||
void stop_();
|
void enter_stopping_state_();
|
||||||
|
void send_command_(uint32_t command_bit, bool wake_loop = false);
|
||||||
|
|
||||||
/// @brief Ducks audio samples by a specified amount. When changing the ducking amount, it can transition gradually
|
/// @brief Ducks audio samples by a specified amount. When changing the ducking amount, it can transition gradually
|
||||||
/// over a specified amount of samples.
|
/// over a specified amount of samples.
|
||||||
/// @param input_buffer buffer with audio samples to be ducked in place
|
/// @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 input_samples_to_duck number of samples to process in ``input_buffer``
|
||||||
/// @param current_ducking_db_reduction pointer to the current dB reduction
|
/// @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 the
|
/// @param ducking_transition_samples_remaining pointer to the total number of samples left before the
|
||||||
/// transition is finished
|
/// transition is finished
|
||||||
/// @param samples_per_ducking_step total number of samples per ducking step for the transition
|
/// @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
|
/// @param db_change_per_ducking_step the change in dB reduction per step
|
||||||
@@ -114,7 +122,12 @@ class SourceSpeaker : public speaker::Speaker, public Component {
|
|||||||
uint32_t ducking_transition_samples_remaining_{0};
|
uint32_t ducking_transition_samples_remaining_{0};
|
||||||
uint32_t samples_per_ducking_step_{0};
|
uint32_t samples_per_ducking_step_{0};
|
||||||
|
|
||||||
uint32_t pending_playback_frames_{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};
|
||||||
};
|
};
|
||||||
|
|
||||||
class MixerSpeaker : public Component {
|
class MixerSpeaker : public Component {
|
||||||
@@ -123,10 +136,11 @@ class MixerSpeaker : public Component {
|
|||||||
void setup() override;
|
void setup() override;
|
||||||
void loop() 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); }
|
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
|
/// @brief Starts the mixer task. Called by a source speaker giving the current audio stream information
|
||||||
/// @param stream_info The calling source speakers audio stream information
|
/// @param stream_info The calling source speaker's audio stream information
|
||||||
/// @return ESP_ERR_NOT_SUPPORTED if the incoming stream is incompatible due to unsupported bits per sample
|
/// @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_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
|
/// ESP_ERR_NO_MEM if there isn't enough memory for the task's stack
|
||||||
@@ -134,8 +148,6 @@ class MixerSpeaker : public Component {
|
|||||||
/// ESP_OK if the incoming stream is compatible and the mixer task starts
|
/// ESP_OK if the incoming stream is compatible and the mixer task starts
|
||||||
esp_err_t start(audio::AudioStreamInfo &stream_info);
|
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_channels(uint8_t output_channels) { this->output_channels_ = output_channels; }
|
||||||
void set_output_speaker(speaker::Speaker *speaker) { this->output_speaker_ = speaker; }
|
void set_output_speaker(speaker::Speaker *speaker) { this->output_speaker_ = speaker; }
|
||||||
void set_queue_mode(bool queue_mode) { this->queue_mode_ = queue_mode; }
|
void set_queue_mode(bool queue_mode) { this->queue_mode_ = queue_mode; }
|
||||||
@@ -143,6 +155,9 @@ class MixerSpeaker : public Component {
|
|||||||
|
|
||||||
speaker::Speaker *get_output_speaker() const { return this->output_speaker_; }
|
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:
|
protected:
|
||||||
/// @brief Copies audio frames from the input buffer to the output buffer taking into account the number of channels
|
/// @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
|
/// in each stream. If the output stream has more channels, the input samples are duplicated. If the output stream has
|
||||||
@@ -159,11 +174,11 @@ class MixerSpeaker : public Component {
|
|||||||
/// and secondary samples are duplicated or dropped as necessary to ensure the output stream has the configured number
|
/// 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
|
/// of channels. Output samples are clamped to the corresponding int16 min or max values if the mixed sample
|
||||||
/// overflows.
|
/// overflows.
|
||||||
/// @param primary_buffer (int16_t *) samples buffer for the primary stream
|
/// @param primary_buffer samples buffer for the primary stream
|
||||||
/// @param primary_stream_info stream info for the primary stream
|
/// @param primary_stream_info stream info for the primary stream
|
||||||
/// @param secondary_buffer (int16_t *) samples buffer for secondary stream
|
/// @param secondary_buffer samples buffer for secondary stream
|
||||||
/// @param secondary_stream_info stream info for the secondary stream
|
/// @param secondary_stream_info stream info for the secondary stream
|
||||||
/// @param output_buffer (int16_t *) buffer for the mixed samples
|
/// @param output_buffer buffer for the mixed samples
|
||||||
/// @param output_stream_info stream info for the output buffer
|
/// @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
|
/// @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,
|
static void mix_audio_samples(const int16_t *primary_buffer, audio::AudioStreamInfo primary_stream_info,
|
||||||
@@ -185,20 +200,20 @@ class MixerSpeaker : public Component {
|
|||||||
|
|
||||||
EventGroupHandle_t event_group_{nullptr};
|
EventGroupHandle_t event_group_{nullptr};
|
||||||
|
|
||||||
std::vector<SourceSpeaker *> source_speakers_;
|
FixedVector<SourceSpeaker *> source_speakers_;
|
||||||
speaker::Speaker *output_speaker_{nullptr};
|
speaker::Speaker *output_speaker_{nullptr};
|
||||||
|
|
||||||
uint8_t output_channels_;
|
uint8_t output_channels_;
|
||||||
bool queue_mode_;
|
bool queue_mode_;
|
||||||
bool task_stack_in_psram_{false};
|
bool task_stack_in_psram_{false};
|
||||||
|
|
||||||
bool task_created_{false};
|
|
||||||
|
|
||||||
TaskHandle_t task_handle_{nullptr};
|
TaskHandle_t task_handle_{nullptr};
|
||||||
StaticTask_t task_stack_;
|
StaticTask_t task_stack_;
|
||||||
StackType_t *task_stack_buffer_{nullptr};
|
StackType_t *task_stack_buffer_{nullptr};
|
||||||
|
|
||||||
optional<audio::AudioStreamInfo> audio_stream_info_;
|
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
|
} // namespace mixer_speaker
|
||||||
|
|||||||
@@ -19,16 +19,25 @@ void Modbus::setup() {
|
|||||||
void Modbus::loop() {
|
void Modbus::loop() {
|
||||||
const uint32_t now = App.get_loop_component_start_time();
|
const uint32_t now = App.get_loop_component_start_time();
|
||||||
|
|
||||||
while (this->available()) {
|
// Read all available bytes in batches to reduce UART call overhead.
|
||||||
uint8_t byte;
|
int avail = this->available();
|
||||||
this->read_byte(&byte);
|
uint8_t buf[64];
|
||||||
if (this->parse_modbus_byte_(byte)) {
|
while (avail > 0) {
|
||||||
this->last_modbus_byte_ = now;
|
size_t to_read = std::min(static_cast<size_t>(avail), sizeof(buf));
|
||||||
} else {
|
if (!this->read_array(buf, to_read)) {
|
||||||
size_t at = this->rx_buffer_.size();
|
break;
|
||||||
if (at > 0) {
|
}
|
||||||
ESP_LOGV(TAG, "Clearing buffer of %d bytes - parse failed", at);
|
avail -= to_read;
|
||||||
this->rx_buffer_.clear();
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,31 +13,12 @@ static const char *const TAG = "mqtt.alarm_control_panel";
|
|||||||
|
|
||||||
using namespace esphome::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) {
|
static ProgmemStr alarm_state_to_mqtt_str(AlarmControlPanelState state) {
|
||||||
switch (state) {
|
return AlarmMqttStateStrings::get_progmem_str(static_cast<uint8_t>(state), AlarmMqttStateStrings::LAST_INDEX);
|
||||||
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)
|
MQTTAlarmControlPanelComponent::MQTTAlarmControlPanelComponent(AlarmControlPanel *alarm_control_panel)
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
#include "esphome/core/entity_base.h"
|
#include "esphome/core/entity_base.h"
|
||||||
#include "esphome/core/helpers.h"
|
#include "esphome/core/helpers.h"
|
||||||
#include "esphome/core/log.h"
|
#include "esphome/core/log.h"
|
||||||
|
#include "esphome/core/progmem.h"
|
||||||
#include "esphome/core/version.h"
|
#include "esphome/core/version.h"
|
||||||
#ifdef USE_LOGGER
|
#ifdef USE_LOGGER
|
||||||
#include "esphome/components/logger/logger.h"
|
#include "esphome/components/logger/logger.h"
|
||||||
@@ -27,6 +28,11 @@ namespace esphome::mqtt {
|
|||||||
|
|
||||||
static const char *const TAG = "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() {
|
MQTTClientComponent::MQTTClientComponent() {
|
||||||
global_mqtt_client = this;
|
global_mqtt_client = this;
|
||||||
char mac_addr[MAC_ADDRESS_BUFFER_SIZE];
|
char mac_addr[MAC_ADDRESS_BUFFER_SIZE];
|
||||||
@@ -348,36 +354,8 @@ void MQTTClientComponent::loop() {
|
|||||||
mqtt_backend_.loop();
|
mqtt_backend_.loop();
|
||||||
|
|
||||||
if (this->disconnect_reason_.has_value()) {
|
if (this->disconnect_reason_.has_value()) {
|
||||||
const LogString *reason_s;
|
const LogString *reason_s = MQTTDisconnectReasonStrings::get_log_str(
|
||||||
switch (*this->disconnect_reason_) {
|
static_cast<uint8_t>(*this->disconnect_reason_), MQTTDisconnectReasonStrings::LAST_INDEX);
|
||||||
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()) {
|
if (!network::is_connected()) {
|
||||||
reason_s = LOG_STR("WiFi disconnected");
|
reason_s = LOG_STR("WiFi disconnected");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,109 +13,44 @@ static const char *const TAG = "mqtt.climate";
|
|||||||
|
|
||||||
using namespace esphome::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) {
|
static ProgmemStr climate_mode_to_mqtt_str(ClimateMode mode) {
|
||||||
switch (mode) {
|
return ClimateMqttModeStrings::get_progmem_str(static_cast<uint8_t>(mode), ClimateMqttModeStrings::LAST_INDEX);
|
||||||
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) {
|
static ProgmemStr climate_action_to_mqtt_str(ClimateAction action) {
|
||||||
switch (action) {
|
return ClimateMqttActionStrings::get_progmem_str(static_cast<uint8_t>(action), ClimateMqttActionStrings::LAST_INDEX);
|
||||||
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) {
|
static ProgmemStr climate_fan_mode_to_mqtt_str(ClimateFanMode fan_mode) {
|
||||||
switch (fan_mode) {
|
return ClimateMqttFanModeStrings::get_progmem_str(static_cast<uint8_t>(fan_mode),
|
||||||
case CLIMATE_FAN_ON:
|
ClimateMqttFanModeStrings::LAST_INDEX);
|
||||||
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) {
|
static ProgmemStr climate_swing_mode_to_mqtt_str(ClimateSwingMode swing_mode) {
|
||||||
switch (swing_mode) {
|
return ClimateMqttSwingModeStrings::get_progmem_str(static_cast<uint8_t>(swing_mode),
|
||||||
case CLIMATE_SWING_OFF:
|
ClimateMqttSwingModeStrings::LAST_INDEX);
|
||||||
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) {
|
static ProgmemStr climate_preset_to_mqtt_str(ClimatePreset preset) {
|
||||||
switch (preset) {
|
return ClimateMqttPresetStrings::get_progmem_str(static_cast<uint8_t>(preset), ClimateMqttPresetStrings::LAST_INDEX);
|
||||||
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) {
|
void MQTTClimateComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) {
|
||||||
|
|||||||
@@ -14,6 +14,9 @@ namespace esphome::mqtt {
|
|||||||
|
|
||||||
static const char *const TAG = "mqtt.component";
|
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
|
// Helper functions for building topic strings on stack
|
||||||
inline char *append_str(char *p, const char *s, size_t len) {
|
inline char *append_str(char *p, const char *s, size_t len) {
|
||||||
memcpy(p, s, len);
|
memcpy(p, s, len);
|
||||||
@@ -31,10 +34,7 @@ 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.
|
// 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.
|
// 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.
|
// This ensures the stack buffers below are always large enough.
|
||||||
static constexpr size_t DISCOVERY_PREFIX_MAX_LEN = 64; // Validated in Python: cv.Length(max=64)
|
// MQTT_DISCOVERY_PREFIX_MAX_LEN and MQTT_DISCOVERY_TOPIC_MAX_LEN are defined in mqtt_component.h
|
||||||
// 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
|
// 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) {
|
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; }
|
void MQTTComponent::set_retain(bool retain) { this->retain_ = retain; }
|
||||||
|
|
||||||
std::string MQTTComponent::get_discovery_topic_(const MQTTDiscoveryInfo &discovery_info) const {
|
StringRef MQTTComponent::get_discovery_topic_to_(std::span<char, MQTT_DISCOVERY_TOPIC_MAX_LEN> buf,
|
||||||
|
const MQTTDiscoveryInfo &discovery_info) const {
|
||||||
char sanitized_name[ESPHOME_DEVICE_NAME_MAX_LEN + 1];
|
char sanitized_name[ESPHOME_DEVICE_NAME_MAX_LEN + 1];
|
||||||
str_sanitize_to(sanitized_name, App.get_name().c_str());
|
str_sanitize_to(sanitized_name, App.get_name().c_str());
|
||||||
const char *comp_type = this->component_type();
|
const char *comp_type = this->component_type();
|
||||||
char object_id_buf[OBJECT_ID_MAX_LEN];
|
char object_id_buf[OBJECT_ID_MAX_LEN];
|
||||||
StringRef object_id = this->get_default_object_id_to_(object_id_buf);
|
StringRef object_id = this->get_default_object_id_to_(object_id_buf);
|
||||||
|
|
||||||
char buf[DISCOVERY_TOPIC_MAX_LEN];
|
char *p = buf.data();
|
||||||
char *p = buf;
|
|
||||||
|
|
||||||
p = append_str(p, discovery_info.prefix.data(), discovery_info.prefix.size());
|
p = append_str(p, discovery_info.prefix.data(), discovery_info.prefix.size());
|
||||||
p = append_char(p, '/');
|
p = append_char(p, '/');
|
||||||
@@ -69,8 +69,9 @@ std::string MQTTComponent::get_discovery_topic_(const MQTTDiscoveryInfo &discove
|
|||||||
p = append_char(p, '/');
|
p = append_char(p, '/');
|
||||||
p = append_str(p, object_id.c_str(), object_id.size());
|
p = append_str(p, object_id.c_str(), object_id.size());
|
||||||
p = append_str(p, "/config", 7);
|
p = append_str(p, "/config", 7);
|
||||||
|
*p = '\0';
|
||||||
|
|
||||||
return std::string(buf, p - buf);
|
return StringRef(buf.data(), p - buf.data());
|
||||||
}
|
}
|
||||||
|
|
||||||
StringRef MQTTComponent::get_default_topic_for_to_(std::span<char, MQTT_DEFAULT_TOPIC_MAX_LEN> buf, const char *suffix,
|
StringRef MQTTComponent::get_default_topic_for_to_(std::span<char, MQTT_DEFAULT_TOPIC_MAX_LEN> buf, const char *suffix,
|
||||||
@@ -179,16 +180,19 @@ bool MQTTComponent::publish_json(const char *topic, const json::json_build_t &f)
|
|||||||
bool MQTTComponent::send_discovery_() {
|
bool MQTTComponent::send_discovery_() {
|
||||||
const MQTTDiscoveryInfo &discovery_info = global_mqtt_client->get_discovery_info();
|
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) {
|
if (discovery_info.clean) {
|
||||||
ESP_LOGV(TAG, "'%s': Cleaning discovery", this->friendly_name_().c_str());
|
ESP_LOGV(TAG, "'%s': Cleaning discovery", this->friendly_name_().c_str());
|
||||||
return global_mqtt_client->publish(this->get_discovery_topic_(discovery_info), "", 0, this->qos_, true);
|
return global_mqtt_client->publish(discovery_topic.c_str(), "", 0, this->qos_, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
ESP_LOGV(TAG, "'%s': Sending discovery", this->friendly_name_().c_str());
|
ESP_LOGV(TAG, "'%s': Sending discovery", this->friendly_name_().c_str());
|
||||||
|
|
||||||
// NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
|
// NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
|
||||||
return global_mqtt_client->publish_json(
|
return global_mqtt_client->publish_json(
|
||||||
this->get_discovery_topic_(discovery_info),
|
discovery_topic.c_str(),
|
||||||
[this](JsonObject root) {
|
[this](JsonObject root) {
|
||||||
SendDiscoveryConfig config;
|
SendDiscoveryConfig config;
|
||||||
config.state_topic = true;
|
config.state_topic = true;
|
||||||
@@ -201,7 +205,7 @@ bool MQTTComponent::send_discovery_() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Fields from EntityBase
|
// Fields from EntityBase
|
||||||
root[MQTT_NAME] = this->get_entity()->has_own_name() ? this->friendly_name_() : "";
|
root[MQTT_NAME] = this->get_entity()->has_own_name() ? this->friendly_name_() : StringRef();
|
||||||
|
|
||||||
if (this->is_disabled_by_default_())
|
if (this->is_disabled_by_default_())
|
||||||
root[MQTT_ENABLED_BY_DEFAULT] = false;
|
root[MQTT_ENABLED_BY_DEFAULT] = false;
|
||||||
@@ -213,13 +217,9 @@ bool MQTTComponent::send_discovery_() {
|
|||||||
// NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks)
|
// NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks)
|
||||||
|
|
||||||
const auto entity_category = this->get_entity()->get_entity_category();
|
const auto entity_category = this->get_entity()->get_entity_category();
|
||||||
switch (entity_category) {
|
if (entity_category != ENTITY_CATEGORY_NONE) {
|
||||||
case ENTITY_CATEGORY_NONE:
|
root[MQTT_ENTITY_CATEGORY] = EntityCategoryMqttStrings::get_progmem_str(
|
||||||
break;
|
static_cast<uint8_t>(entity_category), static_cast<uint8_t>(ENTITY_CATEGORY_CONFIG));
|
||||||
case ENTITY_CATEGORY_CONFIG:
|
|
||||||
case ENTITY_CATEGORY_DIAGNOSTIC:
|
|
||||||
root[MQTT_ENTITY_CATEGORY] = entity_category == ENTITY_CATEGORY_CONFIG ? "config" : "diagnostic";
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (config.state_topic) {
|
if (config.state_topic) {
|
||||||
@@ -249,7 +249,7 @@ bool MQTTComponent::send_discovery_() {
|
|||||||
if (discovery_info.unique_id_generator == MQTT_MAC_ADDRESS_UNIQUE_ID_GENERATOR) {
|
if (discovery_info.unique_id_generator == MQTT_MAC_ADDRESS_UNIQUE_ID_GENERATOR) {
|
||||||
char friendly_name_hash[9];
|
char friendly_name_hash[9];
|
||||||
buf_append_printf(friendly_name_hash, sizeof(friendly_name_hash), 0, "%08" PRIx32,
|
buf_append_printf(friendly_name_hash, sizeof(friendly_name_hash), 0, "%08" PRIx32,
|
||||||
fnv1_hash(this->friendly_name_()));
|
fnv1_hash(this->friendly_name_().c_str()));
|
||||||
// Format: mac-component_type-hash (e.g. "aabbccddeeff-sensor-12345678")
|
// Format: mac-component_type-hash (e.g. "aabbccddeeff-sensor-12345678")
|
||||||
// MAC (12) + "-" (1) + domain (max 20) + "-" (1) + hash (8) + null (1) = 43
|
// MAC (12) + "-" (1) + domain (max 20) + "-" (1) + hash (8) + null (1) = 43
|
||||||
char unique_id[MAC_ADDRESS_BUFFER_SIZE + ESPHOME_DOMAIN_MAX_LEN + 11];
|
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(); }
|
bool MQTTComponent::is_connected_() const { return global_mqtt_client->is_connected(); }
|
||||||
|
|
||||||
// Pull these properties from EntityBase if not overridden
|
// Pull these properties from EntityBase if not overridden
|
||||||
std::string MQTTComponent::friendly_name_() const { return this->get_entity()->get_name(); }
|
const StringRef &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 {
|
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);
|
return this->get_entity()->get_object_id_to(buf);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,6 +32,10 @@ static constexpr size_t MQTT_TOPIC_PREFIX_MAX_LEN = 64; // Validated in Python:
|
|||||||
// Format: prefix + "/" + type + "/" + object_id + "/" + suffix + null
|
// Format: prefix + "/" + type + "/" + object_id + "/" + suffix + null
|
||||||
static constexpr size_t MQTT_DEFAULT_TOPIC_MAX_LEN =
|
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;
|
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
|
class MQTTComponent; // Forward declaration
|
||||||
void log_mqtt_component(const char *tag, MQTTComponent *obj, bool state_topic, bool command_topic);
|
void log_mqtt_component(const char *tag, MQTTComponent *obj, bool state_topic, bool command_topic);
|
||||||
@@ -263,8 +267,9 @@ class MQTTComponent : public Component {
|
|||||||
void subscribe_json(const std::string &topic, const mqtt_json_callback_t &callback, uint8_t qos = 0);
|
void subscribe_json(const std::string &topic, const mqtt_json_callback_t &callback, uint8_t qos = 0);
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
/// Helper method to get the discovery topic for this component.
|
/// Helper method to get the discovery topic for this component into a buffer.
|
||||||
std::string get_discovery_topic_(const MQTTDiscoveryInfo &discovery_info) const;
|
StringRef get_discovery_topic_to_(std::span<char, MQTT_DISCOVERY_TOPIC_MAX_LEN> buf,
|
||||||
|
const MQTTDiscoveryInfo &discovery_info) const;
|
||||||
|
|
||||||
/** Get this components state/command/... topic into a buffer.
|
/** Get this components state/command/... topic into a buffer.
|
||||||
*
|
*
|
||||||
@@ -288,7 +293,7 @@ class MQTTComponent : public Component {
|
|||||||
virtual const EntityBase *get_entity() const = 0;
|
virtual const EntityBase *get_entity() const = 0;
|
||||||
|
|
||||||
/// Get the friendly name of this MQTT component.
|
/// Get the friendly name of this MQTT component.
|
||||||
std::string friendly_name_() const;
|
const StringRef &friendly_name_() const;
|
||||||
|
|
||||||
/// Get the icon field of this component as StringRef
|
/// Get the icon field of this component as StringRef
|
||||||
StringRef get_icon_ref_() const;
|
StringRef get_icon_ref_() const;
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
#include "mqtt_number.h"
|
#include "mqtt_number.h"
|
||||||
#include "esphome/core/log.h"
|
#include "esphome/core/log.h"
|
||||||
|
#include "esphome/core/progmem.h"
|
||||||
|
|
||||||
#include "mqtt_const.h"
|
#include "mqtt_const.h"
|
||||||
|
|
||||||
@@ -12,6 +13,9 @@ static const char *const TAG = "mqtt.number";
|
|||||||
|
|
||||||
using namespace esphome::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) {}
|
MQTTNumberComponent::MQTTNumberComponent(Number *number) : number_(number) {}
|
||||||
|
|
||||||
void MQTTNumberComponent::setup() {
|
void MQTTNumberComponent::setup() {
|
||||||
@@ -48,15 +52,10 @@ void MQTTNumberComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryCon
|
|||||||
if (!unit_of_measurement.empty()) {
|
if (!unit_of_measurement.empty()) {
|
||||||
root[MQTT_UNIT_OF_MEASUREMENT] = unit_of_measurement;
|
root[MQTT_UNIT_OF_MEASUREMENT] = unit_of_measurement;
|
||||||
}
|
}
|
||||||
switch (this->number_->traits.get_mode()) {
|
const auto mode = this->number_->traits.get_mode();
|
||||||
case NUMBER_MODE_AUTO:
|
if (mode != NUMBER_MODE_AUTO) {
|
||||||
break;
|
root[MQTT_MODE] =
|
||||||
case NUMBER_MODE_BOX:
|
NumberMqttModeStrings::get_progmem_str(static_cast<uint8_t>(mode), static_cast<uint8_t>(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();
|
const auto device_class = this->number_->traits.get_device_class_ref();
|
||||||
if (!device_class.empty()) {
|
if (!device_class.empty()) {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
#include "mqtt_text.h"
|
#include "mqtt_text.h"
|
||||||
#include "esphome/core/log.h"
|
#include "esphome/core/log.h"
|
||||||
|
#include "esphome/core/progmem.h"
|
||||||
|
|
||||||
#include "mqtt_const.h"
|
#include "mqtt_const.h"
|
||||||
|
|
||||||
@@ -12,6 +13,9 @@ static const char *const TAG = "mqtt.text";
|
|||||||
|
|
||||||
using namespace esphome::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) {}
|
MQTTTextComponent::MQTTTextComponent(Text *text) : text_(text) {}
|
||||||
|
|
||||||
void MQTTTextComponent::setup() {
|
void MQTTTextComponent::setup() {
|
||||||
@@ -34,14 +38,8 @@ const EntityBase *MQTTTextComponent::get_entity() const { return this->text_; }
|
|||||||
|
|
||||||
void MQTTTextComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) {
|
void MQTTTextComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) {
|
||||||
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
|
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
|
||||||
switch (this->text_->traits.get_mode()) {
|
root[MQTT_MODE] = TextMqttModeStrings::get_progmem_str(static_cast<uint8_t>(this->text_->traits.get_mode()),
|
||||||
case TEXT_MODE_TEXT:
|
static_cast<uint8_t>(TEXT_MODE_TEXT));
|
||||||
root[MQTT_MODE] = "text";
|
|
||||||
break;
|
|
||||||
case TEXT_MODE_PASSWORD:
|
|
||||||
root[MQTT_MODE] = "password";
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
config.command_topic = true;
|
config.command_topic = true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -72,53 +72,55 @@ void MS8607Component::setup() {
|
|||||||
|
|
||||||
// I do not know why the device sometimes NACKs the reset command, but
|
// 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
|
// try 3 times in case it's a transitory issue on this boot
|
||||||
this->set_retry(
|
// Backoff: executes at now, +5ms, +30ms
|
||||||
"reset", 5, 3,
|
this->reset_attempts_remaining_ = 3;
|
||||||
[this](const uint8_t remaining_setup_attempts) {
|
this->reset_interval_ = 5;
|
||||||
ESP_LOGD(TAG, "Resetting both I2C addresses: 0x%02X, 0x%02X", this->address_,
|
this->try_reset_();
|
||||||
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)) {
|
void MS8607Component::try_reset_() {
|
||||||
ESP_LOGE(TAG, "Resetting I2C devices failed");
|
ESP_LOGD(TAG, "Resetting both I2C addresses: 0x%02X, 0x%02X", this->address_, this->humidity_device_->get_address());
|
||||||
if (!pt_successful && !h_successful) {
|
// I believe sending the reset command to both addresses is preferable to
|
||||||
this->error_code_ = ErrorCode::PTH_RESET_FAILED;
|
// skipping humidity if PT fails for some reason.
|
||||||
} else if (!pt_successful) {
|
// However, only consider the reset successful if they both ACK
|
||||||
this->error_code_ = ErrorCode::PT_RESET_FAILED;
|
bool const pt_successful = this->write_bytes(MS8607_PT_CMD_RESET, nullptr, 0);
|
||||||
} else {
|
bool const h_successful = this->humidity_device_->write_bytes(MS8607_CMD_H_RESET, nullptr, 0);
|
||||||
this->error_code_ = ErrorCode::H_RESET_FAILED;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (remaining_setup_attempts > 0) {
|
if (!(pt_successful && h_successful)) {
|
||||||
this->status_set_error();
|
ESP_LOGE(TAG, "Resetting I2C devices failed");
|
||||||
} else {
|
if (!pt_successful && !h_successful) {
|
||||||
this->mark_failed();
|
this->error_code_ = ErrorCode::PTH_RESET_FAILED;
|
||||||
}
|
} else if (!pt_successful) {
|
||||||
return RetryResult::RETRY;
|
this->error_code_ = ErrorCode::PT_RESET_FAILED;
|
||||||
}
|
} else {
|
||||||
|
this->error_code_ = ErrorCode::H_RESET_FAILED;
|
||||||
|
}
|
||||||
|
|
||||||
this->setup_status_ = SetupStatus::NEEDS_PROM_READ;
|
if (--this->reset_attempts_remaining_ > 0) {
|
||||||
this->error_code_ = ErrorCode::NONE;
|
uint32_t delay = this->reset_interval_;
|
||||||
this->status_clear_error();
|
this->reset_interval_ *= 5;
|
||||||
|
this->set_timeout("reset", delay, [this]() { this->try_reset_(); });
|
||||||
|
this->status_set_error();
|
||||||
|
} else {
|
||||||
|
this->mark_failed();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// 15ms delay matches datasheet, Adafruit_MS8607 & SparkFun_PHT_MS8607_Arduino_Library
|
this->setup_status_ = SetupStatus::NEEDS_PROM_READ;
|
||||||
this->set_timeout("prom-read", 15, [this]() {
|
this->error_code_ = ErrorCode::NONE;
|
||||||
if (this->read_calibration_values_from_prom_()) {
|
this->status_clear_error();
|
||||||
this->setup_status_ = SetupStatus::SUCCESSFUL;
|
|
||||||
this->status_clear_error();
|
|
||||||
} else {
|
|
||||||
this->mark_failed();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return RetryResult::DONE;
|
// 15ms delay matches datasheet, Adafruit_MS8607 & SparkFun_PHT_MS8607_Arduino_Library
|
||||||
},
|
this->set_timeout("prom-read", 15, [this]() {
|
||||||
5.0f); // executes at now, +5ms, +25ms
|
if (this->read_calibration_values_from_prom_()) {
|
||||||
|
this->setup_status_ = SetupStatus::SUCCESSFUL;
|
||||||
|
this->status_clear_error();
|
||||||
|
} else {
|
||||||
|
this->mark_failed();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void MS8607Component::update() {
|
void MS8607Component::update() {
|
||||||
|
|||||||
@@ -44,6 +44,8 @@ class MS8607Component : public PollingComponent, public i2c::I2CDevice {
|
|||||||
void set_humidity_device(MS8607HumidityDevice *humidity_device) { humidity_device_ = humidity_device; }
|
void set_humidity_device(MS8607HumidityDevice *humidity_device) { humidity_device_ = humidity_device; }
|
||||||
|
|
||||||
protected:
|
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.
|
Read and store the Pressure & Temperature calibration settings from the PROM.
|
||||||
Intended to be called during setup(), this will set the `failure_reason_`
|
Intended to be called during setup(), this will set the `failure_reason_`
|
||||||
@@ -102,6 +104,8 @@ class MS8607Component : public PollingComponent, public i2c::I2CDevice {
|
|||||||
enum class SetupStatus;
|
enum class SetupStatus;
|
||||||
/// Current step in the multi-step & possibly delayed setup() process
|
/// Current step in the multi-step & possibly delayed setup() process
|
||||||
SetupStatus setup_status_;
|
SetupStatus setup_status_;
|
||||||
|
uint32_t reset_interval_{5};
|
||||||
|
uint8_t reset_attempts_remaining_{0};
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace ms8607
|
} // namespace ms8607
|
||||||
|
|||||||
@@ -397,11 +397,17 @@ bool Nextion::remove_from_q_(bool report_empty) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void Nextion::process_serial_() {
|
void Nextion::process_serial_() {
|
||||||
uint8_t d;
|
// 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;
|
||||||
|
|
||||||
while (this->available()) {
|
this->command_data_.append(reinterpret_cast<const char *>(buf), to_read);
|
||||||
read_byte(&d);
|
|
||||||
this->command_data_ += d;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// nextion.tech/instruction-set/
|
// nextion.tech/instruction-set/
|
||||||
|
|||||||
@@ -396,9 +396,9 @@ static bool process_rolling_code(Provider &provider, PacketDecoder &decoder) {
|
|||||||
/**
|
/**
|
||||||
* Process a received packet
|
* Process a received packet
|
||||||
*/
|
*/
|
||||||
void PacketTransport::process_(const std::vector<uint8_t> &data) {
|
void PacketTransport::process_(std::span<const uint8_t> data) {
|
||||||
auto ping_key_seen = !this->ping_pong_enable_;
|
auto ping_key_seen = !this->ping_pong_enable_;
|
||||||
PacketDecoder decoder((data.data()), data.size());
|
PacketDecoder decoder(data.data(), data.size());
|
||||||
char namebuf[256]{};
|
char namebuf[256]{};
|
||||||
uint8_t byte;
|
uint8_t byte;
|
||||||
FuData rdata{};
|
FuData rdata{};
|
||||||
|
|||||||
@@ -9,8 +9,9 @@
|
|||||||
#include "esphome/components/binary_sensor/binary_sensor.h"
|
#include "esphome/components/binary_sensor/binary_sensor.h"
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#include <vector>
|
|
||||||
#include <map>
|
#include <map>
|
||||||
|
#include <span>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Providing packet encoding functions for exchanging data with a remote host.
|
* Providing packet encoding functions for exchanging data with a remote host.
|
||||||
@@ -113,7 +114,7 @@ class PacketTransport : public PollingComponent {
|
|||||||
virtual bool should_send() { return true; }
|
virtual bool should_send() { return true; }
|
||||||
|
|
||||||
// to be called by child classes when a data packet is received.
|
// to be called by child classes when a data packet is received.
|
||||||
void process_(const std::vector<uint8_t> &data);
|
void process_(std::span<const uint8_t> data);
|
||||||
void send_data_(bool all);
|
void send_data_(bool all);
|
||||||
void flush_();
|
void flush_();
|
||||||
void add_data_(uint8_t key, const char *id, float data);
|
void add_data_(uint8_t key, const char *id, float data);
|
||||||
|
|||||||
@@ -13,9 +13,12 @@ void Pipsolar::setup() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void Pipsolar::empty_uart_buffer_() {
|
void Pipsolar::empty_uart_buffer_() {
|
||||||
uint8_t byte;
|
uint8_t buf[64];
|
||||||
while (this->available()) {
|
int avail;
|
||||||
this->read_byte(&byte);
|
while ((avail = this->available()) > 0) {
|
||||||
|
if (!this->read_array(buf, std::min(static_cast<size_t>(avail), sizeof(buf)))) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -94,32 +97,47 @@ void Pipsolar::loop() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (this->state_ == STATE_COMMAND || this->state_ == STATE_POLL) {
|
if (this->state_ == STATE_COMMAND || this->state_ == STATE_POLL) {
|
||||||
while (this->available()) {
|
int avail = this->available();
|
||||||
uint8_t byte;
|
while (avail > 0) {
|
||||||
this->read_byte(&byte);
|
uint8_t buf[64];
|
||||||
|
size_t to_read = std::min(static_cast<size_t>(avail), sizeof(buf));
|
||||||
// make sure data and null terminator fit in buffer
|
if (!this->read_array(buf, to_read)) {
|
||||||
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;
|
break;
|
||||||
}
|
}
|
||||||
this->read_buffer_[this->read_pos_] = byte;
|
avail -= to_read;
|
||||||
this->read_pos_++;
|
bool done = false;
|
||||||
|
for (size_t i = 0; i < to_read; i++) {
|
||||||
|
uint8_t byte = buf[i];
|
||||||
|
|
||||||
// end of answer
|
// make sure data and null terminator fit in buffer
|
||||||
if (byte == 0x0D) {
|
if (this->read_pos_ >= PIPSOLAR_READ_BUFFER_LENGTH - 1) {
|
||||||
this->read_buffer_[this->read_pos_] = 0;
|
this->read_pos_ = 0;
|
||||||
this->empty_uart_buffer_();
|
this->empty_uart_buffer_();
|
||||||
if (this->state_ == STATE_POLL) {
|
ESP_LOGW(TAG, "response data too long, discarding.");
|
||||||
this->state_ = STATE_POLL_COMPLETE;
|
done = true;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
if (this->state_ == STATE_COMMAND) {
|
this->read_buffer_[this->read_pos_] = byte;
|
||||||
this->state_ = STATE_COMMAND_COMPLETE;
|
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} // available
|
if (done) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (this->state_ == STATE_COMMAND) {
|
if (this->state_ == STATE_COMMAND) {
|
||||||
if (millis() - this->command_start_millis_ > esphome::pipsolar::Pipsolar::COMMAND_TIMEOUT) {
|
if (millis() - this->command_start_millis_ > esphome::pipsolar::Pipsolar::COMMAND_TIMEOUT) {
|
||||||
|
|||||||
@@ -56,17 +56,23 @@ void PylontechComponent::setup() {
|
|||||||
void PylontechComponent::update() { this->write_str("pwr\n"); }
|
void PylontechComponent::update() { this->write_str("pwr\n"); }
|
||||||
|
|
||||||
void PylontechComponent::loop() {
|
void PylontechComponent::loop() {
|
||||||
if (this->available() > 0) {
|
int avail = this->available();
|
||||||
|
if (avail > 0) {
|
||||||
// pylontech sends a lot of data very suddenly
|
// 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
|
// we need to quickly put it all into our own buffer, otherwise the uart's buffer will overflow
|
||||||
uint8_t data;
|
|
||||||
int recv = 0;
|
int recv = 0;
|
||||||
while (this->available() > 0) {
|
uint8_t buf[64];
|
||||||
if (this->read_byte(&data)) {
|
while (avail > 0) {
|
||||||
buffer_[buffer_index_write_] += (char) data;
|
size_t to_read = std::min(static_cast<size_t>(avail), sizeof(buf));
|
||||||
recv++;
|
if (!this->read_array(buf, to_read)) {
|
||||||
if (buffer_[buffer_index_write_].back() == static_cast<char>(ASCII_LF) ||
|
break;
|
||||||
buffer_[buffer_index_write_].length() >= MAX_DATA_LENGTH_BYTES) {
|
}
|
||||||
|
avail -= to_read;
|
||||||
|
recv += to_read;
|
||||||
|
|
||||||
|
for (size_t i = 0; i < to_read; i++) {
|
||||||
|
buffer_[buffer_index_write_] += (char) buf[i];
|
||||||
|
if (buf[i] == ASCII_LF || buffer_[buffer_index_write_].length() >= MAX_DATA_LENGTH_BYTES) {
|
||||||
// complete line received
|
// complete line received
|
||||||
buffer_index_write_ = (buffer_index_write_ + 1) % NUM_BUFFERS;
|
buffer_index_write_ = (buffer_index_write_ + 1) % NUM_BUFFERS;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
#include "rd03d.h"
|
#include "rd03d.h"
|
||||||
|
#include "esphome/core/helpers.h"
|
||||||
#include "esphome/core/log.h"
|
#include "esphome/core/log.h"
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
|
|
||||||
@@ -80,37 +81,47 @@ void RD03DComponent::dump_config() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void RD03DComponent::loop() {
|
void RD03DComponent::loop() {
|
||||||
while (this->available()) {
|
// Read all available bytes in batches to reduce UART call overhead.
|
||||||
uint8_t byte = this->read();
|
int avail = this->available();
|
||||||
ESP_LOGVV(TAG, "Received byte: 0x%02X, buffer_pos: %d", byte, this->buffer_pos_);
|
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_);
|
||||||
|
|
||||||
// Check if we're looking for frame header
|
// Check if we're looking for frame header
|
||||||
if (this->buffer_pos_ < FRAME_HEADER_SIZE) {
|
if (this->buffer_pos_ < FRAME_HEADER_SIZE) {
|
||||||
if (byte == FRAME_HEADER[this->buffer_pos_]) {
|
if (byte == FRAME_HEADER[this->buffer_pos_]) {
|
||||||
this->buffer_[this->buffer_pos_++] = byte;
|
this->buffer_[this->buffer_pos_++] = byte;
|
||||||
} else if (byte == FRAME_HEADER[0]) {
|
} else if (byte == FRAME_HEADER[0]) {
|
||||||
// Start over if we see a potential new header
|
// Start over if we see a potential new header
|
||||||
this->buffer_[0] = byte;
|
this->buffer_[0] = byte;
|
||||||
this->buffer_pos_ = 1;
|
this->buffer_pos_ = 1;
|
||||||
} else {
|
} 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;
|
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,14 +136,21 @@ void RFBridgeComponent::loop() {
|
|||||||
this->last_bridge_byte_ = now;
|
this->last_bridge_byte_ = now;
|
||||||
}
|
}
|
||||||
|
|
||||||
while (this->available()) {
|
int avail = this->available();
|
||||||
uint8_t byte;
|
while (avail > 0) {
|
||||||
this->read_byte(&byte);
|
uint8_t buf[64];
|
||||||
if (this->parse_bridge_byte_(byte)) {
|
size_t to_read = std::min(static_cast<size_t>(avail), sizeof(buf));
|
||||||
ESP_LOGVV(TAG, "Parsed: 0x%02X", byte);
|
if (!this->read_array(buf, to_read)) {
|
||||||
this->last_bridge_byte_ = now;
|
break;
|
||||||
} else {
|
}
|
||||||
this->rx_buffer_.clear();
|
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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -106,12 +106,19 @@ void MR24HPC1Component::update_() {
|
|||||||
|
|
||||||
// main loop
|
// main loop
|
||||||
void MR24HPC1Component::loop() {
|
void MR24HPC1Component::loop() {
|
||||||
uint8_t byte;
|
// 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;
|
||||||
|
|
||||||
// Is there data on the serial port
|
for (size_t i = 0; i < to_read; i++) {
|
||||||
while (this->available()) {
|
this->r24_split_data_frame_(buf[i]); // split data frame
|
||||||
this->read_byte(&byte);
|
}
|
||||||
this->r24_split_data_frame_(byte); // split data frame
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((this->s_output_info_switch_flag_ == OUTPUT_SWTICH_OFF) &&
|
if ((this->s_output_info_switch_flag_ == OUTPUT_SWTICH_OFF) &&
|
||||||
|
|||||||
@@ -30,14 +30,21 @@ void MR60BHA2Component::dump_config() {
|
|||||||
|
|
||||||
// main loop
|
// main loop
|
||||||
void MR60BHA2Component::loop() {
|
void MR60BHA2Component::loop() {
|
||||||
uint8_t byte;
|
// 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;
|
||||||
|
|
||||||
// Is there data on the serial port
|
for (size_t i = 0; i < to_read; i++) {
|
||||||
while (this->available()) {
|
this->rx_message_.push_back(buf[i]);
|
||||||
this->read_byte(&byte);
|
if (!this->validate_message_()) {
|
||||||
this->rx_message_.push_back(byte);
|
this->rx_message_.clear();
|
||||||
if (!this->validate_message_()) {
|
}
|
||||||
this->rx_message_.clear();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,12 +49,19 @@ void MR60FDA2Component::setup() {
|
|||||||
|
|
||||||
// main loop
|
// main loop
|
||||||
void MR60FDA2Component::loop() {
|
void MR60FDA2Component::loop() {
|
||||||
uint8_t byte;
|
// 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;
|
||||||
|
|
||||||
// Is there data on the serial port
|
for (size_t i = 0; i < to_read; i++) {
|
||||||
while (this->available()) {
|
this->split_frame_(buf[i]); // split data frame
|
||||||
this->read_byte(&byte);
|
}
|
||||||
this->split_frame_(byte); // split data frame
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -103,6 +103,20 @@ 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_() {
|
void SpeakerMediaPlayer::watch_media_commands_() {
|
||||||
if (!this->is_ready()) {
|
if (!this->is_ready()) {
|
||||||
return;
|
return;
|
||||||
@@ -144,15 +158,7 @@ void SpeakerMediaPlayer::watch_media_commands_() {
|
|||||||
if (this->is_paused_) {
|
if (this->is_paused_) {
|
||||||
// If paused, stop the media pipeline and unpause it after confirming its stopped. This avoids playing a
|
// 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.
|
// short segment of the paused file before starting the new one.
|
||||||
this->media_pipeline_->stop();
|
this->stop_and_unpause_media_();
|
||||||
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 {
|
} else {
|
||||||
// Not paused, just directly start the file
|
// Not paused, just directly start the file
|
||||||
if (media_command.file.has_value()) {
|
if (media_command.file.has_value()) {
|
||||||
@@ -197,27 +203,21 @@ void SpeakerMediaPlayer::watch_media_commands_() {
|
|||||||
this->cancel_timeout("next_ann");
|
this->cancel_timeout("next_ann");
|
||||||
this->announcement_playlist_.clear();
|
this->announcement_playlist_.clear();
|
||||||
this->announcement_pipeline_->stop();
|
this->announcement_pipeline_->stop();
|
||||||
this->set_retry("unpause_ann", 50, 3, [this](const uint8_t remaining_attempts) {
|
this->unpause_announcement_remaining_ = 3;
|
||||||
|
this->set_interval("unpause_ann", 50, [this]() {
|
||||||
if (this->announcement_pipeline_state_ == AudioPipelineState::STOPPED) {
|
if (this->announcement_pipeline_state_ == AudioPipelineState::STOPPED) {
|
||||||
|
this->cancel_interval("unpause_ann");
|
||||||
this->announcement_pipeline_->set_pause_state(false);
|
this->announcement_pipeline_->set_pause_state(false);
|
||||||
return RetryResult::DONE;
|
} else if (--this->unpause_announcement_remaining_ == 0) {
|
||||||
|
this->cancel_interval("unpause_ann");
|
||||||
}
|
}
|
||||||
return RetryResult::RETRY;
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (this->media_pipeline_ != nullptr) {
|
if (this->media_pipeline_ != nullptr) {
|
||||||
this->cancel_timeout("next_media");
|
this->cancel_timeout("next_media");
|
||||||
this->media_playlist_.clear();
|
this->media_playlist_.clear();
|
||||||
this->media_pipeline_->stop();
|
this->stop_and_unpause_media_();
|
||||||
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,6 +112,9 @@ class SpeakerMediaPlayer : public Component,
|
|||||||
/// media pipelines are defined.
|
/// media pipelines are defined.
|
||||||
inline bool single_pipeline_() { return (this->media_speaker_ == nullptr); }
|
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_.
|
// Processes commands from media_control_command_queue_.
|
||||||
void watch_media_commands_();
|
void watch_media_commands_();
|
||||||
|
|
||||||
@@ -141,6 +144,8 @@ class SpeakerMediaPlayer : public Component,
|
|||||||
|
|
||||||
bool is_paused_{false};
|
bool is_paused_{false};
|
||||||
bool is_muted_{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
|
// The amount to change the volume on volume up/down commands
|
||||||
float volume_increment_;
|
float volume_increment_;
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
#include "esphome/core/application.h"
|
#include "esphome/core/application.h"
|
||||||
#include "esphome/core/helpers.h"
|
#include "esphome/core/helpers.h"
|
||||||
#include "esphome/core/log.h"
|
#include "esphome/core/log.h"
|
||||||
|
#include "esphome/core/progmem.h"
|
||||||
|
|
||||||
namespace esphome::template_ {
|
namespace esphome::template_ {
|
||||||
|
|
||||||
@@ -28,18 +29,11 @@ void TemplateAlarmControlPanel::add_sensor(binary_sensor::BinarySensor *sensor,
|
|||||||
this->sensor_data_.push_back(sd);
|
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) {
|
static const LogString *sensor_type_to_string(AlarmSensorType type) {
|
||||||
switch (type) {
|
return AlarmSensorTypeStrings::get_log_str(static_cast<uint8_t>(type), 0);
|
||||||
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
|
#endif
|
||||||
|
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ enum BinarySensorFlags : uint16_t {
|
|||||||
BINARY_SENSOR_MODE_BYPASS_AUTO = 1 << 4,
|
BINARY_SENSOR_MODE_BYPASS_AUTO = 1 << 4,
|
||||||
};
|
};
|
||||||
|
|
||||||
enum AlarmSensorType : uint16_t {
|
enum AlarmSensorType : uint8_t {
|
||||||
ALARM_SENSOR_TYPE_DELAYED = 0,
|
ALARM_SENSOR_TYPE_DELAYED = 0,
|
||||||
ALARM_SENSOR_TYPE_INSTANT,
|
ALARM_SENSOR_TYPE_INSTANT,
|
||||||
ALARM_SENSOR_TYPE_DELAYED_FOLLOWER,
|
ALARM_SENSOR_TYPE_DELAYED_FOLLOWER,
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ CONFIG_SCHEMA = (
|
|||||||
RESTORE_MODES, upper=True
|
RESTORE_MODES, upper=True
|
||||||
),
|
),
|
||||||
cv.Optional(CONF_CURRENT_TEMPERATURE): cv.returning_lambda,
|
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_MODE): cv.returning_lambda,
|
||||||
cv.Optional(CONF_SUPPORTED_MODES): cv.ensure_list(
|
cv.Optional(CONF_SUPPORTED_MODES): cv.ensure_list(
|
||||||
water_heater.validate_water_heater_mode
|
water_heater.validate_water_heater_mode
|
||||||
@@ -78,6 +79,14 @@ async def to_code(config: ConfigType) -> None:
|
|||||||
)
|
)
|
||||||
cg.add(var.set_current_temperature_lambda(template_))
|
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:
|
if CONF_MODE in config:
|
||||||
template_ = await cg.process_lambda(
|
template_ = await cg.process_lambda(
|
||||||
config[CONF_MODE],
|
config[CONF_MODE],
|
||||||
|
|||||||
@@ -16,7 +16,8 @@ void TemplateWaterHeater::setup() {
|
|||||||
restore->perform();
|
restore->perform();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!this->current_temperature_f_.has_value() && !this->mode_f_.has_value())
|
if (!this->current_temperature_f_.has_value() && !this->target_temperature_f_.has_value() &&
|
||||||
|
!this->mode_f_.has_value())
|
||||||
this->disable_loop();
|
this->disable_loop();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -28,6 +29,9 @@ water_heater::WaterHeaterTraits TemplateWaterHeater::traits() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
traits.set_supports_current_temperature(true);
|
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;
|
return traits;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -42,6 +46,14 @@ 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();
|
auto new_mode = this->mode_f_.call();
|
||||||
if (new_mode.has_value()) {
|
if (new_mode.has_value()) {
|
||||||
if (*new_mode != this->mode_) {
|
if (*new_mode != this->mode_) {
|
||||||
|
|||||||
@@ -20,6 +20,9 @@ class TemplateWaterHeater : public Component, public water_heater::WaterHeater {
|
|||||||
template<typename F> void set_current_temperature_lambda(F &&f) {
|
template<typename F> void set_current_temperature_lambda(F &&f) {
|
||||||
this->current_temperature_f_.set(std::forward<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)); }
|
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; }
|
void set_optimistic(bool optimistic) { this->optimistic_ = optimistic; }
|
||||||
@@ -44,6 +47,7 @@ class TemplateWaterHeater : public Component, public water_heater::WaterHeater {
|
|||||||
// Ordered to minimize padding on 32-bit: 4-byte members first, then smaller
|
// Ordered to minimize padding on 32-bit: 4-byte members first, then smaller
|
||||||
Trigger<> set_trigger_;
|
Trigger<> set_trigger_;
|
||||||
TemplateLambda<float> current_temperature_f_;
|
TemplateLambda<float> current_temperature_f_;
|
||||||
|
TemplateLambda<float> target_temperature_f_;
|
||||||
TemplateLambda<water_heater::WaterHeaterMode> mode_f_;
|
TemplateLambda<water_heater::WaterHeaterMode> mode_f_;
|
||||||
TemplateWaterHeaterRestoreMode restore_mode_{WATER_HEATER_NO_RESTORE};
|
TemplateWaterHeaterRestoreMode restore_mode_{WATER_HEATER_NO_RESTORE};
|
||||||
water_heater::WaterHeaterModeMask supported_modes_;
|
water_heater::WaterHeaterModeMask supported_modes_;
|
||||||
|
|||||||
@@ -31,10 +31,19 @@ void Tuya::setup() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void Tuya::loop() {
|
void Tuya::loop() {
|
||||||
while (this->available()) {
|
// Read all available bytes in batches to reduce UART call overhead.
|
||||||
uint8_t c;
|
int avail = this->available();
|
||||||
this->read_byte(&c);
|
uint8_t buf[64];
|
||||||
this->handle_char_(c);
|
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]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
process_command_queue_();
|
process_command_queue_();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ from esphome.components.packet_transport import (
|
|||||||
import esphome.config_validation as cv
|
import esphome.config_validation as cv
|
||||||
from esphome.const import CONF_DATA, CONF_ID, CONF_PORT, CONF_TRIGGER_ID
|
from esphome.const import CONF_DATA, CONF_ID, CONF_PORT, CONF_TRIGGER_ID
|
||||||
from esphome.core import ID
|
from esphome.core import ID
|
||||||
from esphome.cpp_generator import literal
|
from esphome.cpp_generator import MockObj
|
||||||
|
|
||||||
CODEOWNERS = ["@clydebarrow"]
|
CODEOWNERS = ["@clydebarrow"]
|
||||||
DEPENDENCIES = ["network"]
|
DEPENDENCIES = ["network"]
|
||||||
@@ -23,8 +23,12 @@ MULTI_CONF = True
|
|||||||
udp_ns = cg.esphome_ns.namespace("udp")
|
udp_ns = cg.esphome_ns.namespace("udp")
|
||||||
UDPComponent = udp_ns.class_("UDPComponent", cg.Component)
|
UDPComponent = udp_ns.class_("UDPComponent", cg.Component)
|
||||||
UDPWriteAction = udp_ns.class_("UDPWriteAction", automation.Action)
|
UDPWriteAction = udp_ns.class_("UDPWriteAction", automation.Action)
|
||||||
trigger_args = cg.std_vector.template(cg.uint8)
|
|
||||||
trigger_argname = "data"
|
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_argtype = [(trigger_args, trigger_argname)]
|
trigger_argtype = [(trigger_args, trigger_argname)]
|
||||||
|
|
||||||
CONF_ADDRESSES = "addresses"
|
CONF_ADDRESSES = "addresses"
|
||||||
@@ -118,7 +122,13 @@ async def to_code(config):
|
|||||||
trigger_id, trigger_argtype, on_receive
|
trigger_id, trigger_argtype, on_receive
|
||||||
)
|
)
|
||||||
trigger_lambda = await cg.process_lambda(
|
trigger_lambda = await cg.process_lambda(
|
||||||
trigger.trigger(literal(trigger_argname)), trigger_argtype
|
trigger.trigger(
|
||||||
|
cg.std_vector.template(cg.uint8)(
|
||||||
|
MockObj(trigger_argname).begin(),
|
||||||
|
MockObj(trigger_argname).end(),
|
||||||
|
)
|
||||||
|
),
|
||||||
|
listener_argtype,
|
||||||
)
|
)
|
||||||
cg.add(var.add_listener(trigger_lambda))
|
cg.add(var.add_listener(trigger_lambda))
|
||||||
cg.add(var.set_should_listen())
|
cg.add(var.set_should_listen())
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ bool UDPTransport::should_send() { return network::is_connected(); }
|
|||||||
void UDPTransport::setup() {
|
void UDPTransport::setup() {
|
||||||
PacketTransport::setup();
|
PacketTransport::setup();
|
||||||
if (!this->providers_.empty() || this->is_encrypted_()) {
|
if (!this->providers_.empty() || this->is_encrypted_()) {
|
||||||
this->parent_->add_listener([this](std::vector<uint8_t> &buf) { this->process_(buf); });
|
this->parent_->add_listener([this](std::span<const uint8_t> data) { this->process_(data); });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -103,8 +103,8 @@ void UDPComponent::setup() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void UDPComponent::loop() {
|
void UDPComponent::loop() {
|
||||||
auto buf = std::vector<uint8_t>(MAX_PACKET_SIZE);
|
|
||||||
if (this->should_listen_) {
|
if (this->should_listen_) {
|
||||||
|
std::array<uint8_t, MAX_PACKET_SIZE> buf;
|
||||||
for (;;) {
|
for (;;) {
|
||||||
#if defined(USE_SOCKET_IMPL_BSD_SOCKETS) || defined(USE_SOCKET_IMPL_LWIP_SOCKETS)
|
#if defined(USE_SOCKET_IMPL_BSD_SOCKETS) || defined(USE_SOCKET_IMPL_LWIP_SOCKETS)
|
||||||
auto len = this->listen_socket_->read(buf.data(), buf.size());
|
auto len = this->listen_socket_->read(buf.data(), buf.size());
|
||||||
@@ -116,9 +116,9 @@ void UDPComponent::loop() {
|
|||||||
#endif
|
#endif
|
||||||
if (len <= 0)
|
if (len <= 0)
|
||||||
break;
|
break;
|
||||||
buf.resize(len);
|
size_t packet_len = static_cast<size_t>(len);
|
||||||
ESP_LOGV(TAG, "Received packet of length %zu", len);
|
ESP_LOGV(TAG, "Received packet of length %zu", packet_len);
|
||||||
this->packet_listeners_.call(buf);
|
this->packet_listeners_.call(std::span<const uint8_t>(buf.data(), packet_len));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,9 @@
|
|||||||
#ifdef USE_SOCKET_IMPL_LWIP_TCP
|
#ifdef USE_SOCKET_IMPL_LWIP_TCP
|
||||||
#include <WiFiUdp.h>
|
#include <WiFiUdp.h>
|
||||||
#endif
|
#endif
|
||||||
|
#include <array>
|
||||||
#include <initializer_list>
|
#include <initializer_list>
|
||||||
|
#include <span>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
namespace esphome::udp {
|
namespace esphome::udp {
|
||||||
@@ -26,7 +28,7 @@ class UDPComponent : public Component {
|
|||||||
void set_broadcast_port(uint16_t port) { this->broadcast_port_ = port; }
|
void set_broadcast_port(uint16_t port) { this->broadcast_port_ = port; }
|
||||||
void set_should_broadcast() { this->should_broadcast_ = true; }
|
void set_should_broadcast() { this->should_broadcast_ = true; }
|
||||||
void set_should_listen() { this->should_listen_ = true; }
|
void set_should_listen() { this->should_listen_ = true; }
|
||||||
void add_listener(std::function<void(std::vector<uint8_t> &)> &&listener) {
|
void add_listener(std::function<void(std::span<const uint8_t>)> &&listener) {
|
||||||
this->packet_listeners_.add(std::move(listener));
|
this->packet_listeners_.add(std::move(listener));
|
||||||
}
|
}
|
||||||
void setup() override;
|
void setup() override;
|
||||||
@@ -41,7 +43,7 @@ class UDPComponent : public Component {
|
|||||||
uint16_t broadcast_port_{};
|
uint16_t broadcast_port_{};
|
||||||
bool should_broadcast_{};
|
bool should_broadcast_{};
|
||||||
bool should_listen_{};
|
bool should_listen_{};
|
||||||
CallbackManager<void(std::vector<uint8_t> &)> packet_listeners_{};
|
CallbackManager<void(std::span<const uint8_t>)> packet_listeners_{};
|
||||||
|
|
||||||
#if defined(USE_SOCKET_IMPL_BSD_SOCKETS) || defined(USE_SOCKET_IMPL_LWIP_SOCKETS)
|
#if defined(USE_SOCKET_IMPL_BSD_SOCKETS) || defined(USE_SOCKET_IMPL_LWIP_SOCKETS)
|
||||||
std::unique_ptr<socket::Socket> broadcast_socket_ = nullptr;
|
std::unique_ptr<socket::Socket> broadcast_socket_ = nullptr;
|
||||||
|
|||||||
@@ -65,6 +65,7 @@ WaterHeaterCall &WaterHeaterCall::set_away(bool away) {
|
|||||||
} else {
|
} else {
|
||||||
this->state_ &= ~WATER_HEATER_STATE_AWAY;
|
this->state_ &= ~WATER_HEATER_STATE_AWAY;
|
||||||
}
|
}
|
||||||
|
this->state_mask_ |= WATER_HEATER_STATE_AWAY;
|
||||||
return *this;
|
return *this;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -74,6 +75,7 @@ WaterHeaterCall &WaterHeaterCall::set_on(bool on) {
|
|||||||
} else {
|
} else {
|
||||||
this->state_ &= ~WATER_HEATER_STATE_ON;
|
this->state_ &= ~WATER_HEATER_STATE_ON;
|
||||||
}
|
}
|
||||||
|
this->state_mask_ |= WATER_HEATER_STATE_ON;
|
||||||
return *this;
|
return *this;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -92,11 +94,11 @@ void WaterHeaterCall::perform() {
|
|||||||
if (!std::isnan(this->target_temperature_high_)) {
|
if (!std::isnan(this->target_temperature_high_)) {
|
||||||
ESP_LOGD(TAG, " Target Temperature High: %.2f", this->target_temperature_high_);
|
ESP_LOGD(TAG, " Target Temperature High: %.2f", this->target_temperature_high_);
|
||||||
}
|
}
|
||||||
if (this->state_ & WATER_HEATER_STATE_AWAY) {
|
if (this->state_mask_ & WATER_HEATER_STATE_AWAY) {
|
||||||
ESP_LOGD(TAG, " Away: YES");
|
ESP_LOGD(TAG, " Away: %s", (this->state_ & WATER_HEATER_STATE_AWAY) ? "YES" : "NO");
|
||||||
}
|
}
|
||||||
if (this->state_ & WATER_HEATER_STATE_ON) {
|
if (this->state_mask_ & WATER_HEATER_STATE_ON) {
|
||||||
ESP_LOGD(TAG, " On: YES");
|
ESP_LOGD(TAG, " On: %s", (this->state_ & WATER_HEATER_STATE_ON) ? "YES" : "NO");
|
||||||
}
|
}
|
||||||
this->parent_->control(*this);
|
this->parent_->control(*this);
|
||||||
}
|
}
|
||||||
@@ -137,13 +139,17 @@ void WaterHeaterCall::validate_() {
|
|||||||
this->target_temperature_high_ = NAN;
|
this->target_temperature_high_ = NAN;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if ((this->state_ & WATER_HEATER_STATE_AWAY) && !traits.get_supports_away_mode()) {
|
if (!traits.get_supports_away_mode()) {
|
||||||
ESP_LOGW(TAG, "'%s' - Away mode not supported", this->parent_->get_name().c_str());
|
if (this->state_ & WATER_HEATER_STATE_AWAY) {
|
||||||
|
ESP_LOGW(TAG, "'%s' - Away mode not supported", this->parent_->get_name().c_str());
|
||||||
|
}
|
||||||
this->state_ &= ~WATER_HEATER_STATE_AWAY;
|
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 ON/OFF not supported, device is always on - clear the flag silently
|
||||||
if (!traits.has_feature_flags(WATER_HEATER_SUPPORTS_ON_OFF)) {
|
if (!traits.has_feature_flags(WATER_HEATER_SUPPORTS_ON_OFF)) {
|
||||||
this->state_ &= ~WATER_HEATER_STATE_ON;
|
this->state_ &= ~WATER_HEATER_STATE_ON;
|
||||||
|
this->state_mask_ &= ~WATER_HEATER_STATE_ON;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -91,6 +91,8 @@ class WaterHeaterCall {
|
|||||||
float get_target_temperature_high() const { return this->target_temperature_high_; }
|
float get_target_temperature_high() const { return this->target_temperature_high_; }
|
||||||
/// Get state flags value
|
/// Get state flags value
|
||||||
uint32_t get_state() const { return this->state_; }
|
uint32_t get_state() const { return this->state_; }
|
||||||
|
/// Get mask of state flags that are being changed
|
||||||
|
uint32_t get_state_mask() const { return this->state_mask_; }
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
void validate_();
|
void validate_();
|
||||||
@@ -100,6 +102,7 @@ class WaterHeaterCall {
|
|||||||
float target_temperature_low_{NAN};
|
float target_temperature_low_{NAN};
|
||||||
float target_temperature_high_{NAN};
|
float target_temperature_high_{NAN};
|
||||||
uint32_t state_{0};
|
uint32_t state_{0};
|
||||||
|
uint32_t state_mask_{0};
|
||||||
};
|
};
|
||||||
|
|
||||||
struct WaterHeaterCallInternal : public WaterHeaterCall {
|
struct WaterHeaterCallInternal : public WaterHeaterCall {
|
||||||
@@ -111,6 +114,7 @@ struct WaterHeaterCallInternal : public WaterHeaterCall {
|
|||||||
this->target_temperature_low_ = restore.target_temperature_low_;
|
this->target_temperature_low_ = restore.target_temperature_low_;
|
||||||
this->target_temperature_high_ = restore.target_temperature_high_;
|
this->target_temperature_high_ = restore.target_temperature_high_;
|
||||||
this->state_ = restore.state_;
|
this->state_ = restore.state_;
|
||||||
|
this->state_mask_ = restore.state_mask_;
|
||||||
return *this;
|
return *this;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -6,8 +6,7 @@
|
|||||||
#include <cstring>
|
#include <cstring>
|
||||||
#include "multipart_parser.h"
|
#include "multipart_parser.h"
|
||||||
|
|
||||||
namespace esphome {
|
namespace esphome::web_server_idf {
|
||||||
namespace web_server_idf {
|
|
||||||
|
|
||||||
static const char *const TAG = "multipart";
|
static const char *const TAG = "multipart";
|
||||||
|
|
||||||
@@ -249,6 +248,5 @@ std::string str_trim(const std::string &str) {
|
|||||||
return str.substr(start, end - start + 1);
|
return str.substr(start, end - start + 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace web_server_idf
|
} // namespace esphome::web_server_idf
|
||||||
} // namespace esphome
|
|
||||||
#endif // defined(USE_ESP32) && defined(USE_WEBSERVER_OTA)
|
#endif // defined(USE_ESP32) && defined(USE_WEBSERVER_OTA)
|
||||||
|
|||||||
@@ -10,8 +10,7 @@
|
|||||||
#include <string>
|
#include <string>
|
||||||
#include <utility>
|
#include <utility>
|
||||||
|
|
||||||
namespace esphome {
|
namespace esphome::web_server_idf {
|
||||||
namespace web_server_idf {
|
|
||||||
|
|
||||||
// Wrapper around zorxx/multipart-parser for ESP-IDF OTA uploads
|
// Wrapper around zorxx/multipart-parser for ESP-IDF OTA uploads
|
||||||
class MultipartReader {
|
class MultipartReader {
|
||||||
@@ -81,6 +80,5 @@ bool parse_multipart_boundary(const char *content_type, const char **boundary_st
|
|||||||
// Trim whitespace from both ends of a string
|
// Trim whitespace from both ends of a string
|
||||||
std::string str_trim(const std::string &str);
|
std::string str_trim(const std::string &str);
|
||||||
|
|
||||||
} // namespace web_server_idf
|
} // namespace esphome::web_server_idf
|
||||||
} // namespace esphome
|
|
||||||
#endif // defined(USE_ESP32) && defined(USE_WEBSERVER_OTA)
|
#endif // defined(USE_ESP32) && defined(USE_WEBSERVER_OTA)
|
||||||
|
|||||||
@@ -8,8 +8,7 @@
|
|||||||
|
|
||||||
#include "utils.h"
|
#include "utils.h"
|
||||||
|
|
||||||
namespace esphome {
|
namespace esphome::web_server_idf {
|
||||||
namespace web_server_idf {
|
|
||||||
|
|
||||||
static const char *const TAG = "web_server_idf_utils";
|
static const char *const TAG = "web_server_idf_utils";
|
||||||
|
|
||||||
@@ -119,6 +118,5 @@ const char *stristr(const char *haystack, const char *needle) {
|
|||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace web_server_idf
|
} // namespace esphome::web_server_idf
|
||||||
} // namespace esphome
|
|
||||||
#endif // USE_ESP32
|
#endif // USE_ESP32
|
||||||
|
|||||||
@@ -5,8 +5,7 @@
|
|||||||
#include <string>
|
#include <string>
|
||||||
#include "esphome/core/helpers.h"
|
#include "esphome/core/helpers.h"
|
||||||
|
|
||||||
namespace esphome {
|
namespace esphome::web_server_idf {
|
||||||
namespace web_server_idf {
|
|
||||||
|
|
||||||
/// Decode URL-encoded string in-place (e.g., %20 -> space, + -> space)
|
/// Decode URL-encoded string in-place (e.g., %20 -> space, + -> space)
|
||||||
/// Returns the new length of the decoded string
|
/// Returns the new length of the decoded string
|
||||||
@@ -29,6 +28,5 @@ bool str_ncmp_ci(const char *s1, const char *s2, size_t n);
|
|||||||
// Case-insensitive string search (like strstr but case-insensitive)
|
// Case-insensitive string search (like strstr but case-insensitive)
|
||||||
const char *stristr(const char *haystack, const char *needle);
|
const char *stristr(const char *haystack, const char *needle);
|
||||||
|
|
||||||
} // namespace web_server_idf
|
} // namespace esphome::web_server_idf
|
||||||
} // namespace esphome
|
|
||||||
#endif // USE_ESP32
|
#endif // USE_ESP32
|
||||||
|
|||||||
@@ -30,8 +30,7 @@
|
|||||||
#include <cerrno>
|
#include <cerrno>
|
||||||
#include <sys/socket.h>
|
#include <sys/socket.h>
|
||||||
|
|
||||||
namespace esphome {
|
namespace esphome::web_server_idf {
|
||||||
namespace web_server_idf {
|
|
||||||
|
|
||||||
#ifndef HTTPD_409
|
#ifndef HTTPD_409
|
||||||
#define HTTPD_409 "409 Conflict"
|
#define HTTPD_409 "409 Conflict"
|
||||||
@@ -258,8 +257,6 @@ StringRef AsyncWebServerRequest::url_to(std::span<char, URL_BUF_SIZE> buffer) co
|
|||||||
return StringRef(buffer.data(), decoded_len);
|
return StringRef(buffer.data(), decoded_len);
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string AsyncWebServerRequest::host() const { return this->get_header("Host").value(); }
|
|
||||||
|
|
||||||
void AsyncWebServerRequest::send(AsyncWebServerResponse *response) {
|
void AsyncWebServerRequest::send(AsyncWebServerResponse *response) {
|
||||||
httpd_resp_send(*this, response->get_content_data(), response->get_content_size());
|
httpd_resp_send(*this, response->get_content_data(), response->get_content_size());
|
||||||
}
|
}
|
||||||
@@ -897,7 +894,6 @@ esp_err_t AsyncWebServer::handle_multipart_upload_(httpd_req_t *r, const char *c
|
|||||||
}
|
}
|
||||||
#endif // USE_WEBSERVER_OTA
|
#endif // USE_WEBSERVER_OTA
|
||||||
|
|
||||||
} // namespace web_server_idf
|
} // namespace esphome::web_server_idf
|
||||||
} // namespace esphome
|
|
||||||
|
|
||||||
#endif // !defined(USE_ESP32)
|
#endif // !defined(USE_ESP32)
|
||||||
|
|||||||
@@ -121,7 +121,6 @@ class AsyncWebServerRequest {
|
|||||||
char buffer[URL_BUF_SIZE];
|
char buffer[URL_BUF_SIZE];
|
||||||
return std::string(this->url_to(buffer));
|
return std::string(this->url_to(buffer));
|
||||||
}
|
}
|
||||||
std::string host() const;
|
|
||||||
// NOLINTNEXTLINE(readability-identifier-naming)
|
// NOLINTNEXTLINE(readability-identifier-naming)
|
||||||
size_t contentLength() const { return this->req_->content_len; }
|
size_t contentLength() const { return this->req_->content_len; }
|
||||||
|
|
||||||
|
|||||||
@@ -390,20 +390,19 @@ void Scheduler::full_cleanup_removed_items_() {
|
|||||||
// 4. No operations inside can block or take other locks, so no deadlock risk
|
// 4. No operations inside can block or take other locks, so no deadlock risk
|
||||||
LockGuard guard{this->lock_};
|
LockGuard guard{this->lock_};
|
||||||
|
|
||||||
std::vector<std::unique_ptr<SchedulerItem>> valid_items;
|
// Compact in-place: move valid items forward, recycle removed ones
|
||||||
|
size_t write = 0;
|
||||||
// Move all non-removed items to valid_items, recycle removed ones
|
for (size_t read = 0; read < this->items_.size(); ++read) {
|
||||||
for (auto &item : this->items_) {
|
if (!is_item_removed_(this->items_[read].get())) {
|
||||||
if (!is_item_removed_(item.get())) {
|
if (write != read) {
|
||||||
valid_items.push_back(std::move(item));
|
this->items_[write] = std::move(this->items_[read]);
|
||||||
|
}
|
||||||
|
++write;
|
||||||
} else {
|
} else {
|
||||||
// Recycle removed items
|
this->recycle_item_main_loop_(std::move(this->items_[read]));
|
||||||
this->recycle_item_main_loop_(std::move(item));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
this->items_.erase(this->items_.begin() + write, this->items_.end());
|
||||||
// Replace items_ with the filtered list
|
|
||||||
this->items_ = std::move(valid_items);
|
|
||||||
// Rebuild the heap structure since items are no longer in heap order
|
// Rebuild the heap structure since items are no longer in heap order
|
||||||
std::make_heap(this->items_.begin(), this->items_.end(), SchedulerItem::cmp);
|
std::make_heap(this->items_.begin(), this->items_.end(), SchedulerItem::cmp);
|
||||||
this->to_remove_ = 0;
|
this->to_remove_ = 0;
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ std_shared_ptr = std_ns.class_("shared_ptr")
|
|||||||
std_string = std_ns.class_("string")
|
std_string = std_ns.class_("string")
|
||||||
std_string_ref = std_ns.namespace("string &")
|
std_string_ref = std_ns.namespace("string &")
|
||||||
std_vector = std_ns.class_("vector")
|
std_vector = std_ns.class_("vector")
|
||||||
|
std_span = std_ns.class_("span")
|
||||||
uint8 = global_ns.namespace("uint8_t")
|
uint8 = global_ns.namespace("uint8_t")
|
||||||
uint16 = global_ns.namespace("uint16_t")
|
uint16 = global_ns.namespace("uint16_t")
|
||||||
uint32 = global_ns.namespace("uint32_t")
|
uint32 = global_ns.namespace("uint32_t")
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ class DashboardSettings:
|
|||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
"""Initialize the dashboard settings."""
|
"""Initialize the dashboard settings."""
|
||||||
self.config_dir: Path = None
|
self.config_dir: Path = None
|
||||||
self.password_hash: str = ""
|
self.password_hash: bytes = b""
|
||||||
self.username: str = ""
|
self.username: str = ""
|
||||||
self.using_password: bool = False
|
self.using_password: bool = False
|
||||||
self.on_ha_addon: bool = False
|
self.on_ha_addon: bool = False
|
||||||
@@ -84,11 +84,14 @@ class DashboardSettings:
|
|||||||
def check_password(self, username: str, password: str) -> bool:
|
def check_password(self, username: str, password: str) -> bool:
|
||||||
if not self.using_auth:
|
if not self.using_auth:
|
||||||
return True
|
return True
|
||||||
if username != self.username:
|
# Compare in constant running time (to prevent timing attacks)
|
||||||
return False
|
username_matches = hmac.compare_digest(
|
||||||
|
username.encode("utf-8"), self.username.encode("utf-8")
|
||||||
# Compare password in constant running time (to prevent timing attacks)
|
)
|
||||||
return hmac.compare_digest(self.password_hash, password_hash(password))
|
password_matches = hmac.compare_digest(
|
||||||
|
self.password_hash, password_hash(password)
|
||||||
|
)
|
||||||
|
return username_matches and password_matches
|
||||||
|
|
||||||
def rel_path(self, *args: Any) -> Path:
|
def rel_path(self, *args: Any) -> Path:
|
||||||
"""Return a path relative to the ESPHome config folder."""
|
"""Return a path relative to the ESPHome config folder."""
|
||||||
|
|||||||
@@ -120,8 +120,11 @@ def is_authenticated(handler: BaseHandler) -> bool:
|
|||||||
if auth_header := handler.request.headers.get("Authorization"):
|
if auth_header := handler.request.headers.get("Authorization"):
|
||||||
assert isinstance(auth_header, str)
|
assert isinstance(auth_header, str)
|
||||||
if auth_header.startswith("Basic "):
|
if auth_header.startswith("Basic "):
|
||||||
auth_decoded = base64.b64decode(auth_header[6:]).decode()
|
try:
|
||||||
username, password = auth_decoded.split(":", 1)
|
auth_decoded = base64.b64decode(auth_header[6:]).decode()
|
||||||
|
username, password = auth_decoded.split(":", 1)
|
||||||
|
except (binascii.Error, ValueError, UnicodeDecodeError):
|
||||||
|
return False
|
||||||
return settings.check_password(username, password)
|
return settings.check_password(username, password)
|
||||||
return handler.get_secure_cookie(AUTH_COOKIE_NAME) == COOKIE_AUTHENTICATED_YES
|
return handler.get_secure_cookie(AUTH_COOKIE_NAME) == COOKIE_AUTHENTICATED_YES
|
||||||
|
|
||||||
@@ -317,6 +320,7 @@ class EsphomeCommandWebSocket(CheckOriginMixin, tornado.websocket.WebSocketHandl
|
|||||||
# Check if the proc was not forcibly closed
|
# Check if the proc was not forcibly closed
|
||||||
_LOGGER.info("Process exited with return code %s", returncode)
|
_LOGGER.info("Process exited with return code %s", returncode)
|
||||||
self.write_message({"event": "exit", "code": returncode})
|
self.write_message({"event": "exit", "code": returncode})
|
||||||
|
self.close()
|
||||||
|
|
||||||
def on_close(self) -> None:
|
def on_close(self) -> None:
|
||||||
# Check if proc exists (if 'start' has been run)
|
# Check if proc exists (if 'start' has been run)
|
||||||
@@ -1053,17 +1057,26 @@ class DownloadBinaryRequestHandler(BaseHandler):
|
|||||||
# fallback to type=, but prioritize file=
|
# fallback to type=, but prioritize file=
|
||||||
file_name = self.get_argument("type", None)
|
file_name = self.get_argument("type", None)
|
||||||
file_name = self.get_argument("file", file_name)
|
file_name = self.get_argument("file", file_name)
|
||||||
if file_name is None:
|
if file_name is None or not file_name.strip():
|
||||||
self.send_error(400)
|
self.send_error(400)
|
||||||
return
|
return
|
||||||
file_name = file_name.replace("..", "").lstrip("/")
|
|
||||||
# get requested download name, or build it based on filename
|
# get requested download name, or build it based on filename
|
||||||
download_name = self.get_argument(
|
download_name = self.get_argument(
|
||||||
"download",
|
"download",
|
||||||
f"{storage_json.name}-{file_name}",
|
f"{storage_json.name}-{file_name}",
|
||||||
)
|
)
|
||||||
|
|
||||||
path = storage_json.firmware_bin_path.parent.joinpath(file_name)
|
if storage_json.firmware_bin_path is None:
|
||||||
|
self.send_error(404)
|
||||||
|
return
|
||||||
|
|
||||||
|
base_dir = storage_json.firmware_bin_path.parent.resolve()
|
||||||
|
path = base_dir.joinpath(file_name).resolve()
|
||||||
|
try:
|
||||||
|
path.relative_to(base_dir)
|
||||||
|
except ValueError:
|
||||||
|
self.send_error(403)
|
||||||
|
return
|
||||||
|
|
||||||
if not path.is_file():
|
if not path.is_file():
|
||||||
args = ["esphome", "idedata", settings.rel_path(configuration)]
|
args = ["esphome", "idedata", settings.rel_path(configuration)]
|
||||||
@@ -1077,7 +1090,7 @@ class DownloadBinaryRequestHandler(BaseHandler):
|
|||||||
|
|
||||||
found = False
|
found = False
|
||||||
for image in idedata.extra_flash_images:
|
for image in idedata.extra_flash_images:
|
||||||
if image.path.endswith(file_name):
|
if image.path.as_posix().endswith(file_name):
|
||||||
path = image.path
|
path = image.path
|
||||||
download_name = file_name
|
download_name = file_name
|
||||||
found = True
|
found = True
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import hashlib
|
|||||||
import io
|
import io
|
||||||
import logging
|
import logging
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import random
|
import secrets
|
||||||
import socket
|
import socket
|
||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
@@ -300,8 +300,8 @@ def perform_ota(
|
|||||||
nonce = nonce_bytes.decode()
|
nonce = nonce_bytes.decode()
|
||||||
_LOGGER.debug("Auth: %s Nonce is %s", hash_name, nonce)
|
_LOGGER.debug("Auth: %s Nonce is %s", hash_name, nonce)
|
||||||
|
|
||||||
# Generate cnonce
|
# Generate cnonce matching the hash algorithm's digest size
|
||||||
cnonce = hash_func(str(random.random()).encode()).hexdigest()
|
cnonce = secrets.token_hex(nonce_size // 2)
|
||||||
_LOGGER.debug("Auth: %s CNonce is %s", hash_name, cnonce)
|
_LOGGER.debug("Auth: %s CNonce is %s", hash_name, cnonce)
|
||||||
|
|
||||||
send_check(sock, cnonce, "auth cnonce")
|
send_check(sock, cnonce, "auth cnonce")
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import base64
|
import base64
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import random
|
|
||||||
import secrets
|
import secrets
|
||||||
import string
|
import string
|
||||||
from typing import Literal, NotRequired, TypedDict, Unpack
|
from typing import Literal, NotRequired, TypedDict, Unpack
|
||||||
@@ -130,7 +129,7 @@ def wizard_file(**kwargs: Unpack[WizardFileKwargs]) -> str:
|
|||||||
if len(ap_name) > 32:
|
if len(ap_name) > 32:
|
||||||
ap_name = ap_name_base
|
ap_name = ap_name_base
|
||||||
kwargs["fallback_name"] = ap_name
|
kwargs["fallback_name"] = ap_name
|
||||||
kwargs["fallback_psk"] = "".join(random.choice(letters) for _ in range(12))
|
kwargs["fallback_psk"] = "".join(secrets.choice(letters) for _ in range(12))
|
||||||
|
|
||||||
base = BASE_CONFIG_FRIENDLY if kwargs.get("friendly_name") else BASE_CONFIG
|
base = BASE_CONFIG_FRIENDLY if kwargs.get("friendly_name") else BASE_CONFIG
|
||||||
|
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ lib_deps_base =
|
|||||||
wjtje/qr-code-generator-library@1.7.0 ; qr_code
|
wjtje/qr-code-generator-library@1.7.0 ; qr_code
|
||||||
functionpointer/arduino-MLX90393@1.0.2 ; mlx90393
|
functionpointer/arduino-MLX90393@1.0.2 ; mlx90393
|
||||||
pavlodn/HaierProtocol@0.9.31 ; haier
|
pavlodn/HaierProtocol@0.9.31 ; haier
|
||||||
esphome/dsmr_parser@1.0.0 ; dsmr
|
esphome/dsmr_parser@1.1.0 ; dsmr
|
||||||
polargoose/Crypto-no-arduino@0.4.0 ; dsmr
|
polargoose/Crypto-no-arduino@0.4.0 ; dsmr
|
||||||
https://github.com/esphome/TinyGPSPlus.git#v1.1.0 ; gps
|
https://github.com/esphome/TinyGPSPlus.git#v1.1.0 ; gps
|
||||||
; This is using the repository until a new release is published to PlatformIO
|
; This is using the repository until a new release is published to PlatformIO
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ resvg-py==0.2.6
|
|||||||
freetype-py==2.5.1
|
freetype-py==2.5.1
|
||||||
jinja2==3.1.6
|
jinja2==3.1.6
|
||||||
bleak==2.1.1
|
bleak==2.1.1
|
||||||
|
requests==2.32.5
|
||||||
|
|
||||||
# esp-idf >= 5.0 requires this
|
# esp-idf >= 5.0 requires this
|
||||||
pyparsing >= 3.0
|
pyparsing >= 3.0
|
||||||
|
|||||||
@@ -2270,10 +2270,13 @@ SOURCE_NAMES = {
|
|||||||
SOURCE_CLIENT: "SOURCE_CLIENT",
|
SOURCE_CLIENT: "SOURCE_CLIENT",
|
||||||
}
|
}
|
||||||
|
|
||||||
RECEIVE_CASES: dict[int, tuple[str, str | None]] = {}
|
RECEIVE_CASES: dict[int, tuple[str, str | None, str]] = {}
|
||||||
|
|
||||||
ifdefs: dict[str, str] = {}
|
ifdefs: dict[str, str] = {}
|
||||||
|
|
||||||
|
# Track messages with no fields (empty messages) for parameter elision
|
||||||
|
EMPTY_MESSAGES: set[str] = set()
|
||||||
|
|
||||||
|
|
||||||
def get_opt(
|
def get_opt(
|
||||||
desc: descriptor.DescriptorProto,
|
desc: descriptor.DescriptorProto,
|
||||||
@@ -2504,26 +2507,26 @@ def build_service_message_type(
|
|||||||
# Only add ifdef when we're actually generating content
|
# Only add ifdef when we're actually generating content
|
||||||
if ifdef is not None:
|
if ifdef is not None:
|
||||||
hout += f"#ifdef {ifdef}\n"
|
hout += f"#ifdef {ifdef}\n"
|
||||||
# Generate receive
|
# Generate receive handler and switch case
|
||||||
func = f"on_{snake}"
|
func = f"on_{snake}"
|
||||||
hout += f"virtual void {func}(const {mt.name} &value){{}};\n"
|
|
||||||
case = ""
|
|
||||||
case += f"{mt.name} msg;\n"
|
|
||||||
# Check if this message has any fields (excluding deprecated ones)
|
|
||||||
has_fields = any(not field.options.deprecated for field in mt.field)
|
has_fields = any(not field.options.deprecated for field in mt.field)
|
||||||
if has_fields:
|
is_empty = not has_fields
|
||||||
# Normal case: decode the message
|
if is_empty:
|
||||||
|
EMPTY_MESSAGES.add(mt.name)
|
||||||
|
hout += f"virtual void {func}({'' if is_empty else f'const {mt.name} &value'}){{}};\n"
|
||||||
|
case = ""
|
||||||
|
if not is_empty:
|
||||||
|
case += f"{mt.name} msg;\n"
|
||||||
case += "msg.decode(msg_data, msg_size);\n"
|
case += "msg.decode(msg_data, msg_size);\n"
|
||||||
else:
|
|
||||||
# Empty message optimization: skip decode since there are no fields
|
|
||||||
case += "// Empty message: no decode needed\n"
|
|
||||||
if log:
|
if log:
|
||||||
case += "#ifdef HAS_PROTO_MESSAGE_DUMP\n"
|
case += "#ifdef HAS_PROTO_MESSAGE_DUMP\n"
|
||||||
case += f'this->log_receive_message_(LOG_STR("{func}"), msg);\n'
|
if is_empty:
|
||||||
|
case += f'this->log_receive_message_(LOG_STR("{func}"));\n'
|
||||||
|
else:
|
||||||
|
case += f'this->log_receive_message_(LOG_STR("{func}"), msg);\n'
|
||||||
case += "#endif\n"
|
case += "#endif\n"
|
||||||
case += f"this->{func}(msg);\n"
|
case += f"this->{func}({'msg' if not is_empty else ''});\n"
|
||||||
case += "break;"
|
case += "break;"
|
||||||
# Store the message name and ifdef with the case for later use
|
|
||||||
RECEIVE_CASES[id_] = (case, ifdef, mt.name)
|
RECEIVE_CASES[id_] = (case, ifdef, mt.name)
|
||||||
|
|
||||||
# Only close ifdef if we opened it
|
# Only close ifdef if we opened it
|
||||||
@@ -2839,6 +2842,7 @@ static const char *const TAG = "api.service";
|
|||||||
hpp += (
|
hpp += (
|
||||||
" void log_receive_message_(const LogString *name, const ProtoMessage &msg);\n"
|
" void log_receive_message_(const LogString *name, const ProtoMessage &msg);\n"
|
||||||
)
|
)
|
||||||
|
hpp += " void log_receive_message_(const LogString *name);\n"
|
||||||
hpp += " public:\n"
|
hpp += " public:\n"
|
||||||
hpp += "#endif\n\n"
|
hpp += "#endif\n\n"
|
||||||
|
|
||||||
@@ -2862,6 +2866,9 @@ static const char *const TAG = "api.service";
|
|||||||
cpp += " DumpBuffer dump_buf;\n"
|
cpp += " DumpBuffer dump_buf;\n"
|
||||||
cpp += ' ESP_LOGVV(TAG, "%s: %s", LOG_STR_ARG(name), msg.dump_to(dump_buf));\n'
|
cpp += ' ESP_LOGVV(TAG, "%s: %s", LOG_STR_ARG(name), msg.dump_to(dump_buf));\n'
|
||||||
cpp += "}\n"
|
cpp += "}\n"
|
||||||
|
cpp += f"void {class_name}::log_receive_message_(const LogString *name) {{\n"
|
||||||
|
cpp += ' ESP_LOGVV(TAG, "%s: {}", LOG_STR_ARG(name));\n'
|
||||||
|
cpp += "}\n"
|
||||||
cpp += "#endif\n\n"
|
cpp += "#endif\n\n"
|
||||||
|
|
||||||
for mt in file.message_type:
|
for mt in file.message_type:
|
||||||
@@ -2899,7 +2906,6 @@ static const char *const TAG = "api.service";
|
|||||||
class_name = "APIServerConnection"
|
class_name = "APIServerConnection"
|
||||||
hpp += "\n"
|
hpp += "\n"
|
||||||
hpp += f"class {class_name} : public {class_name}Base {{\n"
|
hpp += f"class {class_name} : public {class_name}Base {{\n"
|
||||||
hpp += " public:\n"
|
|
||||||
hpp_protected = ""
|
hpp_protected = ""
|
||||||
cpp += "\n"
|
cpp += "\n"
|
||||||
|
|
||||||
@@ -2907,14 +2913,8 @@ static const char *const TAG = "api.service";
|
|||||||
message_auth_map: dict[str, bool] = {}
|
message_auth_map: dict[str, bool] = {}
|
||||||
message_conn_map: dict[str, bool] = {}
|
message_conn_map: dict[str, bool] = {}
|
||||||
|
|
||||||
m = serv.method[0]
|
|
||||||
for m in serv.method:
|
for m in serv.method:
|
||||||
func = m.name
|
|
||||||
inp = m.input_type[1:]
|
inp = m.input_type[1:]
|
||||||
ret = m.output_type[1:]
|
|
||||||
is_void = ret == "void"
|
|
||||||
snake = camel_to_snake(inp)
|
|
||||||
on_func = f"on_{snake}"
|
|
||||||
needs_conn = get_opt(m, pb.needs_setup_connection, True)
|
needs_conn = get_opt(m, pb.needs_setup_connection, True)
|
||||||
needs_auth = get_opt(m, pb.needs_authentication, True)
|
needs_auth = get_opt(m, pb.needs_authentication, True)
|
||||||
|
|
||||||
@@ -2922,39 +2922,6 @@ static const char *const TAG = "api.service";
|
|||||||
message_auth_map[inp] = needs_auth
|
message_auth_map[inp] = needs_auth
|
||||||
message_conn_map[inp] = needs_conn
|
message_conn_map[inp] = needs_conn
|
||||||
|
|
||||||
ifdef = message_ifdef_map.get(inp, ifdefs.get(inp))
|
|
||||||
|
|
||||||
if ifdef is not None:
|
|
||||||
hpp += f"#ifdef {ifdef}\n"
|
|
||||||
hpp_protected += f"#ifdef {ifdef}\n"
|
|
||||||
cpp += f"#ifdef {ifdef}\n"
|
|
||||||
|
|
||||||
hpp_protected += f" void {on_func}(const {inp} &msg) override;\n"
|
|
||||||
|
|
||||||
# For non-void methods, generate a send_ method instead of return-by-value
|
|
||||||
if is_void:
|
|
||||||
hpp += f" virtual void {func}(const {inp} &msg) = 0;\n"
|
|
||||||
else:
|
|
||||||
hpp += f" virtual bool send_{func}_response(const {inp} &msg) = 0;\n"
|
|
||||||
|
|
||||||
cpp += f"void {class_name}::{on_func}(const {inp} &msg) {{\n"
|
|
||||||
|
|
||||||
# No authentication check here - it's done in read_message
|
|
||||||
body = ""
|
|
||||||
if is_void:
|
|
||||||
body += f"this->{func}(msg);\n"
|
|
||||||
else:
|
|
||||||
body += f"if (!this->send_{func}_response(msg)) {{\n"
|
|
||||||
body += " this->on_fatal_error();\n"
|
|
||||||
body += "}\n"
|
|
||||||
|
|
||||||
cpp += indent(body) + "\n" + "}\n"
|
|
||||||
|
|
||||||
if ifdef is not None:
|
|
||||||
hpp += "#endif\n"
|
|
||||||
hpp_protected += "#endif\n"
|
|
||||||
cpp += "#endif\n"
|
|
||||||
|
|
||||||
# Generate optimized read_message with authentication checking
|
# Generate optimized read_message with authentication checking
|
||||||
# Categorize messages by their authentication requirements
|
# Categorize messages by their authentication requirements
|
||||||
no_conn_ids: set[int] = set()
|
no_conn_ids: set[int] = set()
|
||||||
|
|||||||
@@ -25,6 +25,22 @@ display:
|
|||||||
lambda: |-
|
lambda: |-
|
||||||
it.circle(64, 64, 50, Color::BLACK);
|
it.circle(64, 64, 50, Color::BLACK);
|
||||||
|
|
||||||
|
- platform: epaper_spi
|
||||||
|
spi_id: spi_bus
|
||||||
|
model: waveshare-1.54in-G
|
||||||
|
cs_pin:
|
||||||
|
allow_other_uses: true
|
||||||
|
number: GPIO5
|
||||||
|
dc_pin:
|
||||||
|
allow_other_uses: true
|
||||||
|
number: GPIO17
|
||||||
|
reset_pin:
|
||||||
|
allow_other_uses: true
|
||||||
|
number: GPIO16
|
||||||
|
busy_pin:
|
||||||
|
allow_other_uses: true
|
||||||
|
number: GPIO4
|
||||||
|
|
||||||
- platform: epaper_spi
|
- platform: epaper_spi
|
||||||
spi_id: spi_bus
|
spi_id: spi_bus
|
||||||
model: waveshare-2.13in-v3
|
model: waveshare-2.13in-v3
|
||||||
|
|||||||
@@ -20,6 +20,8 @@ lvgl:
|
|||||||
- id: lvgl_0
|
- id: lvgl_0
|
||||||
default_font: space16
|
default_font: space16
|
||||||
displays: sdl0
|
displays: sdl0
|
||||||
|
top_layer:
|
||||||
|
|
||||||
- id: lvgl_1
|
- id: lvgl_1
|
||||||
displays: sdl1
|
displays: sdl1
|
||||||
on_idle:
|
on_idle:
|
||||||
|
|||||||
@@ -412,6 +412,7 @@ water_heater:
|
|||||||
name: "Template Water Heater"
|
name: "Template Water Heater"
|
||||||
optimistic: true
|
optimistic: true
|
||||||
current_temperature: !lambda "return 42.0f;"
|
current_temperature: !lambda "return 42.0f;"
|
||||||
|
target_temperature: !lambda "return 60.0f;"
|
||||||
mode: !lambda "return water_heater::WATER_HEATER_MODE_ECO;"
|
mode: !lambda "return water_heater::WATER_HEATER_MODE_ECO;"
|
||||||
supported_modes:
|
supported_modes:
|
||||||
- "OFF"
|
- "OFF"
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
"""Tests for dashboard settings Path-related functionality."""
|
"""Tests for DashboardSettings (path resolution and authentication)."""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
@@ -10,6 +10,7 @@ import pytest
|
|||||||
|
|
||||||
from esphome.core import CORE
|
from esphome.core import CORE
|
||||||
from esphome.dashboard.settings import DashboardSettings
|
from esphome.dashboard.settings import DashboardSettings
|
||||||
|
from esphome.dashboard.util.password import password_hash
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
@@ -221,3 +222,66 @@ def test_config_path_parent_resolves_to_config_dir(tmp_path: Path) -> None:
|
|||||||
# Verify that CORE.config_path itself uses the sentinel file
|
# Verify that CORE.config_path itself uses the sentinel file
|
||||||
assert CORE.config_path.name == "___DASHBOARD_SENTINEL___.yaml"
|
assert CORE.config_path.name == "___DASHBOARD_SENTINEL___.yaml"
|
||||||
assert not CORE.config_path.exists() # Sentinel file doesn't actually exist
|
assert not CORE.config_path.exists() # Sentinel file doesn't actually exist
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def auth_settings(dashboard_settings: DashboardSettings) -> DashboardSettings:
|
||||||
|
"""Create DashboardSettings with auth configured, based on dashboard_settings."""
|
||||||
|
dashboard_settings.username = "admin"
|
||||||
|
dashboard_settings.using_password = True
|
||||||
|
dashboard_settings.password_hash = password_hash("correctpassword")
|
||||||
|
return dashboard_settings
|
||||||
|
|
||||||
|
|
||||||
|
def test_check_password_correct_credentials(auth_settings: DashboardSettings) -> None:
|
||||||
|
"""Test check_password returns True for correct username and password."""
|
||||||
|
assert auth_settings.check_password("admin", "correctpassword") is True
|
||||||
|
|
||||||
|
|
||||||
|
def test_check_password_wrong_password(auth_settings: DashboardSettings) -> None:
|
||||||
|
"""Test check_password returns False for wrong password."""
|
||||||
|
assert auth_settings.check_password("admin", "wrongpassword") is False
|
||||||
|
|
||||||
|
|
||||||
|
def test_check_password_wrong_username(auth_settings: DashboardSettings) -> None:
|
||||||
|
"""Test check_password returns False for wrong username."""
|
||||||
|
assert auth_settings.check_password("notadmin", "correctpassword") is False
|
||||||
|
|
||||||
|
|
||||||
|
def test_check_password_both_wrong(auth_settings: DashboardSettings) -> None:
|
||||||
|
"""Test check_password returns False when both are wrong."""
|
||||||
|
assert auth_settings.check_password("notadmin", "wrongpassword") is False
|
||||||
|
|
||||||
|
|
||||||
|
def test_check_password_no_auth(dashboard_settings: DashboardSettings) -> None:
|
||||||
|
"""Test check_password returns True when auth is not configured."""
|
||||||
|
assert dashboard_settings.check_password("anyone", "anything") is True
|
||||||
|
|
||||||
|
|
||||||
|
def test_check_password_non_ascii_username(
|
||||||
|
dashboard_settings: DashboardSettings,
|
||||||
|
) -> None:
|
||||||
|
"""Test check_password handles non-ASCII usernames without TypeError."""
|
||||||
|
dashboard_settings.username = "\u00e9l\u00e8ve"
|
||||||
|
dashboard_settings.using_password = True
|
||||||
|
dashboard_settings.password_hash = password_hash("pass")
|
||||||
|
assert dashboard_settings.check_password("\u00e9l\u00e8ve", "pass") is True
|
||||||
|
assert dashboard_settings.check_password("\u00e9l\u00e8ve", "wrong") is False
|
||||||
|
assert dashboard_settings.check_password("other", "pass") is False
|
||||||
|
|
||||||
|
|
||||||
|
def test_check_password_ha_addon_no_password(
|
||||||
|
dashboard_settings: DashboardSettings,
|
||||||
|
monkeypatch: pytest.MonkeyPatch,
|
||||||
|
) -> None:
|
||||||
|
"""Test check_password doesn't crash in HA add-on mode without a password.
|
||||||
|
|
||||||
|
In HA add-on mode, using_ha_addon_auth can be True while using_password
|
||||||
|
is False, leaving password_hash as b"". This must not raise TypeError
|
||||||
|
in hmac.compare_digest.
|
||||||
|
"""
|
||||||
|
monkeypatch.delenv("DISABLE_HA_AUTHENTICATION", raising=False)
|
||||||
|
dashboard_settings.on_ha_addon = True
|
||||||
|
dashboard_settings.using_password = False
|
||||||
|
# password_hash stays as default b""
|
||||||
|
assert dashboard_settings.check_password("anyone", "anything") is False
|
||||||
|
|||||||
@@ -2,12 +2,14 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from argparse import Namespace
|
from argparse import Namespace
|
||||||
import asyncio
|
import asyncio
|
||||||
|
import base64
|
||||||
from collections.abc import Generator
|
from collections.abc import Generator
|
||||||
from contextlib import asynccontextmanager
|
from contextlib import asynccontextmanager
|
||||||
import gzip
|
import gzip
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
import sys
|
||||||
from unittest.mock import AsyncMock, MagicMock, Mock, patch
|
from unittest.mock import AsyncMock, MagicMock, Mock, patch
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
@@ -29,7 +31,7 @@ from esphome.dashboard.entries import (
|
|||||||
bool_to_entry_state,
|
bool_to_entry_state,
|
||||||
)
|
)
|
||||||
from esphome.dashboard.models import build_importable_device_dict
|
from esphome.dashboard.models import build_importable_device_dict
|
||||||
from esphome.dashboard.web_server import DashboardSubscriber
|
from esphome.dashboard.web_server import DashboardSubscriber, EsphomeCommandWebSocket
|
||||||
from esphome.zeroconf import DiscoveredImport
|
from esphome.zeroconf import DiscoveredImport
|
||||||
|
|
||||||
from .common import get_fixture_path
|
from .common import get_fixture_path
|
||||||
@@ -421,7 +423,7 @@ async def test_download_binary_handler_idedata_fallback(
|
|||||||
|
|
||||||
# Mock idedata response
|
# Mock idedata response
|
||||||
mock_image = Mock()
|
mock_image = Mock()
|
||||||
mock_image.path = str(bootloader_file)
|
mock_image.path = bootloader_file
|
||||||
mock_idedata_instance = Mock()
|
mock_idedata_instance = Mock()
|
||||||
mock_idedata_instance.extra_flash_images = [mock_image]
|
mock_idedata_instance.extra_flash_images = [mock_image]
|
||||||
mock_idedata.return_value = mock_idedata_instance
|
mock_idedata.return_value = mock_idedata_instance
|
||||||
@@ -528,14 +530,22 @@ async def test_download_binary_handler_subdirectory_file_url_encoded(
|
|||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
@pytest.mark.usefixtures("mock_ext_storage_path")
|
@pytest.mark.usefixtures("mock_ext_storage_path")
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"attack_path",
|
("attack_path", "expected_code"),
|
||||||
[
|
[
|
||||||
pytest.param("../../../secrets.yaml", id="basic_traversal"),
|
pytest.param("../../../secrets.yaml", 403, id="basic_traversal"),
|
||||||
pytest.param("..%2F..%2F..%2Fsecrets.yaml", id="url_encoded"),
|
pytest.param("..%2F..%2F..%2Fsecrets.yaml", 403, id="url_encoded"),
|
||||||
pytest.param("zephyr/../../../secrets.yaml", id="traversal_with_prefix"),
|
pytest.param("zephyr/../../../secrets.yaml", 403, id="traversal_with_prefix"),
|
||||||
pytest.param("/etc/passwd", id="absolute_path"),
|
pytest.param("/etc/passwd", 403, id="absolute_path"),
|
||||||
pytest.param("//etc/passwd", id="double_slash_absolute"),
|
pytest.param("//etc/passwd", 403, id="double_slash_absolute"),
|
||||||
pytest.param("....//secrets.yaml", id="multiple_dots"),
|
pytest.param(
|
||||||
|
"....//secrets.yaml",
|
||||||
|
# On Windows, Path.resolve() treats "..." and "...." as parent
|
||||||
|
# traversal (like ".."), so the path escapes base_dir -> 403.
|
||||||
|
# On Unix, "...." is a literal directory name that stays inside
|
||||||
|
# base_dir but doesn't exist -> 404.
|
||||||
|
403 if sys.platform == "win32" else 404,
|
||||||
|
id="multiple_dots",
|
||||||
|
),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
async def test_download_binary_handler_path_traversal_protection(
|
async def test_download_binary_handler_path_traversal_protection(
|
||||||
@@ -543,11 +553,14 @@ async def test_download_binary_handler_path_traversal_protection(
|
|||||||
tmp_path: Path,
|
tmp_path: Path,
|
||||||
mock_storage_json: MagicMock,
|
mock_storage_json: MagicMock,
|
||||||
attack_path: str,
|
attack_path: str,
|
||||||
|
expected_code: int,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test that DownloadBinaryRequestHandler prevents path traversal attacks.
|
"""Test that DownloadBinaryRequestHandler prevents path traversal attacks.
|
||||||
|
|
||||||
Verifies that attempts to use '..' in file paths are sanitized to prevent
|
Verifies that attempts to escape the build directory via '..' are rejected
|
||||||
accessing files outside the build directory. Tests multiple attack vectors.
|
using resolve()/relative_to() validation. Tests multiple attack vectors.
|
||||||
|
Real traversals that escape the base directory get 403. Paths like '....'
|
||||||
|
that resolve inside the base directory but don't exist get 404.
|
||||||
"""
|
"""
|
||||||
# Create build structure
|
# Create build structure
|
||||||
build_dir = get_build_path(tmp_path, "test")
|
build_dir = get_build_path(tmp_path, "test")
|
||||||
@@ -565,14 +578,67 @@ async def test_download_binary_handler_path_traversal_protection(
|
|||||||
mock_storage.firmware_bin_path = firmware_file
|
mock_storage.firmware_bin_path = firmware_file
|
||||||
mock_storage_json.load.return_value = mock_storage
|
mock_storage_json.load.return_value = mock_storage
|
||||||
|
|
||||||
# Attempt path traversal attack - should be blocked
|
# Mock async_run_system_command so paths that pass validation but don't exist
|
||||||
with pytest.raises(HTTPClientError) as exc_info:
|
# return 404 deterministically without spawning a real subprocess.
|
||||||
|
with (
|
||||||
|
patch(
|
||||||
|
"esphome.dashboard.web_server.async_run_system_command",
|
||||||
|
new_callable=AsyncMock,
|
||||||
|
return_value=(2, "", ""),
|
||||||
|
),
|
||||||
|
pytest.raises(HTTPClientError) as exc_info,
|
||||||
|
):
|
||||||
await dashboard.fetch(
|
await dashboard.fetch(
|
||||||
f"/download.bin?configuration=test.yaml&file={attack_path}",
|
f"/download.bin?configuration=test.yaml&file={attack_path}",
|
||||||
method="GET",
|
method="GET",
|
||||||
)
|
)
|
||||||
# Should get 404 (file not found after sanitization) or 500 (idedata fails)
|
assert exc_info.value.code == expected_code
|
||||||
assert exc_info.value.code in (404, 500)
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
@pytest.mark.usefixtures("mock_ext_storage_path")
|
||||||
|
async def test_download_binary_handler_no_firmware_bin_path(
|
||||||
|
dashboard: DashboardTestHelper,
|
||||||
|
mock_storage_json: MagicMock,
|
||||||
|
) -> None:
|
||||||
|
"""Test that download returns 404 when firmware_bin_path is None.
|
||||||
|
|
||||||
|
This covers configs created by StorageJSON.from_wizard() where no
|
||||||
|
firmware has been compiled yet.
|
||||||
|
"""
|
||||||
|
mock_storage = Mock()
|
||||||
|
mock_storage.name = "test_device"
|
||||||
|
mock_storage.firmware_bin_path = None
|
||||||
|
mock_storage_json.load.return_value = mock_storage
|
||||||
|
|
||||||
|
with pytest.raises(HTTPClientError) as exc_info:
|
||||||
|
await dashboard.fetch(
|
||||||
|
"/download.bin?configuration=test.yaml&file=firmware.bin",
|
||||||
|
method="GET",
|
||||||
|
)
|
||||||
|
assert exc_info.value.code == 404
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
@pytest.mark.usefixtures("mock_ext_storage_path")
|
||||||
|
@pytest.mark.parametrize("file_value", ["", "%20%20", "%20"])
|
||||||
|
async def test_download_binary_handler_empty_file_name(
|
||||||
|
dashboard: DashboardTestHelper,
|
||||||
|
mock_storage_json: MagicMock,
|
||||||
|
file_value: str,
|
||||||
|
) -> None:
|
||||||
|
"""Test that download returns 400 for empty or whitespace-only file names."""
|
||||||
|
mock_storage = Mock()
|
||||||
|
mock_storage.name = "test_device"
|
||||||
|
mock_storage.firmware_bin_path = Path("/fake/firmware.bin")
|
||||||
|
mock_storage_json.load.return_value = mock_storage
|
||||||
|
|
||||||
|
with pytest.raises(HTTPClientError) as exc_info:
|
||||||
|
await dashboard.fetch(
|
||||||
|
f"/download.bin?configuration=test.yaml&file={file_value}",
|
||||||
|
method="GET",
|
||||||
|
)
|
||||||
|
assert exc_info.value.code == 400
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
@@ -1654,3 +1720,107 @@ async def test_websocket_check_origin_multiple_trusted_domains(
|
|||||||
assert data["event"] == "initial_state"
|
assert data["event"] == "initial_state"
|
||||||
finally:
|
finally:
|
||||||
ws.close()
|
ws.close()
|
||||||
|
|
||||||
|
|
||||||
|
def test_proc_on_exit_calls_close() -> None:
|
||||||
|
"""Test _proc_on_exit sends exit event and closes the WebSocket."""
|
||||||
|
handler = Mock(spec=EsphomeCommandWebSocket)
|
||||||
|
handler._is_closed = False
|
||||||
|
|
||||||
|
EsphomeCommandWebSocket._proc_on_exit(handler, 0)
|
||||||
|
|
||||||
|
handler.write_message.assert_called_once_with({"event": "exit", "code": 0})
|
||||||
|
handler.close.assert_called_once()
|
||||||
|
|
||||||
|
|
||||||
|
def test_proc_on_exit_skips_when_already_closed() -> None:
|
||||||
|
"""Test _proc_on_exit does nothing when WebSocket is already closed."""
|
||||||
|
handler = Mock(spec=EsphomeCommandWebSocket)
|
||||||
|
handler._is_closed = True
|
||||||
|
|
||||||
|
EsphomeCommandWebSocket._proc_on_exit(handler, 0)
|
||||||
|
|
||||||
|
handler.write_message.assert_not_called()
|
||||||
|
handler.close.assert_not_called()
|
||||||
|
|
||||||
|
|
||||||
|
def _make_auth_handler(auth_header: str | None = None) -> Mock:
|
||||||
|
"""Create a mock handler with the given Authorization header."""
|
||||||
|
handler = Mock()
|
||||||
|
handler.request = Mock()
|
||||||
|
if auth_header is not None:
|
||||||
|
handler.request.headers = {"Authorization": auth_header}
|
||||||
|
else:
|
||||||
|
handler.request.headers = {}
|
||||||
|
handler.get_secure_cookie = Mock(return_value=None)
|
||||||
|
return handler
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def mock_auth_settings(mock_dashboard_settings: MagicMock) -> MagicMock:
|
||||||
|
"""Fixture to configure mock dashboard settings with auth enabled."""
|
||||||
|
mock_dashboard_settings.using_auth = True
|
||||||
|
mock_dashboard_settings.on_ha_addon = False
|
||||||
|
return mock_dashboard_settings
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.usefixtures("mock_auth_settings")
|
||||||
|
def test_is_authenticated_malformed_base64() -> None:
|
||||||
|
"""Test that invalid base64 in Authorization header returns False."""
|
||||||
|
handler = _make_auth_handler("Basic !!!not-valid-base64!!!")
|
||||||
|
assert web_server.is_authenticated(handler) is False
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.usefixtures("mock_auth_settings")
|
||||||
|
def test_is_authenticated_bad_base64_padding() -> None:
|
||||||
|
"""Test that incorrect base64 padding (binascii.Error) returns False."""
|
||||||
|
handler = _make_auth_handler("Basic abc")
|
||||||
|
assert web_server.is_authenticated(handler) is False
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.usefixtures("mock_auth_settings")
|
||||||
|
def test_is_authenticated_invalid_utf8() -> None:
|
||||||
|
"""Test that base64 decoding to invalid UTF-8 returns False."""
|
||||||
|
# \xff\xfe is invalid UTF-8
|
||||||
|
bad_payload = base64.b64encode(b"\xff\xfe").decode("ascii")
|
||||||
|
handler = _make_auth_handler(f"Basic {bad_payload}")
|
||||||
|
assert web_server.is_authenticated(handler) is False
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.usefixtures("mock_auth_settings")
|
||||||
|
def test_is_authenticated_no_colon() -> None:
|
||||||
|
"""Test that base64 payload without ':' separator returns False."""
|
||||||
|
no_colon = base64.b64encode(b"nocolonhere").decode("ascii")
|
||||||
|
handler = _make_auth_handler(f"Basic {no_colon}")
|
||||||
|
assert web_server.is_authenticated(handler) is False
|
||||||
|
|
||||||
|
|
||||||
|
def test_is_authenticated_valid_credentials(
|
||||||
|
mock_auth_settings: MagicMock,
|
||||||
|
) -> None:
|
||||||
|
"""Test that valid Basic auth credentials are checked."""
|
||||||
|
creds = base64.b64encode(b"admin:secret").decode("ascii")
|
||||||
|
mock_auth_settings.check_password.return_value = True
|
||||||
|
handler = _make_auth_handler(f"Basic {creds}")
|
||||||
|
assert web_server.is_authenticated(handler) is True
|
||||||
|
mock_auth_settings.check_password.assert_called_once_with("admin", "secret")
|
||||||
|
|
||||||
|
|
||||||
|
def test_is_authenticated_wrong_credentials(
|
||||||
|
mock_auth_settings: MagicMock,
|
||||||
|
) -> None:
|
||||||
|
"""Test that valid Basic auth with wrong credentials returns False."""
|
||||||
|
creds = base64.b64encode(b"admin:wrong").decode("ascii")
|
||||||
|
mock_auth_settings.check_password.return_value = False
|
||||||
|
handler = _make_auth_handler(f"Basic {creds}")
|
||||||
|
assert web_server.is_authenticated(handler) is False
|
||||||
|
|
||||||
|
|
||||||
|
def test_is_authenticated_no_auth_configured(
|
||||||
|
mock_dashboard_settings: MagicMock,
|
||||||
|
) -> None:
|
||||||
|
"""Test that requests pass when auth is not configured."""
|
||||||
|
mock_dashboard_settings.using_auth = False
|
||||||
|
mock_dashboard_settings.on_ha_addon = False
|
||||||
|
handler = _make_auth_handler()
|
||||||
|
assert web_server.is_authenticated(handler) is True
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ water_heater:
|
|||||||
name: Test Boiler
|
name: Test Boiler
|
||||||
optimistic: true
|
optimistic: true
|
||||||
current_temperature: !lambda "return 45.0f;"
|
current_temperature: !lambda "return 45.0f;"
|
||||||
|
target_temperature: !lambda "return 60.0f;"
|
||||||
# Note: No mode lambda - we want optimistic mode changes to stick
|
# Note: No mode lambda - we want optimistic mode changes to stick
|
||||||
# A mode lambda would override mode changes in loop()
|
# A mode lambda would override mode changes in loop()
|
||||||
supported_modes:
|
supported_modes:
|
||||||
|
|||||||
@@ -93,23 +93,34 @@ async def udp_listener(port: int = 0) -> AsyncGenerator[tuple[int, UDPReceiver]]
|
|||||||
sock.close()
|
sock.close()
|
||||||
|
|
||||||
|
|
||||||
|
def _get_free_udp_port() -> int:
|
||||||
|
"""Get a free UDP port by binding to port 0 and releasing."""
|
||||||
|
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||||
|
sock.bind(("127.0.0.1", 0))
|
||||||
|
port = sock.getsockname()[1]
|
||||||
|
sock.close()
|
||||||
|
return port
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_udp_send_receive(
|
async def test_udp_send_receive(
|
||||||
yaml_config: str,
|
yaml_config: str,
|
||||||
run_compiled: RunCompiledFunction,
|
run_compiled: RunCompiledFunction,
|
||||||
api_client_connected: APIClientConnectedFactory,
|
api_client_connected: APIClientConnectedFactory,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test UDP component can send messages with multiple addresses configured."""
|
"""Test UDP component can send and receive messages."""
|
||||||
# Track log lines to verify dump_config output
|
|
||||||
log_lines: list[str] = []
|
log_lines: list[str] = []
|
||||||
|
receive_event = asyncio.Event()
|
||||||
|
|
||||||
def on_log_line(line: str) -> None:
|
def on_log_line(line: str) -> None:
|
||||||
log_lines.append(line)
|
log_lines.append(line)
|
||||||
|
if "Received UDP:" in line:
|
||||||
|
receive_event.set()
|
||||||
|
|
||||||
async with udp_listener() as (udp_port, receiver):
|
async with udp_listener() as (broadcast_port, receiver):
|
||||||
# Replace placeholders in the config
|
listen_port = _get_free_udp_port()
|
||||||
config = yaml_config.replace("UDP_LISTEN_PORT_PLACEHOLDER", str(udp_port + 1))
|
config = yaml_config.replace("UDP_LISTEN_PORT_PLACEHOLDER", str(listen_port))
|
||||||
config = config.replace("UDP_BROADCAST_PORT_PLACEHOLDER", str(udp_port))
|
config = config.replace("UDP_BROADCAST_PORT_PLACEHOLDER", str(broadcast_port))
|
||||||
|
|
||||||
async with (
|
async with (
|
||||||
run_compiled(config, line_callback=on_log_line),
|
run_compiled(config, line_callback=on_log_line),
|
||||||
@@ -169,3 +180,19 @@ async def test_udp_send_receive(
|
|||||||
assert "Address: 127.0.0.2" in log_text, (
|
assert "Address: 127.0.0.2" in log_text, (
|
||||||
f"Address 127.0.0.2 not found in dump_config. Log: {log_text[-2000:]}"
|
f"Address 127.0.0.2 not found in dump_config. Log: {log_text[-2000:]}"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Test receiving a UDP packet (exercises on_receive with std::span)
|
||||||
|
test_payload = b"TEST_RECEIVE_UDP"
|
||||||
|
send_sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||||
|
try:
|
||||||
|
send_sock.sendto(test_payload, ("127.0.0.1", listen_port))
|
||||||
|
finally:
|
||||||
|
send_sock.close()
|
||||||
|
|
||||||
|
try:
|
||||||
|
await asyncio.wait_for(receive_event.wait(), timeout=5.0)
|
||||||
|
except TimeoutError:
|
||||||
|
pytest.fail(
|
||||||
|
f"on_receive did not fire. Expected 'Received UDP:' in logs. "
|
||||||
|
f"Last log lines: {log_lines[-20:]}"
|
||||||
|
)
|
||||||
|
|||||||
@@ -85,6 +85,9 @@ async def test_water_heater_template(
|
|||||||
assert initial_state.current_temperature == 45.0, (
|
assert initial_state.current_temperature == 45.0, (
|
||||||
f"Expected current temp 45.0, got {initial_state.current_temperature}"
|
f"Expected current temp 45.0, got {initial_state.current_temperature}"
|
||||||
)
|
)
|
||||||
|
assert initial_state.target_temperature == 60.0, (
|
||||||
|
f"Expected target temp 60.0, got {initial_state.target_temperature}"
|
||||||
|
)
|
||||||
|
|
||||||
# Test changing to GAS mode
|
# Test changing to GAS mode
|
||||||
client.water_heater_command(test_water_heater.key, mode=WaterHeaterMode.GAS)
|
client.water_heater_command(test_water_heater.key, mode=WaterHeaterMode.GAS)
|
||||||
|
|||||||
@@ -18,8 +18,8 @@ from esphome import espota2
|
|||||||
from esphome.core import EsphomeError
|
from esphome.core import EsphomeError
|
||||||
|
|
||||||
# Test constants
|
# Test constants
|
||||||
MOCK_RANDOM_VALUE = 0.123456
|
MOCK_MD5_CNONCE = "a" * 32 # Mock 32-char hex string from secrets.token_hex(16)
|
||||||
MOCK_RANDOM_BYTES = b"0.123456"
|
MOCK_SHA256_CNONCE = "b" * 64 # Mock 64-char hex string from secrets.token_hex(32)
|
||||||
MOCK_MD5_NONCE = b"12345678901234567890123456789012" # 32 char nonce for MD5
|
MOCK_MD5_NONCE = b"12345678901234567890123456789012" # 32 char nonce for MD5
|
||||||
MOCK_SHA256_NONCE = b"1234567890123456789012345678901234567890123456789012345678901234" # 64 char nonce for SHA256
|
MOCK_SHA256_NONCE = b"1234567890123456789012345678901234567890123456789012345678901234" # 64 char nonce for SHA256
|
||||||
|
|
||||||
@@ -55,10 +55,18 @@ def mock_time() -> Generator[None]:
|
|||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def mock_random() -> Generator[Mock]:
|
def mock_token_hex() -> Generator[Mock]:
|
||||||
"""Mock random for predictable test values."""
|
"""Mock secrets.token_hex for predictable test values."""
|
||||||
with patch("random.random", return_value=MOCK_RANDOM_VALUE) as mock_rand:
|
|
||||||
yield mock_rand
|
def _token_hex(nbytes: int) -> str:
|
||||||
|
if nbytes == 16:
|
||||||
|
return MOCK_MD5_CNONCE
|
||||||
|
if nbytes == 32:
|
||||||
|
return MOCK_SHA256_CNONCE
|
||||||
|
raise ValueError(f"Unexpected nbytes for token_hex mock: {nbytes}")
|
||||||
|
|
||||||
|
with patch("esphome.espota2.secrets.token_hex", side_effect=_token_hex) as mock:
|
||||||
|
yield mock
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
@@ -236,7 +244,7 @@ def test_send_check_socket_error(mock_socket: Mock) -> None:
|
|||||||
|
|
||||||
@pytest.mark.usefixtures("mock_time")
|
@pytest.mark.usefixtures("mock_time")
|
||||||
def test_perform_ota_successful_md5_auth(
|
def test_perform_ota_successful_md5_auth(
|
||||||
mock_socket: Mock, mock_file: io.BytesIO, mock_random: Mock
|
mock_socket: Mock, mock_file: io.BytesIO, mock_token_hex: Mock
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test successful OTA with MD5 authentication."""
|
"""Test successful OTA with MD5 authentication."""
|
||||||
# Setup socket responses for recv calls
|
# Setup socket responses for recv calls
|
||||||
@@ -272,8 +280,11 @@ def test_perform_ota_successful_md5_auth(
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
# Verify cnonce was sent (MD5 of random.random())
|
# Verify token_hex was called with MD5 digest size
|
||||||
cnonce = hashlib.md5(MOCK_RANDOM_BYTES).hexdigest()
|
mock_token_hex.assert_called_once_with(16)
|
||||||
|
|
||||||
|
# Verify cnonce was sent
|
||||||
|
cnonce = MOCK_MD5_CNONCE
|
||||||
assert mock_socket.sendall.call_args_list[2] == call(cnonce.encode())
|
assert mock_socket.sendall.call_args_list[2] == call(cnonce.encode())
|
||||||
|
|
||||||
# Verify auth result was computed correctly
|
# Verify auth result was computed correctly
|
||||||
@@ -366,7 +377,7 @@ def test_perform_ota_auth_without_password(mock_socket: Mock) -> None:
|
|||||||
|
|
||||||
@pytest.mark.usefixtures("mock_time")
|
@pytest.mark.usefixtures("mock_time")
|
||||||
def test_perform_ota_md5_auth_wrong_password(
|
def test_perform_ota_md5_auth_wrong_password(
|
||||||
mock_socket: Mock, mock_file: io.BytesIO, mock_random: Mock
|
mock_socket: Mock, mock_file: io.BytesIO, mock_token_hex: Mock
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test OTA fails when MD5 authentication is rejected due to wrong password."""
|
"""Test OTA fails when MD5 authentication is rejected due to wrong password."""
|
||||||
# Setup socket responses for recv calls
|
# Setup socket responses for recv calls
|
||||||
@@ -390,7 +401,7 @@ def test_perform_ota_md5_auth_wrong_password(
|
|||||||
|
|
||||||
@pytest.mark.usefixtures("mock_time")
|
@pytest.mark.usefixtures("mock_time")
|
||||||
def test_perform_ota_sha256_auth_wrong_password(
|
def test_perform_ota_sha256_auth_wrong_password(
|
||||||
mock_socket: Mock, mock_file: io.BytesIO, mock_random: Mock
|
mock_socket: Mock, mock_file: io.BytesIO, mock_token_hex: Mock
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test OTA fails when SHA256 authentication is rejected due to wrong password."""
|
"""Test OTA fails when SHA256 authentication is rejected due to wrong password."""
|
||||||
# Setup socket responses for recv calls
|
# Setup socket responses for recv calls
|
||||||
@@ -603,7 +614,7 @@ def test_progress_bar(capsys: CaptureFixture[str]) -> None:
|
|||||||
# Tests for SHA256 authentication
|
# Tests for SHA256 authentication
|
||||||
@pytest.mark.usefixtures("mock_time")
|
@pytest.mark.usefixtures("mock_time")
|
||||||
def test_perform_ota_successful_sha256_auth(
|
def test_perform_ota_successful_sha256_auth(
|
||||||
mock_socket: Mock, mock_file: io.BytesIO, mock_random: Mock
|
mock_socket: Mock, mock_file: io.BytesIO, mock_token_hex: Mock
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test successful OTA with SHA256 authentication."""
|
"""Test successful OTA with SHA256 authentication."""
|
||||||
# Setup socket responses for recv calls
|
# Setup socket responses for recv calls
|
||||||
@@ -639,8 +650,11 @@ def test_perform_ota_successful_sha256_auth(
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
# Verify cnonce was sent (SHA256 of random.random())
|
# Verify token_hex was called with SHA256 digest size
|
||||||
cnonce = hashlib.sha256(MOCK_RANDOM_BYTES).hexdigest()
|
mock_token_hex.assert_called_once_with(32)
|
||||||
|
|
||||||
|
# Verify cnonce was sent
|
||||||
|
cnonce = MOCK_SHA256_CNONCE
|
||||||
assert mock_socket.sendall.call_args_list[2] == call(cnonce.encode())
|
assert mock_socket.sendall.call_args_list[2] == call(cnonce.encode())
|
||||||
|
|
||||||
# Verify auth result was computed correctly with SHA256
|
# Verify auth result was computed correctly with SHA256
|
||||||
@@ -654,7 +668,7 @@ def test_perform_ota_successful_sha256_auth(
|
|||||||
|
|
||||||
@pytest.mark.usefixtures("mock_time")
|
@pytest.mark.usefixtures("mock_time")
|
||||||
def test_perform_ota_sha256_fallback_to_md5(
|
def test_perform_ota_sha256_fallback_to_md5(
|
||||||
mock_socket: Mock, mock_file: io.BytesIO, mock_random: Mock
|
mock_socket: Mock, mock_file: io.BytesIO, mock_token_hex: Mock
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test SHA256-capable client falls back to MD5 for compatibility."""
|
"""Test SHA256-capable client falls back to MD5 for compatibility."""
|
||||||
# This test verifies the temporary backward compatibility
|
# This test verifies the temporary backward compatibility
|
||||||
@@ -692,7 +706,8 @@ def test_perform_ota_sha256_fallback_to_md5(
|
|||||||
)
|
)
|
||||||
|
|
||||||
# But authentication was done with MD5
|
# But authentication was done with MD5
|
||||||
cnonce = hashlib.md5(MOCK_RANDOM_BYTES).hexdigest()
|
mock_token_hex.assert_called_once_with(16)
|
||||||
|
cnonce = MOCK_MD5_CNONCE
|
||||||
expected_hash = hashlib.md5()
|
expected_hash = hashlib.md5()
|
||||||
expected_hash.update(b"testpass")
|
expected_hash.update(b"testpass")
|
||||||
expected_hash.update(MOCK_MD5_NONCE)
|
expected_hash.update(MOCK_MD5_NONCE)
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user