mirror of
https://github.com/esphome/esphome.git
synced 2026-02-10 09:42:01 +00:00
Compare commits
31 Commits
http-reque
...
compact_st
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ce5b93d9ca | ||
|
|
6f9eee42d6 | ||
|
|
a3c2248b44 | ||
|
|
d75254309f | ||
|
|
5d1acb0cb8 | ||
|
|
cafc7651c2 | ||
|
|
4099e944d6 | ||
|
|
5ad989a13a | ||
|
|
7336985753 | ||
|
|
73d076c278 | ||
|
|
3a2c66171b | ||
|
|
fca867e18d | ||
|
|
0ae90512cf | ||
|
|
165f81dc97 | ||
|
|
dc971b4ed0 | ||
|
|
a4fe9852aa | ||
|
|
f6ec5e9c28 | ||
|
|
0051196e86 | ||
|
|
9f83b24913 | ||
|
|
5c0747cfe0 | ||
|
|
f0c7306ad5 | ||
|
|
09b42b778b | ||
|
|
d610c3ae91 | ||
|
|
687f9a762d | ||
|
|
acb22ed286 | ||
|
|
692167341e | ||
|
|
d5d6936845 | ||
|
|
bffe4a2e05 | ||
|
|
d7c3947ccc | ||
|
|
6f3a49e509 | ||
|
|
7aef173e65 |
@@ -43,7 +43,6 @@ _READELF_SECTION_PATTERN = re.compile(
|
||||
# Component category prefixes
|
||||
_COMPONENT_PREFIX_ESPHOME = "[esphome]"
|
||||
_COMPONENT_PREFIX_EXTERNAL = "[external]"
|
||||
_COMPONENT_PREFIX_LIB = "[lib]"
|
||||
_COMPONENT_CORE = f"{_COMPONENT_PREFIX_ESPHOME}core"
|
||||
_COMPONENT_API = f"{_COMPONENT_PREFIX_ESPHOME}api"
|
||||
|
||||
@@ -57,16 +56,6 @@ SymbolInfoType = tuple[str, int, str]
|
||||
# RAM sections - symbols in these sections consume RAM
|
||||
RAM_SECTIONS = frozenset([".data", ".bss"])
|
||||
|
||||
# nm symbol types for global/weak defined symbols (used for library symbol mapping)
|
||||
# Only global (uppercase) and weak symbols are safe to use - local symbols (lowercase)
|
||||
# can have name collisions across compilation units
|
||||
_NM_DEFINED_GLOBAL_TYPES = frozenset({"T", "D", "B", "R", "W", "V"})
|
||||
|
||||
# Pattern matching compiler-generated local names that can collide across compilation
|
||||
# units (e.g., packet$19, buf$20, flag$5261). These are unsafe for name-based lookup.
|
||||
# Does NOT match mangled C++ names with optimization suffixes (e.g., func$isra$0).
|
||||
_COMPILER_LOCAL_PATTERN = re.compile(r"^[a-zA-Z_]\w*\$\d+$")
|
||||
|
||||
|
||||
@dataclass
|
||||
class MemorySection:
|
||||
@@ -190,19 +179,11 @@ class MemoryAnalyzer:
|
||||
self._sdk_symbols: list[SDKSymbol] = []
|
||||
# CSWTCH symbols: list of (name, size, source_file, component)
|
||||
self._cswtch_symbols: list[tuple[str, int, str, str]] = []
|
||||
# Library symbol mapping: symbol_name -> library_name
|
||||
self._lib_symbol_map: dict[str, str] = {}
|
||||
# Library dir to name mapping: "lib641" -> "espsoftwareserial",
|
||||
# "espressif__mdns" -> "mdns"
|
||||
self._lib_hash_to_name: dict[str, str] = {}
|
||||
# Heuristic category to library redirect: "mdns_lib" -> "[lib]mdns"
|
||||
self._heuristic_to_lib: dict[str, str] = {}
|
||||
|
||||
def analyze(self) -> dict[str, ComponentMemory]:
|
||||
"""Analyze the ELF file and return component memory usage."""
|
||||
self._parse_sections()
|
||||
self._parse_symbols()
|
||||
self._scan_libraries()
|
||||
self._categorize_symbols()
|
||||
self._analyze_cswtch_symbols()
|
||||
self._analyze_sdk_libraries()
|
||||
@@ -347,19 +328,15 @@ class MemoryAnalyzer:
|
||||
# If no component match found, it's core
|
||||
return _COMPONENT_CORE
|
||||
|
||||
# Check library symbol map (more accurate than heuristic patterns)
|
||||
if lib_name := self._lib_symbol_map.get(symbol_name):
|
||||
return f"{_COMPONENT_PREFIX_LIB}{lib_name}"
|
||||
|
||||
# Check against symbol patterns
|
||||
for component, patterns in SYMBOL_PATTERNS.items():
|
||||
if any(pattern in symbol_name for pattern in patterns):
|
||||
return self._heuristic_to_lib.get(component, component)
|
||||
return component
|
||||
|
||||
# Check against demangled patterns
|
||||
for component, patterns in DEMANGLED_PATTERNS.items():
|
||||
if any(pattern in demangled for pattern in patterns):
|
||||
return self._heuristic_to_lib.get(component, component)
|
||||
return component
|
||||
|
||||
# Special cases that need more complex logic
|
||||
|
||||
@@ -407,327 +384,6 @@ class MemoryAnalyzer:
|
||||
|
||||
return "Other Core"
|
||||
|
||||
def _discover_pio_libraries(
|
||||
self,
|
||||
libraries: dict[str, list[Path]],
|
||||
hash_to_name: dict[str, str],
|
||||
) -> None:
|
||||
"""Discover PlatformIO third-party libraries from the build directory.
|
||||
|
||||
Scans ``lib<hex>/`` directories under ``.pioenvs/<env>/`` to find
|
||||
library names and their ``.a`` archive or ``.o`` file paths.
|
||||
|
||||
Args:
|
||||
libraries: Dict to populate with library name -> file path list mappings.
|
||||
Prefers ``.a`` archives when available, falls back to ``.o`` files
|
||||
(e.g., pioarduino ESP32 Arduino builds only produce ``.o`` files).
|
||||
hash_to_name: Dict to populate with dir name -> library name mappings
|
||||
for CSWTCH attribution (e.g., ``lib641`` -> ``espsoftwareserial``).
|
||||
"""
|
||||
build_dir = self.elf_path.parent
|
||||
|
||||
for entry in build_dir.iterdir():
|
||||
if not entry.is_dir() or not entry.name.startswith("lib"):
|
||||
continue
|
||||
# Validate that the suffix after "lib" is a hex hash
|
||||
hex_part = entry.name[3:]
|
||||
if not hex_part:
|
||||
continue
|
||||
try:
|
||||
int(hex_part, 16)
|
||||
except ValueError:
|
||||
continue
|
||||
|
||||
# Each lib<hex>/ directory contains a subdirectory named after the library
|
||||
for lib_subdir in entry.iterdir():
|
||||
if not lib_subdir.is_dir():
|
||||
continue
|
||||
lib_name = lib_subdir.name.lower()
|
||||
|
||||
# Prefer .a archive (lib<LibraryName>.a), fall back to .o files
|
||||
# e.g., lib72a/ESPAsyncTCP/... has lib72a/libESPAsyncTCP.a
|
||||
archive = entry / f"lib{lib_subdir.name}.a"
|
||||
if archive.exists():
|
||||
file_paths = [archive]
|
||||
elif archives := list(entry.glob("*.a")):
|
||||
# Case-insensitive fallback
|
||||
file_paths = [archives[0]]
|
||||
else:
|
||||
# No .a archive (e.g., pioarduino CMake builds) - use .o files
|
||||
file_paths = sorted(lib_subdir.rglob("*.o"))
|
||||
|
||||
if file_paths:
|
||||
libraries[lib_name] = file_paths
|
||||
hash_to_name[entry.name] = lib_name
|
||||
_LOGGER.debug(
|
||||
"Discovered PlatformIO library: %s -> %s",
|
||||
lib_subdir.name,
|
||||
file_paths[0],
|
||||
)
|
||||
|
||||
def _discover_idf_managed_components(
|
||||
self,
|
||||
libraries: dict[str, list[Path]],
|
||||
hash_to_name: dict[str, str],
|
||||
) -> None:
|
||||
"""Discover ESP-IDF managed component libraries from the build directory.
|
||||
|
||||
ESP-IDF managed components (from the IDF component registry) use a
|
||||
``<vendor>__<name>`` naming convention. Source files live under
|
||||
``managed_components/<vendor>__<name>/`` and the compiled archives are at
|
||||
``esp-idf/<vendor>__<name>/lib<vendor>__<name>.a``.
|
||||
|
||||
Args:
|
||||
libraries: Dict to populate with library name -> file path list mappings.
|
||||
hash_to_name: Dict to populate with dir name -> library name mappings
|
||||
for CSWTCH attribution (e.g., ``espressif__mdns`` -> ``mdns``).
|
||||
"""
|
||||
build_dir = self.elf_path.parent
|
||||
|
||||
managed_dir = build_dir / "managed_components"
|
||||
if not managed_dir.is_dir():
|
||||
return
|
||||
|
||||
espidf_dir = build_dir / "esp-idf"
|
||||
|
||||
for entry in managed_dir.iterdir():
|
||||
if not entry.is_dir() or "__" not in entry.name:
|
||||
continue
|
||||
|
||||
# Extract the short name: espressif__mdns -> mdns
|
||||
full_name = entry.name # e.g., espressif__mdns
|
||||
short_name = full_name.split("__", 1)[1].lower()
|
||||
|
||||
# Find the .a archive under esp-idf/<vendor>__<name>/
|
||||
archive = espidf_dir / full_name / f"lib{full_name}.a"
|
||||
if archive.exists():
|
||||
libraries[short_name] = [archive]
|
||||
hash_to_name[full_name] = short_name
|
||||
_LOGGER.debug(
|
||||
"Discovered IDF managed component: %s -> %s",
|
||||
short_name,
|
||||
archive,
|
||||
)
|
||||
|
||||
def _build_library_symbol_map(
|
||||
self, libraries: dict[str, list[Path]]
|
||||
) -> dict[str, str]:
|
||||
"""Build a symbol-to-library mapping from library archives or object files.
|
||||
|
||||
Runs ``nm --defined-only`` on each ``.a`` or ``.o`` file to collect
|
||||
global and weak defined symbols.
|
||||
|
||||
Args:
|
||||
libraries: Dictionary mapping library name to list of file paths
|
||||
(``.a`` archives or ``.o`` object files).
|
||||
|
||||
Returns:
|
||||
Dictionary mapping symbol name to library name.
|
||||
"""
|
||||
symbol_map: dict[str, str] = {}
|
||||
|
||||
if not self.nm_path:
|
||||
return symbol_map
|
||||
|
||||
for lib_name, file_paths in libraries.items():
|
||||
result = run_tool(
|
||||
[self.nm_path, "--defined-only", *(str(p) for p in file_paths)],
|
||||
timeout=10,
|
||||
)
|
||||
if result is None or result.returncode != 0:
|
||||
continue
|
||||
|
||||
for line in result.stdout.splitlines():
|
||||
parts = line.split()
|
||||
if len(parts) < 3:
|
||||
continue
|
||||
|
||||
sym_type = parts[-2]
|
||||
sym_name = parts[-1]
|
||||
|
||||
# Include global defined symbols (uppercase) and weak symbols (W/V)
|
||||
if sym_type in _NM_DEFINED_GLOBAL_TYPES:
|
||||
symbol_map[sym_name] = lib_name
|
||||
|
||||
return symbol_map
|
||||
|
||||
@staticmethod
|
||||
def _build_heuristic_to_lib_mapping(
|
||||
library_names: set[str],
|
||||
) -> dict[str, str]:
|
||||
"""Build mapping from heuristic pattern categories to discovered libraries.
|
||||
|
||||
Heuristic categories like ``mdns_lib``, ``web_server_lib``, ``async_tcp``
|
||||
exist as approximations for library attribution. When we discover the
|
||||
actual library, symbols matching those heuristics should be redirected
|
||||
to the ``[lib]`` category instead.
|
||||
|
||||
The mapping is built by checking if the normalized category name
|
||||
(stripped of ``_lib`` suffix and underscores) appears as a substring
|
||||
of any discovered library name.
|
||||
|
||||
Examples::
|
||||
|
||||
mdns_lib -> mdns -> in "mdns" or "esp8266mdns" -> [lib]mdns
|
||||
web_server_lib -> webserver -> in "espasyncwebserver" -> [lib]espasyncwebserver
|
||||
async_tcp -> asynctcp -> in "espasynctcp" -> [lib]espasynctcp
|
||||
|
||||
Args:
|
||||
library_names: Set of discovered library names (lowercase).
|
||||
|
||||
Returns:
|
||||
Dictionary mapping heuristic category to ``[lib]<name>`` string.
|
||||
"""
|
||||
mapping: dict[str, str] = {}
|
||||
all_categories = set(SYMBOL_PATTERNS) | set(DEMANGLED_PATTERNS)
|
||||
|
||||
for category in all_categories:
|
||||
base = category.removesuffix("_lib").replace("_", "")
|
||||
# Collect all libraries whose name contains the base string
|
||||
candidates = [lib_name for lib_name in library_names if base in lib_name]
|
||||
if not candidates:
|
||||
continue
|
||||
|
||||
# Choose a deterministic "best" match:
|
||||
# 1. Prefer exact name matches over substring matches.
|
||||
# 2. Among non-exact matches, prefer the shortest library name.
|
||||
# 3. Break remaining ties lexicographically.
|
||||
best_lib = min(
|
||||
candidates,
|
||||
key=lambda lib_name, _base=base: (
|
||||
lib_name != _base,
|
||||
len(lib_name),
|
||||
lib_name,
|
||||
),
|
||||
)
|
||||
mapping[category] = f"{_COMPONENT_PREFIX_LIB}{best_lib}"
|
||||
|
||||
if mapping:
|
||||
_LOGGER.debug(
|
||||
"Heuristic-to-library redirects: %s",
|
||||
", ".join(f"{k} -> {v}" for k, v in sorted(mapping.items())),
|
||||
)
|
||||
|
||||
return mapping
|
||||
|
||||
def _parse_map_file(self) -> dict[str, str] | None:
|
||||
"""Parse linker map file to build authoritative symbol-to-library mapping.
|
||||
|
||||
The linker map file contains the definitive source attribution for every
|
||||
symbol, including local/static ones that ``nm`` cannot safely export.
|
||||
|
||||
Map file format (GNU ld)::
|
||||
|
||||
.text._mdns_service_task
|
||||
0x400e9fdc 0x65c .pioenvs/env/esp-idf/espressif__mdns/libespressif__mdns.a(mdns.c.o)
|
||||
|
||||
Each section entry has a ``.section.symbol_name`` line followed by an
|
||||
indented line with address, size, and source path.
|
||||
|
||||
Returns:
|
||||
Symbol-to-library dict, or ``None`` if no usable map file exists.
|
||||
"""
|
||||
map_path = self.elf_path.with_suffix(".map")
|
||||
if not map_path.exists() or map_path.stat().st_size < 10000:
|
||||
return None
|
||||
|
||||
_LOGGER.info("Parsing linker map file: %s", map_path.name)
|
||||
|
||||
try:
|
||||
map_text = map_path.read_text(encoding="utf-8", errors="replace")
|
||||
except OSError as err:
|
||||
_LOGGER.warning("Failed to read map file: %s", err)
|
||||
return None
|
||||
|
||||
symbol_map: dict[str, str] = {}
|
||||
current_symbol: str | None = None
|
||||
section_prefixes = (".text.", ".rodata.", ".data.", ".bss.", ".literal.")
|
||||
|
||||
for line in map_text.splitlines():
|
||||
# Match section.symbol line: " .text.symbol_name"
|
||||
# Single space indent, starts with dot
|
||||
if len(line) > 2 and line[0] == " " and line[1] == ".":
|
||||
stripped = line.strip()
|
||||
for prefix in section_prefixes:
|
||||
if stripped.startswith(prefix):
|
||||
current_symbol = stripped[len(prefix) :]
|
||||
break
|
||||
else:
|
||||
current_symbol = None
|
||||
continue
|
||||
|
||||
# Match source attribution line: " 0xADDR 0xSIZE source_path"
|
||||
if current_symbol is None:
|
||||
continue
|
||||
|
||||
fields = line.split()
|
||||
# Skip compiler-generated local names (e.g., packet$19, buf$20)
|
||||
# that can collide across compilation units
|
||||
if (
|
||||
len(fields) >= 3
|
||||
and fields[0].startswith("0x")
|
||||
and fields[1].startswith("0x")
|
||||
and not _COMPILER_LOCAL_PATTERN.match(current_symbol)
|
||||
):
|
||||
source_path = fields[2]
|
||||
# Check if source path contains a known library directory
|
||||
for dir_key, lib_name in self._lib_hash_to_name.items():
|
||||
if dir_key in source_path:
|
||||
symbol_map[current_symbol] = lib_name
|
||||
break
|
||||
|
||||
current_symbol = None
|
||||
|
||||
return symbol_map or None
|
||||
|
||||
def _scan_libraries(self) -> None:
|
||||
"""Discover third-party libraries and build symbol mapping.
|
||||
|
||||
Scans both PlatformIO ``lib<hex>/`` directories (Arduino builds) and
|
||||
ESP-IDF ``managed_components/`` (IDF builds) to find library archives.
|
||||
|
||||
Uses the linker map file for authoritative symbol attribution when
|
||||
available, falling back to ``nm`` scanning with heuristic redirects.
|
||||
"""
|
||||
libraries: dict[str, list[Path]] = {}
|
||||
self._discover_pio_libraries(libraries, self._lib_hash_to_name)
|
||||
self._discover_idf_managed_components(libraries, self._lib_hash_to_name)
|
||||
|
||||
if not libraries:
|
||||
_LOGGER.debug("No third-party libraries found")
|
||||
return
|
||||
|
||||
_LOGGER.info(
|
||||
"Scanning %d libraries: %s",
|
||||
len(libraries),
|
||||
", ".join(sorted(libraries)),
|
||||
)
|
||||
|
||||
# Heuristic redirect catches local symbols (e.g., mdns_task_buffer$14)
|
||||
# that can't be safely added to the symbol map due to name collisions
|
||||
self._heuristic_to_lib = self._build_heuristic_to_lib_mapping(
|
||||
set(libraries.keys())
|
||||
)
|
||||
|
||||
# Try linker map file first (authoritative, includes local symbols)
|
||||
map_symbols = self._parse_map_file()
|
||||
if map_symbols is not None:
|
||||
self._lib_symbol_map = map_symbols
|
||||
_LOGGER.info(
|
||||
"Built library symbol map from linker map: %d symbols",
|
||||
len(self._lib_symbol_map),
|
||||
)
|
||||
return
|
||||
|
||||
# Fall back to nm scanning (global symbols only)
|
||||
self._lib_symbol_map = self._build_library_symbol_map(libraries)
|
||||
|
||||
_LOGGER.info(
|
||||
"Built library symbol map from nm: %d symbols from %d libraries",
|
||||
len(self._lib_symbol_map),
|
||||
len(libraries),
|
||||
)
|
||||
|
||||
def _find_object_files_dir(self) -> Path | None:
|
||||
"""Find the directory containing object files for this build.
|
||||
|
||||
@@ -903,21 +559,9 @@ class MemoryAnalyzer:
|
||||
if "esphome" in parts and "components" not in parts:
|
||||
return _COMPONENT_CORE
|
||||
|
||||
# Framework/library files - check for PlatformIO library hash dirs
|
||||
# e.g., lib65b/ESPAsyncTCP/... -> [lib]espasynctcp
|
||||
if parts and parts[0] in self._lib_hash_to_name:
|
||||
return f"{_COMPONENT_PREFIX_LIB}{self._lib_hash_to_name[parts[0]]}"
|
||||
|
||||
# ESP-IDF managed components: managed_components/espressif__mdns/... -> [lib]mdns
|
||||
if (
|
||||
len(parts) >= 2
|
||||
and parts[0] == "managed_components"
|
||||
and parts[1] in self._lib_hash_to_name
|
||||
):
|
||||
return f"{_COMPONENT_PREFIX_LIB}{self._lib_hash_to_name[parts[1]]}"
|
||||
|
||||
# Other framework/library files - return the first path component
|
||||
# e.g., FrameworkArduino/... -> FrameworkArduino
|
||||
# Framework/library files - return the first path component
|
||||
# e.g., lib65b/ESPAsyncTCP/... -> lib65b
|
||||
# FrameworkArduino/... -> FrameworkArduino
|
||||
return parts[0] if parts else source_file
|
||||
|
||||
def _analyze_cswtch_symbols(self) -> None:
|
||||
|
||||
@@ -14,7 +14,6 @@ from . import (
|
||||
_COMPONENT_CORE,
|
||||
_COMPONENT_PREFIX_ESPHOME,
|
||||
_COMPONENT_PREFIX_EXTERNAL,
|
||||
_COMPONENT_PREFIX_LIB,
|
||||
RAM_SECTIONS,
|
||||
MemoryAnalyzer,
|
||||
)
|
||||
@@ -408,11 +407,6 @@ class MemoryAnalyzerCLI(MemoryAnalyzer):
|
||||
for name, mem in components
|
||||
if name.startswith(_COMPONENT_PREFIX_EXTERNAL)
|
||||
]
|
||||
library_components = [
|
||||
(name, mem)
|
||||
for name, mem in components
|
||||
if name.startswith(_COMPONENT_PREFIX_LIB)
|
||||
]
|
||||
|
||||
top_esphome_components = sorted(
|
||||
esphome_components, key=lambda x: x[1].flash_total, reverse=True
|
||||
@@ -423,11 +417,6 @@ class MemoryAnalyzerCLI(MemoryAnalyzer):
|
||||
external_components, key=lambda x: x[1].flash_total, reverse=True
|
||||
)
|
||||
|
||||
# Include all library components
|
||||
top_library_components = sorted(
|
||||
library_components, key=lambda x: x[1].flash_total, reverse=True
|
||||
)
|
||||
|
||||
# Check if API component exists and ensure it's included
|
||||
api_component = None
|
||||
for name, mem in components:
|
||||
@@ -446,11 +435,10 @@ class MemoryAnalyzerCLI(MemoryAnalyzer):
|
||||
if name in system_components_to_include
|
||||
]
|
||||
|
||||
# Combine all components to analyze: top ESPHome + all external + libraries + API if not already included + system components
|
||||
# Combine all components to analyze: top ESPHome + all external + API if not already included + system components
|
||||
components_to_analyze = (
|
||||
list(top_esphome_components)
|
||||
+ list(top_external_components)
|
||||
+ list(top_library_components)
|
||||
+ system_components
|
||||
)
|
||||
if api_component and api_component not in components_to_analyze:
|
||||
|
||||
@@ -283,7 +283,7 @@ void APIConnection::loop() {
|
||||
#endif
|
||||
}
|
||||
|
||||
bool APIConnection::send_disconnect_response_() {
|
||||
bool APIConnection::send_disconnect_response() {
|
||||
// remote initiated disconnect_client
|
||||
// don't close yet, we still need to send the disconnect response
|
||||
// close will happen on next loop
|
||||
@@ -406,7 +406,7 @@ uint16_t APIConnection::try_send_cover_info(EntityBase *entity, APIConnection *c
|
||||
msg.device_class = cover->get_device_class_ref();
|
||||
return fill_and_encode_entity_info(cover, msg, ListEntitiesCoverResponse::MESSAGE_TYPE, conn, remaining_size);
|
||||
}
|
||||
void APIConnection::on_cover_command_request(const CoverCommandRequest &msg) {
|
||||
void APIConnection::cover_command(const CoverCommandRequest &msg) {
|
||||
ENTITY_COMMAND_MAKE_CALL(cover::Cover, cover, cover)
|
||||
if (msg.has_position)
|
||||
call.set_position(msg.position);
|
||||
@@ -449,7 +449,7 @@ uint16_t APIConnection::try_send_fan_info(EntityBase *entity, APIConnection *con
|
||||
msg.supported_preset_modes = &traits.supported_preset_modes();
|
||||
return fill_and_encode_entity_info(fan, msg, ListEntitiesFanResponse::MESSAGE_TYPE, conn, remaining_size);
|
||||
}
|
||||
void APIConnection::on_fan_command_request(const FanCommandRequest &msg) {
|
||||
void APIConnection::fan_command(const FanCommandRequest &msg) {
|
||||
ENTITY_COMMAND_MAKE_CALL(fan::Fan, fan, fan)
|
||||
if (msg.has_state)
|
||||
call.set_state(msg.state);
|
||||
@@ -517,7 +517,7 @@ uint16_t APIConnection::try_send_light_info(EntityBase *entity, APIConnection *c
|
||||
msg.effects = &effects_list;
|
||||
return fill_and_encode_entity_info(light, msg, ListEntitiesLightResponse::MESSAGE_TYPE, conn, remaining_size);
|
||||
}
|
||||
void APIConnection::on_light_command_request(const LightCommandRequest &msg) {
|
||||
void APIConnection::light_command(const LightCommandRequest &msg) {
|
||||
ENTITY_COMMAND_MAKE_CALL(light::LightState, light, light)
|
||||
if (msg.has_state)
|
||||
call.set_state(msg.state);
|
||||
@@ -594,7 +594,7 @@ uint16_t APIConnection::try_send_switch_info(EntityBase *entity, APIConnection *
|
||||
msg.device_class = a_switch->get_device_class_ref();
|
||||
return fill_and_encode_entity_info(a_switch, msg, ListEntitiesSwitchResponse::MESSAGE_TYPE, conn, remaining_size);
|
||||
}
|
||||
void APIConnection::on_switch_command_request(const SwitchCommandRequest &msg) {
|
||||
void APIConnection::switch_command(const SwitchCommandRequest &msg) {
|
||||
ENTITY_COMMAND_GET(switch_::Switch, a_switch, switch)
|
||||
|
||||
if (msg.state) {
|
||||
@@ -692,7 +692,7 @@ uint16_t APIConnection::try_send_climate_info(EntityBase *entity, APIConnection
|
||||
msg.supported_swing_modes = &traits.get_supported_swing_modes();
|
||||
return fill_and_encode_entity_info(climate, msg, ListEntitiesClimateResponse::MESSAGE_TYPE, conn, remaining_size);
|
||||
}
|
||||
void APIConnection::on_climate_command_request(const ClimateCommandRequest &msg) {
|
||||
void APIConnection::climate_command(const ClimateCommandRequest &msg) {
|
||||
ENTITY_COMMAND_MAKE_CALL(climate::Climate, climate, climate)
|
||||
if (msg.has_mode)
|
||||
call.set_mode(static_cast<climate::ClimateMode>(msg.mode));
|
||||
@@ -742,7 +742,7 @@ uint16_t APIConnection::try_send_number_info(EntityBase *entity, APIConnection *
|
||||
msg.step = number->traits.get_step();
|
||||
return fill_and_encode_entity_info(number, msg, ListEntitiesNumberResponse::MESSAGE_TYPE, conn, remaining_size);
|
||||
}
|
||||
void APIConnection::on_number_command_request(const NumberCommandRequest &msg) {
|
||||
void APIConnection::number_command(const NumberCommandRequest &msg) {
|
||||
ENTITY_COMMAND_MAKE_CALL(number::Number, number, number)
|
||||
call.set_value(msg.state);
|
||||
call.perform();
|
||||
@@ -767,7 +767,7 @@ uint16_t APIConnection::try_send_date_info(EntityBase *entity, APIConnection *co
|
||||
ListEntitiesDateResponse msg;
|
||||
return fill_and_encode_entity_info(date, msg, ListEntitiesDateResponse::MESSAGE_TYPE, conn, remaining_size);
|
||||
}
|
||||
void APIConnection::on_date_command_request(const DateCommandRequest &msg) {
|
||||
void APIConnection::date_command(const DateCommandRequest &msg) {
|
||||
ENTITY_COMMAND_MAKE_CALL(datetime::DateEntity, date, date)
|
||||
call.set_date(msg.year, msg.month, msg.day);
|
||||
call.perform();
|
||||
@@ -792,7 +792,7 @@ uint16_t APIConnection::try_send_time_info(EntityBase *entity, APIConnection *co
|
||||
ListEntitiesTimeResponse msg;
|
||||
return fill_and_encode_entity_info(time, msg, ListEntitiesTimeResponse::MESSAGE_TYPE, conn, remaining_size);
|
||||
}
|
||||
void APIConnection::on_time_command_request(const TimeCommandRequest &msg) {
|
||||
void APIConnection::time_command(const TimeCommandRequest &msg) {
|
||||
ENTITY_COMMAND_MAKE_CALL(datetime::TimeEntity, time, time)
|
||||
call.set_time(msg.hour, msg.minute, msg.second);
|
||||
call.perform();
|
||||
@@ -819,7 +819,7 @@ uint16_t APIConnection::try_send_datetime_info(EntityBase *entity, APIConnection
|
||||
ListEntitiesDateTimeResponse msg;
|
||||
return fill_and_encode_entity_info(datetime, msg, ListEntitiesDateTimeResponse::MESSAGE_TYPE, conn, remaining_size);
|
||||
}
|
||||
void APIConnection::on_date_time_command_request(const DateTimeCommandRequest &msg) {
|
||||
void APIConnection::datetime_command(const DateTimeCommandRequest &msg) {
|
||||
ENTITY_COMMAND_MAKE_CALL(datetime::DateTimeEntity, datetime, datetime)
|
||||
call.set_datetime(msg.epoch_seconds);
|
||||
call.perform();
|
||||
@@ -848,7 +848,7 @@ uint16_t APIConnection::try_send_text_info(EntityBase *entity, APIConnection *co
|
||||
msg.pattern = text->traits.get_pattern_ref();
|
||||
return fill_and_encode_entity_info(text, msg, ListEntitiesTextResponse::MESSAGE_TYPE, conn, remaining_size);
|
||||
}
|
||||
void APIConnection::on_text_command_request(const TextCommandRequest &msg) {
|
||||
void APIConnection::text_command(const TextCommandRequest &msg) {
|
||||
ENTITY_COMMAND_MAKE_CALL(text::Text, text, text)
|
||||
call.set_value(msg.state);
|
||||
call.perform();
|
||||
@@ -874,7 +874,7 @@ uint16_t APIConnection::try_send_select_info(EntityBase *entity, APIConnection *
|
||||
msg.options = &select->traits.get_options();
|
||||
return fill_and_encode_entity_info(select, msg, ListEntitiesSelectResponse::MESSAGE_TYPE, conn, remaining_size);
|
||||
}
|
||||
void APIConnection::on_select_command_request(const SelectCommandRequest &msg) {
|
||||
void APIConnection::select_command(const SelectCommandRequest &msg) {
|
||||
ENTITY_COMMAND_MAKE_CALL(select::Select, select, select)
|
||||
call.set_option(msg.state.c_str(), msg.state.size());
|
||||
call.perform();
|
||||
@@ -888,7 +888,7 @@ uint16_t APIConnection::try_send_button_info(EntityBase *entity, APIConnection *
|
||||
msg.device_class = button->get_device_class_ref();
|
||||
return fill_and_encode_entity_info(button, msg, ListEntitiesButtonResponse::MESSAGE_TYPE, conn, remaining_size);
|
||||
}
|
||||
void esphome::api::APIConnection::on_button_command_request(const ButtonCommandRequest &msg) {
|
||||
void esphome::api::APIConnection::button_command(const ButtonCommandRequest &msg) {
|
||||
ENTITY_COMMAND_GET(button::Button, button, button)
|
||||
button->press();
|
||||
}
|
||||
@@ -914,7 +914,7 @@ uint16_t APIConnection::try_send_lock_info(EntityBase *entity, APIConnection *co
|
||||
msg.requires_code = a_lock->traits.get_requires_code();
|
||||
return fill_and_encode_entity_info(a_lock, msg, ListEntitiesLockResponse::MESSAGE_TYPE, conn, remaining_size);
|
||||
}
|
||||
void APIConnection::on_lock_command_request(const LockCommandRequest &msg) {
|
||||
void APIConnection::lock_command(const LockCommandRequest &msg) {
|
||||
ENTITY_COMMAND_GET(lock::Lock, a_lock, lock)
|
||||
|
||||
switch (msg.command) {
|
||||
@@ -952,7 +952,7 @@ uint16_t APIConnection::try_send_valve_info(EntityBase *entity, APIConnection *c
|
||||
msg.supports_stop = traits.get_supports_stop();
|
||||
return fill_and_encode_entity_info(valve, msg, ListEntitiesValveResponse::MESSAGE_TYPE, conn, remaining_size);
|
||||
}
|
||||
void APIConnection::on_valve_command_request(const ValveCommandRequest &msg) {
|
||||
void APIConnection::valve_command(const ValveCommandRequest &msg) {
|
||||
ENTITY_COMMAND_MAKE_CALL(valve::Valve, valve, valve)
|
||||
if (msg.has_position)
|
||||
call.set_position(msg.position);
|
||||
@@ -996,7 +996,7 @@ uint16_t APIConnection::try_send_media_player_info(EntityBase *entity, APIConnec
|
||||
return fill_and_encode_entity_info(media_player, msg, ListEntitiesMediaPlayerResponse::MESSAGE_TYPE, conn,
|
||||
remaining_size);
|
||||
}
|
||||
void APIConnection::on_media_player_command_request(const MediaPlayerCommandRequest &msg) {
|
||||
void APIConnection::media_player_command(const MediaPlayerCommandRequest &msg) {
|
||||
ENTITY_COMMAND_MAKE_CALL(media_player::MediaPlayer, media_player, media_player)
|
||||
if (msg.has_command) {
|
||||
call.set_command(static_cast<media_player::MediaPlayerCommand>(msg.command));
|
||||
@@ -1063,7 +1063,7 @@ uint16_t APIConnection::try_send_camera_info(EntityBase *entity, APIConnection *
|
||||
ListEntitiesCameraResponse msg;
|
||||
return fill_and_encode_entity_info(camera, msg, ListEntitiesCameraResponse::MESSAGE_TYPE, conn, remaining_size);
|
||||
}
|
||||
void APIConnection::on_camera_image_request(const CameraImageRequest &msg) {
|
||||
void APIConnection::camera_image(const CameraImageRequest &msg) {
|
||||
if (camera::Camera::instance() == nullptr)
|
||||
return;
|
||||
|
||||
@@ -1092,47 +1092,41 @@ void APIConnection::on_get_time_response(const GetTimeResponse &value) {
|
||||
#endif
|
||||
|
||||
#ifdef USE_BLUETOOTH_PROXY
|
||||
void APIConnection::on_subscribe_bluetooth_le_advertisements_request(
|
||||
const SubscribeBluetoothLEAdvertisementsRequest &msg) {
|
||||
void APIConnection::subscribe_bluetooth_le_advertisements(const SubscribeBluetoothLEAdvertisementsRequest &msg) {
|
||||
bluetooth_proxy::global_bluetooth_proxy->subscribe_api_connection(this, msg.flags);
|
||||
}
|
||||
void APIConnection::on_unsubscribe_bluetooth_le_advertisements_request() {
|
||||
void APIConnection::unsubscribe_bluetooth_le_advertisements() {
|
||||
bluetooth_proxy::global_bluetooth_proxy->unsubscribe_api_connection(this);
|
||||
}
|
||||
void APIConnection::on_bluetooth_device_request(const BluetoothDeviceRequest &msg) {
|
||||
void APIConnection::bluetooth_device_request(const BluetoothDeviceRequest &msg) {
|
||||
bluetooth_proxy::global_bluetooth_proxy->bluetooth_device_request(msg);
|
||||
}
|
||||
void APIConnection::on_bluetooth_gatt_read_request(const BluetoothGATTReadRequest &msg) {
|
||||
void APIConnection::bluetooth_gatt_read(const BluetoothGATTReadRequest &msg) {
|
||||
bluetooth_proxy::global_bluetooth_proxy->bluetooth_gatt_read(msg);
|
||||
}
|
||||
void APIConnection::on_bluetooth_gatt_write_request(const BluetoothGATTWriteRequest &msg) {
|
||||
void APIConnection::bluetooth_gatt_write(const BluetoothGATTWriteRequest &msg) {
|
||||
bluetooth_proxy::global_bluetooth_proxy->bluetooth_gatt_write(msg);
|
||||
}
|
||||
void APIConnection::on_bluetooth_gatt_read_descriptor_request(const BluetoothGATTReadDescriptorRequest &msg) {
|
||||
void APIConnection::bluetooth_gatt_read_descriptor(const BluetoothGATTReadDescriptorRequest &msg) {
|
||||
bluetooth_proxy::global_bluetooth_proxy->bluetooth_gatt_read_descriptor(msg);
|
||||
}
|
||||
void APIConnection::on_bluetooth_gatt_write_descriptor_request(const BluetoothGATTWriteDescriptorRequest &msg) {
|
||||
void APIConnection::bluetooth_gatt_write_descriptor(const BluetoothGATTWriteDescriptorRequest &msg) {
|
||||
bluetooth_proxy::global_bluetooth_proxy->bluetooth_gatt_write_descriptor(msg);
|
||||
}
|
||||
void APIConnection::on_bluetooth_gatt_get_services_request(const BluetoothGATTGetServicesRequest &msg) {
|
||||
void APIConnection::bluetooth_gatt_get_services(const BluetoothGATTGetServicesRequest &msg) {
|
||||
bluetooth_proxy::global_bluetooth_proxy->bluetooth_gatt_send_services(msg);
|
||||
}
|
||||
|
||||
void APIConnection::on_bluetooth_gatt_notify_request(const BluetoothGATTNotifyRequest &msg) {
|
||||
void APIConnection::bluetooth_gatt_notify(const BluetoothGATTNotifyRequest &msg) {
|
||||
bluetooth_proxy::global_bluetooth_proxy->bluetooth_gatt_notify(msg);
|
||||
}
|
||||
|
||||
bool APIConnection::send_subscribe_bluetooth_connections_free_response_() {
|
||||
bool APIConnection::send_subscribe_bluetooth_connections_free_response() {
|
||||
bluetooth_proxy::global_bluetooth_proxy->send_connections_free(this);
|
||||
return true;
|
||||
}
|
||||
void APIConnection::on_subscribe_bluetooth_connections_free_request() {
|
||||
if (!this->send_subscribe_bluetooth_connections_free_response_()) {
|
||||
this->on_fatal_error();
|
||||
}
|
||||
}
|
||||
|
||||
void APIConnection::on_bluetooth_scanner_set_mode_request(const BluetoothScannerSetModeRequest &msg) {
|
||||
void APIConnection::bluetooth_scanner_set_mode(const BluetoothScannerSetModeRequest &msg) {
|
||||
bluetooth_proxy::global_bluetooth_proxy->bluetooth_scanner_set_mode(
|
||||
msg.mode == enums::BluetoothScannerMode::BLUETOOTH_SCANNER_MODE_ACTIVE);
|
||||
}
|
||||
@@ -1144,7 +1138,7 @@ bool APIConnection::check_voice_assistant_api_connection_() const {
|
||||
voice_assistant::global_voice_assistant->get_api_connection() == this;
|
||||
}
|
||||
|
||||
void APIConnection::on_subscribe_voice_assistant_request(const SubscribeVoiceAssistantRequest &msg) {
|
||||
void APIConnection::subscribe_voice_assistant(const SubscribeVoiceAssistantRequest &msg) {
|
||||
if (voice_assistant::global_voice_assistant != nullptr) {
|
||||
voice_assistant::global_voice_assistant->client_subscription(this, msg.subscribe);
|
||||
}
|
||||
@@ -1190,7 +1184,7 @@ void APIConnection::on_voice_assistant_announce_request(const VoiceAssistantAnno
|
||||
}
|
||||
}
|
||||
|
||||
bool APIConnection::send_voice_assistant_get_configuration_response_(const VoiceAssistantConfigurationRequest &msg) {
|
||||
bool APIConnection::send_voice_assistant_get_configuration_response(const VoiceAssistantConfigurationRequest &msg) {
|
||||
VoiceAssistantConfigurationResponse resp;
|
||||
if (!this->check_voice_assistant_api_connection_()) {
|
||||
return this->send_message(resp, VoiceAssistantConfigurationResponse::MESSAGE_TYPE);
|
||||
@@ -1227,13 +1221,8 @@ bool APIConnection::send_voice_assistant_get_configuration_response_(const Voice
|
||||
resp.max_active_wake_words = config.max_active_wake_words;
|
||||
return this->send_message(resp, VoiceAssistantConfigurationResponse::MESSAGE_TYPE);
|
||||
}
|
||||
void APIConnection::on_voice_assistant_configuration_request(const VoiceAssistantConfigurationRequest &msg) {
|
||||
if (!this->send_voice_assistant_get_configuration_response_(msg)) {
|
||||
this->on_fatal_error();
|
||||
}
|
||||
}
|
||||
|
||||
void APIConnection::on_voice_assistant_set_configuration(const VoiceAssistantSetConfiguration &msg) {
|
||||
void APIConnection::voice_assistant_set_configuration(const VoiceAssistantSetConfiguration &msg) {
|
||||
if (this->check_voice_assistant_api_connection_()) {
|
||||
voice_assistant::global_voice_assistant->on_set_configuration(msg.active_wake_words);
|
||||
}
|
||||
@@ -1241,11 +1230,11 @@ void APIConnection::on_voice_assistant_set_configuration(const VoiceAssistantSet
|
||||
#endif
|
||||
|
||||
#ifdef USE_ZWAVE_PROXY
|
||||
void APIConnection::on_z_wave_proxy_frame(const ZWaveProxyFrame &msg) {
|
||||
void APIConnection::zwave_proxy_frame(const ZWaveProxyFrame &msg) {
|
||||
zwave_proxy::global_zwave_proxy->send_frame(msg.data, msg.data_len);
|
||||
}
|
||||
|
||||
void APIConnection::on_z_wave_proxy_request(const ZWaveProxyRequest &msg) {
|
||||
void APIConnection::zwave_proxy_request(const ZWaveProxyRequest &msg) {
|
||||
zwave_proxy::global_zwave_proxy->zwave_proxy_request(this, msg.type);
|
||||
}
|
||||
#endif
|
||||
@@ -1273,7 +1262,7 @@ uint16_t APIConnection::try_send_alarm_control_panel_info(EntityBase *entity, AP
|
||||
return fill_and_encode_entity_info(a_alarm_control_panel, msg, ListEntitiesAlarmControlPanelResponse::MESSAGE_TYPE,
|
||||
conn, remaining_size);
|
||||
}
|
||||
void APIConnection::on_alarm_control_panel_command_request(const AlarmControlPanelCommandRequest &msg) {
|
||||
void APIConnection::alarm_control_panel_command(const AlarmControlPanelCommandRequest &msg) {
|
||||
ENTITY_COMMAND_MAKE_CALL(alarm_control_panel::AlarmControlPanel, a_alarm_control_panel, alarm_control_panel)
|
||||
switch (msg.command) {
|
||||
case enums::ALARM_CONTROL_PANEL_DISARM:
|
||||
@@ -1333,7 +1322,7 @@ uint16_t APIConnection::try_send_water_heater_info(EntityBase *entity, APIConnec
|
||||
return fill_and_encode_entity_info(wh, msg, ListEntitiesWaterHeaterResponse::MESSAGE_TYPE, conn, remaining_size);
|
||||
}
|
||||
|
||||
void APIConnection::on_water_heater_command_request(const WaterHeaterCommandRequest &msg) {
|
||||
void APIConnection::water_heater_command(const WaterHeaterCommandRequest &msg) {
|
||||
ENTITY_COMMAND_MAKE_CALL(water_heater::WaterHeater, water_heater, water_heater)
|
||||
if (msg.has_fields & enums::WATER_HEATER_COMMAND_HAS_MODE)
|
||||
call.set_mode(static_cast<water_heater::WaterHeaterMode>(msg.mode));
|
||||
@@ -1375,7 +1364,7 @@ uint16_t APIConnection::try_send_event_info(EntityBase *entity, APIConnection *c
|
||||
#endif
|
||||
|
||||
#ifdef USE_IR_RF
|
||||
void APIConnection::on_infrared_rf_transmit_raw_timings_request(const InfraredRFTransmitRawTimingsRequest &msg) {
|
||||
void APIConnection::infrared_rf_transmit_raw_timings(const InfraredRFTransmitRawTimingsRequest &msg) {
|
||||
// TODO: When RF is implemented, add a field to the message to distinguish IR vs RF
|
||||
// and dispatch to the appropriate entity type based on that field.
|
||||
#ifdef USE_INFRARED
|
||||
@@ -1429,7 +1418,7 @@ uint16_t APIConnection::try_send_update_info(EntityBase *entity, APIConnection *
|
||||
msg.device_class = update->get_device_class_ref();
|
||||
return fill_and_encode_entity_info(update, msg, ListEntitiesUpdateResponse::MESSAGE_TYPE, conn, remaining_size);
|
||||
}
|
||||
void APIConnection::on_update_command_request(const UpdateCommandRequest &msg) {
|
||||
void APIConnection::update_command(const UpdateCommandRequest &msg) {
|
||||
ENTITY_COMMAND_GET(update::UpdateEntity, update, update)
|
||||
|
||||
switch (msg.command) {
|
||||
@@ -1480,7 +1469,7 @@ void APIConnection::complete_authentication_() {
|
||||
#endif
|
||||
}
|
||||
|
||||
bool APIConnection::send_hello_response_(const HelloRequest &msg) {
|
||||
bool APIConnection::send_hello_response(const HelloRequest &msg) {
|
||||
// Copy client name with truncation if needed (set_client_name handles truncation)
|
||||
this->helper_->set_client_name(msg.client_info.c_str(), msg.client_info.size());
|
||||
this->client_api_version_major_ = msg.api_version_major;
|
||||
@@ -1501,12 +1490,12 @@ bool APIConnection::send_hello_response_(const HelloRequest &msg) {
|
||||
return this->send_message(resp, HelloResponse::MESSAGE_TYPE);
|
||||
}
|
||||
|
||||
bool APIConnection::send_ping_response_() {
|
||||
bool APIConnection::send_ping_response() {
|
||||
PingResponse resp;
|
||||
return this->send_message(resp, PingResponse::MESSAGE_TYPE);
|
||||
}
|
||||
|
||||
bool APIConnection::send_device_info_response_() {
|
||||
bool APIConnection::send_device_info_response() {
|
||||
DeviceInfoResponse resp{};
|
||||
resp.name = StringRef(App.get_name());
|
||||
resp.friendly_name = StringRef(App.get_friendly_name());
|
||||
@@ -1629,26 +1618,6 @@ bool APIConnection::send_device_info_response_() {
|
||||
|
||||
return this->send_message(resp, DeviceInfoResponse::MESSAGE_TYPE);
|
||||
}
|
||||
void APIConnection::on_hello_request(const HelloRequest &msg) {
|
||||
if (!this->send_hello_response_(msg)) {
|
||||
this->on_fatal_error();
|
||||
}
|
||||
}
|
||||
void APIConnection::on_disconnect_request() {
|
||||
if (!this->send_disconnect_response_()) {
|
||||
this->on_fatal_error();
|
||||
}
|
||||
}
|
||||
void APIConnection::on_ping_request() {
|
||||
if (!this->send_ping_response_()) {
|
||||
this->on_fatal_error();
|
||||
}
|
||||
}
|
||||
void APIConnection::on_device_info_request() {
|
||||
if (!this->send_device_info_response_()) {
|
||||
this->on_fatal_error();
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef USE_API_HOMEASSISTANT_STATES
|
||||
void APIConnection::on_home_assistant_state_response(const HomeAssistantStateResponse &msg) {
|
||||
@@ -1687,7 +1656,7 @@ void APIConnection::on_home_assistant_state_response(const HomeAssistantStateRes
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_API_USER_DEFINED_ACTIONS
|
||||
void APIConnection::on_execute_service_request(const ExecuteServiceRequest &msg) {
|
||||
void APIConnection::execute_service(const ExecuteServiceRequest &msg) {
|
||||
bool found = false;
|
||||
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES
|
||||
// Register the call and get a unique server-generated action_call_id
|
||||
@@ -1753,7 +1722,7 @@ void APIConnection::on_homeassistant_action_response(const HomeassistantActionRe
|
||||
};
|
||||
#endif
|
||||
#ifdef USE_API_NOISE
|
||||
bool APIConnection::send_noise_encryption_set_key_response_(const NoiseEncryptionSetKeyRequest &msg) {
|
||||
bool APIConnection::send_noise_encryption_set_key_response(const NoiseEncryptionSetKeyRequest &msg) {
|
||||
NoiseEncryptionSetKeyResponse resp;
|
||||
resp.success = false;
|
||||
|
||||
@@ -1774,14 +1743,9 @@ bool APIConnection::send_noise_encryption_set_key_response_(const NoiseEncryptio
|
||||
|
||||
return this->send_message(resp, NoiseEncryptionSetKeyResponse::MESSAGE_TYPE);
|
||||
}
|
||||
void APIConnection::on_noise_encryption_set_key_request(const NoiseEncryptionSetKeyRequest &msg) {
|
||||
if (!this->send_noise_encryption_set_key_response_(msg)) {
|
||||
this->on_fatal_error();
|
||||
}
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_API_HOMEASSISTANT_STATES
|
||||
void APIConnection::on_subscribe_home_assistant_states_request() { state_subs_at_ = 0; }
|
||||
void APIConnection::subscribe_home_assistant_states() { state_subs_at_ = 0; }
|
||||
#endif
|
||||
bool APIConnection::try_to_clear_buffer(bool log_out_of_space) {
|
||||
if (this->flags_.remove)
|
||||
|
||||
@@ -47,72 +47,72 @@ class APIConnection final : public APIServerConnection {
|
||||
#endif
|
||||
#ifdef USE_COVER
|
||||
bool send_cover_state(cover::Cover *cover);
|
||||
void on_cover_command_request(const CoverCommandRequest &msg) override;
|
||||
void cover_command(const CoverCommandRequest &msg) override;
|
||||
#endif
|
||||
#ifdef USE_FAN
|
||||
bool send_fan_state(fan::Fan *fan);
|
||||
void on_fan_command_request(const FanCommandRequest &msg) override;
|
||||
void fan_command(const FanCommandRequest &msg) override;
|
||||
#endif
|
||||
#ifdef USE_LIGHT
|
||||
bool send_light_state(light::LightState *light);
|
||||
void on_light_command_request(const LightCommandRequest &msg) override;
|
||||
void light_command(const LightCommandRequest &msg) override;
|
||||
#endif
|
||||
#ifdef USE_SENSOR
|
||||
bool send_sensor_state(sensor::Sensor *sensor);
|
||||
#endif
|
||||
#ifdef USE_SWITCH
|
||||
bool send_switch_state(switch_::Switch *a_switch);
|
||||
void on_switch_command_request(const SwitchCommandRequest &msg) override;
|
||||
void switch_command(const SwitchCommandRequest &msg) override;
|
||||
#endif
|
||||
#ifdef USE_TEXT_SENSOR
|
||||
bool send_text_sensor_state(text_sensor::TextSensor *text_sensor);
|
||||
#endif
|
||||
#ifdef USE_CAMERA
|
||||
void set_camera_state(std::shared_ptr<camera::CameraImage> image);
|
||||
void on_camera_image_request(const CameraImageRequest &msg) override;
|
||||
void camera_image(const CameraImageRequest &msg) override;
|
||||
#endif
|
||||
#ifdef USE_CLIMATE
|
||||
bool send_climate_state(climate::Climate *climate);
|
||||
void on_climate_command_request(const ClimateCommandRequest &msg) override;
|
||||
void climate_command(const ClimateCommandRequest &msg) override;
|
||||
#endif
|
||||
#ifdef USE_NUMBER
|
||||
bool send_number_state(number::Number *number);
|
||||
void on_number_command_request(const NumberCommandRequest &msg) override;
|
||||
void number_command(const NumberCommandRequest &msg) override;
|
||||
#endif
|
||||
#ifdef USE_DATETIME_DATE
|
||||
bool send_date_state(datetime::DateEntity *date);
|
||||
void on_date_command_request(const DateCommandRequest &msg) override;
|
||||
void date_command(const DateCommandRequest &msg) override;
|
||||
#endif
|
||||
#ifdef USE_DATETIME_TIME
|
||||
bool send_time_state(datetime::TimeEntity *time);
|
||||
void on_time_command_request(const TimeCommandRequest &msg) override;
|
||||
void time_command(const TimeCommandRequest &msg) override;
|
||||
#endif
|
||||
#ifdef USE_DATETIME_DATETIME
|
||||
bool send_datetime_state(datetime::DateTimeEntity *datetime);
|
||||
void on_date_time_command_request(const DateTimeCommandRequest &msg) override;
|
||||
void datetime_command(const DateTimeCommandRequest &msg) override;
|
||||
#endif
|
||||
#ifdef USE_TEXT
|
||||
bool send_text_state(text::Text *text);
|
||||
void on_text_command_request(const TextCommandRequest &msg) override;
|
||||
void text_command(const TextCommandRequest &msg) override;
|
||||
#endif
|
||||
#ifdef USE_SELECT
|
||||
bool send_select_state(select::Select *select);
|
||||
void on_select_command_request(const SelectCommandRequest &msg) override;
|
||||
void select_command(const SelectCommandRequest &msg) override;
|
||||
#endif
|
||||
#ifdef USE_BUTTON
|
||||
void on_button_command_request(const ButtonCommandRequest &msg) override;
|
||||
void button_command(const ButtonCommandRequest &msg) override;
|
||||
#endif
|
||||
#ifdef USE_LOCK
|
||||
bool send_lock_state(lock::Lock *a_lock);
|
||||
void on_lock_command_request(const LockCommandRequest &msg) override;
|
||||
void lock_command(const LockCommandRequest &msg) override;
|
||||
#endif
|
||||
#ifdef USE_VALVE
|
||||
bool send_valve_state(valve::Valve *valve);
|
||||
void on_valve_command_request(const ValveCommandRequest &msg) override;
|
||||
void valve_command(const ValveCommandRequest &msg) override;
|
||||
#endif
|
||||
#ifdef USE_MEDIA_PLAYER
|
||||
bool send_media_player_state(media_player::MediaPlayer *media_player);
|
||||
void on_media_player_command_request(const MediaPlayerCommandRequest &msg) override;
|
||||
void media_player_command(const MediaPlayerCommandRequest &msg) override;
|
||||
#endif
|
||||
bool try_send_log_message(int level, const char *tag, const char *line, size_t message_len);
|
||||
#ifdef USE_API_HOMEASSISTANT_SERVICES
|
||||
@@ -126,18 +126,18 @@ class APIConnection final : public APIServerConnection {
|
||||
#endif // USE_API_HOMEASSISTANT_ACTION_RESPONSES
|
||||
#endif // USE_API_HOMEASSISTANT_SERVICES
|
||||
#ifdef USE_BLUETOOTH_PROXY
|
||||
void on_subscribe_bluetooth_le_advertisements_request(const SubscribeBluetoothLEAdvertisementsRequest &msg) override;
|
||||
void on_unsubscribe_bluetooth_le_advertisements_request() override;
|
||||
void subscribe_bluetooth_le_advertisements(const SubscribeBluetoothLEAdvertisementsRequest &msg) override;
|
||||
void unsubscribe_bluetooth_le_advertisements() override;
|
||||
|
||||
void on_bluetooth_device_request(const BluetoothDeviceRequest &msg) override;
|
||||
void on_bluetooth_gatt_read_request(const BluetoothGATTReadRequest &msg) override;
|
||||
void on_bluetooth_gatt_write_request(const BluetoothGATTWriteRequest &msg) override;
|
||||
void on_bluetooth_gatt_read_descriptor_request(const BluetoothGATTReadDescriptorRequest &msg) override;
|
||||
void on_bluetooth_gatt_write_descriptor_request(const BluetoothGATTWriteDescriptorRequest &msg) override;
|
||||
void on_bluetooth_gatt_get_services_request(const BluetoothGATTGetServicesRequest &msg) override;
|
||||
void on_bluetooth_gatt_notify_request(const BluetoothGATTNotifyRequest &msg) override;
|
||||
void on_subscribe_bluetooth_connections_free_request() override;
|
||||
void on_bluetooth_scanner_set_mode_request(const BluetoothScannerSetModeRequest &msg) override;
|
||||
void bluetooth_device_request(const BluetoothDeviceRequest &msg) override;
|
||||
void bluetooth_gatt_read(const BluetoothGATTReadRequest &msg) override;
|
||||
void bluetooth_gatt_write(const BluetoothGATTWriteRequest &msg) override;
|
||||
void bluetooth_gatt_read_descriptor(const BluetoothGATTReadDescriptorRequest &msg) override;
|
||||
void bluetooth_gatt_write_descriptor(const BluetoothGATTWriteDescriptorRequest &msg) override;
|
||||
void bluetooth_gatt_get_services(const BluetoothGATTGetServicesRequest &msg) override;
|
||||
void bluetooth_gatt_notify(const BluetoothGATTNotifyRequest &msg) override;
|
||||
bool send_subscribe_bluetooth_connections_free_response() override;
|
||||
void bluetooth_scanner_set_mode(const BluetoothScannerSetModeRequest &msg) override;
|
||||
|
||||
#endif
|
||||
#ifdef USE_HOMEASSISTANT_TIME
|
||||
@@ -148,33 +148,33 @@ class APIConnection final : public APIServerConnection {
|
||||
#endif
|
||||
|
||||
#ifdef USE_VOICE_ASSISTANT
|
||||
void on_subscribe_voice_assistant_request(const SubscribeVoiceAssistantRequest &msg) override;
|
||||
void subscribe_voice_assistant(const SubscribeVoiceAssistantRequest &msg) override;
|
||||
void on_voice_assistant_response(const VoiceAssistantResponse &msg) override;
|
||||
void on_voice_assistant_event_response(const VoiceAssistantEventResponse &msg) override;
|
||||
void on_voice_assistant_audio(const VoiceAssistantAudio &msg) override;
|
||||
void on_voice_assistant_timer_event_response(const VoiceAssistantTimerEventResponse &msg) override;
|
||||
void on_voice_assistant_announce_request(const VoiceAssistantAnnounceRequest &msg) override;
|
||||
void on_voice_assistant_configuration_request(const VoiceAssistantConfigurationRequest &msg) override;
|
||||
void on_voice_assistant_set_configuration(const VoiceAssistantSetConfiguration &msg) override;
|
||||
bool send_voice_assistant_get_configuration_response(const VoiceAssistantConfigurationRequest &msg) override;
|
||||
void voice_assistant_set_configuration(const VoiceAssistantSetConfiguration &msg) override;
|
||||
#endif
|
||||
|
||||
#ifdef USE_ZWAVE_PROXY
|
||||
void on_z_wave_proxy_frame(const ZWaveProxyFrame &msg) override;
|
||||
void on_z_wave_proxy_request(const ZWaveProxyRequest &msg) override;
|
||||
void zwave_proxy_frame(const ZWaveProxyFrame &msg) override;
|
||||
void zwave_proxy_request(const ZWaveProxyRequest &msg) override;
|
||||
#endif
|
||||
|
||||
#ifdef USE_ALARM_CONTROL_PANEL
|
||||
bool send_alarm_control_panel_state(alarm_control_panel::AlarmControlPanel *a_alarm_control_panel);
|
||||
void on_alarm_control_panel_command_request(const AlarmControlPanelCommandRequest &msg) override;
|
||||
void alarm_control_panel_command(const AlarmControlPanelCommandRequest &msg) override;
|
||||
#endif
|
||||
|
||||
#ifdef USE_WATER_HEATER
|
||||
bool send_water_heater_state(water_heater::WaterHeater *water_heater);
|
||||
void on_water_heater_command_request(const WaterHeaterCommandRequest &msg) override;
|
||||
void water_heater_command(const WaterHeaterCommandRequest &msg) override;
|
||||
#endif
|
||||
|
||||
#ifdef USE_IR_RF
|
||||
void on_infrared_rf_transmit_raw_timings_request(const InfraredRFTransmitRawTimingsRequest &msg) override;
|
||||
void infrared_rf_transmit_raw_timings(const InfraredRFTransmitRawTimingsRequest &msg) override;
|
||||
void send_infrared_rf_receive_event(const InfraredRFReceiveEvent &msg);
|
||||
#endif
|
||||
|
||||
@@ -184,7 +184,7 @@ class APIConnection final : public APIServerConnection {
|
||||
|
||||
#ifdef USE_UPDATE
|
||||
bool send_update_state(update::UpdateEntity *update);
|
||||
void on_update_command_request(const UpdateCommandRequest &msg) override;
|
||||
void update_command(const UpdateCommandRequest &msg) override;
|
||||
#endif
|
||||
|
||||
void on_disconnect_response() override;
|
||||
@@ -198,12 +198,12 @@ class APIConnection final : public APIServerConnection {
|
||||
#ifdef USE_HOMEASSISTANT_TIME
|
||||
void on_get_time_response(const GetTimeResponse &value) override;
|
||||
#endif
|
||||
void on_hello_request(const HelloRequest &msg) override;
|
||||
void on_disconnect_request() override;
|
||||
void on_ping_request() override;
|
||||
void on_device_info_request() override;
|
||||
void on_list_entities_request() override { this->begin_iterator_(ActiveIterator::LIST_ENTITIES); }
|
||||
void on_subscribe_states_request() override {
|
||||
bool send_hello_response(const HelloRequest &msg) override;
|
||||
bool send_disconnect_response() override;
|
||||
bool send_ping_response() override;
|
||||
bool send_device_info_response() override;
|
||||
void list_entities() override { this->begin_iterator_(ActiveIterator::LIST_ENTITIES); }
|
||||
void subscribe_states() override {
|
||||
this->flags_.state_subscription = true;
|
||||
// Start initial state iterator only if no iterator is active
|
||||
// If list_entities is running, we'll start initial_state when it completes
|
||||
@@ -211,19 +211,19 @@ class APIConnection final : public APIServerConnection {
|
||||
this->begin_iterator_(ActiveIterator::INITIAL_STATE);
|
||||
}
|
||||
}
|
||||
void on_subscribe_logs_request(const SubscribeLogsRequest &msg) override {
|
||||
void subscribe_logs(const SubscribeLogsRequest &msg) override {
|
||||
this->flags_.log_subscription = msg.level;
|
||||
if (msg.dump_config)
|
||||
App.schedule_dump_config();
|
||||
}
|
||||
#ifdef USE_API_HOMEASSISTANT_SERVICES
|
||||
void on_subscribe_homeassistant_services_request() override { this->flags_.service_call_subscription = true; }
|
||||
void subscribe_homeassistant_services() override { this->flags_.service_call_subscription = true; }
|
||||
#endif
|
||||
#ifdef USE_API_HOMEASSISTANT_STATES
|
||||
void on_subscribe_home_assistant_states_request() override;
|
||||
void subscribe_home_assistant_states() override;
|
||||
#endif
|
||||
#ifdef USE_API_USER_DEFINED_ACTIONS
|
||||
void on_execute_service_request(const ExecuteServiceRequest &msg) override;
|
||||
void execute_service(const ExecuteServiceRequest &msg) override;
|
||||
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES
|
||||
void send_execute_service_response(uint32_t call_id, bool success, StringRef error_message);
|
||||
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES_JSON
|
||||
@@ -233,7 +233,7 @@ class APIConnection final : public APIServerConnection {
|
||||
#endif // USE_API_USER_DEFINED_ACTION_RESPONSES
|
||||
#endif
|
||||
#ifdef USE_API_NOISE
|
||||
void on_noise_encryption_set_key_request(const NoiseEncryptionSetKeyRequest &msg) override;
|
||||
bool send_noise_encryption_set_key_response(const NoiseEncryptionSetKeyRequest &msg) override;
|
||||
#endif
|
||||
|
||||
bool is_authenticated() override {
|
||||
@@ -283,21 +283,6 @@ class APIConnection final : public APIServerConnection {
|
||||
// Helper function to handle authentication completion
|
||||
void complete_authentication_();
|
||||
|
||||
// Pattern B helpers: send response and return success/failure
|
||||
bool send_hello_response_(const HelloRequest &msg);
|
||||
bool send_disconnect_response_();
|
||||
bool send_ping_response_();
|
||||
bool send_device_info_response_();
|
||||
#ifdef USE_API_NOISE
|
||||
bool send_noise_encryption_set_key_response_(const NoiseEncryptionSetKeyRequest &msg);
|
||||
#endif
|
||||
#ifdef USE_BLUETOOTH_PROXY
|
||||
bool send_subscribe_bluetooth_connections_free_response_();
|
||||
#endif
|
||||
#ifdef USE_VOICE_ASSISTANT
|
||||
bool send_voice_assistant_get_configuration_response_(const VoiceAssistantConfigurationRequest &msg);
|
||||
#endif
|
||||
|
||||
#ifdef USE_CAMERA
|
||||
void try_send_camera_image_();
|
||||
#endif
|
||||
|
||||
@@ -623,6 +623,200 @@ 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() {
|
||||
if (!this->send_disconnect_response()) {
|
||||
this->on_fatal_error();
|
||||
}
|
||||
}
|
||||
void APIServerConnection::on_ping_request() {
|
||||
if (!this->send_ping_response()) {
|
||||
this->on_fatal_error();
|
||||
}
|
||||
}
|
||||
void APIServerConnection::on_device_info_request() {
|
||||
if (!this->send_device_info_response()) {
|
||||
this->on_fatal_error();
|
||||
}
|
||||
}
|
||||
void APIServerConnection::on_list_entities_request() { this->list_entities(); }
|
||||
void APIServerConnection::on_subscribe_states_request() { this->subscribe_states(); }
|
||||
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() { this->subscribe_homeassistant_services(); }
|
||||
#endif
|
||||
#ifdef USE_API_HOMEASSISTANT_STATES
|
||||
void APIServerConnection::on_subscribe_home_assistant_states_request() { this->subscribe_home_assistant_states(); }
|
||||
#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() {
|
||||
if (!this->send_subscribe_bluetooth_connections_free_response()) {
|
||||
this->on_fatal_error();
|
||||
}
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_BLUETOOTH_PROXY
|
||||
void APIServerConnection::on_unsubscribe_bluetooth_le_advertisements_request() {
|
||||
this->unsubscribe_bluetooth_le_advertisements();
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_BLUETOOTH_PROXY
|
||||
void APIServerConnection::on_bluetooth_scanner_set_mode_request(const BluetoothScannerSetModeRequest &msg) {
|
||||
this->bluetooth_scanner_set_mode(msg);
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_VOICE_ASSISTANT
|
||||
void APIServerConnection::on_subscribe_voice_assistant_request(const SubscribeVoiceAssistantRequest &msg) {
|
||||
this->subscribe_voice_assistant(msg);
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_VOICE_ASSISTANT
|
||||
void APIServerConnection::on_voice_assistant_configuration_request(const VoiceAssistantConfigurationRequest &msg) {
|
||||
if (!this->send_voice_assistant_get_configuration_response(msg)) {
|
||||
this->on_fatal_error();
|
||||
}
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_VOICE_ASSISTANT
|
||||
void APIServerConnection::on_voice_assistant_set_configuration(const VoiceAssistantSetConfiguration &msg) {
|
||||
this->voice_assistant_set_configuration(msg);
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_ALARM_CONTROL_PANEL
|
||||
void APIServerConnection::on_alarm_control_panel_command_request(const AlarmControlPanelCommandRequest &msg) {
|
||||
this->alarm_control_panel_command(msg);
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_ZWAVE_PROXY
|
||||
void APIServerConnection::on_z_wave_proxy_frame(const ZWaveProxyFrame &msg) { this->zwave_proxy_frame(msg); }
|
||||
#endif
|
||||
#ifdef USE_ZWAVE_PROXY
|
||||
void APIServerConnection::on_z_wave_proxy_request(const ZWaveProxyRequest &msg) { this->zwave_proxy_request(msg); }
|
||||
#endif
|
||||
#ifdef USE_IR_RF
|
||||
void APIServerConnection::on_infrared_rf_transmit_raw_timings_request(const InfraredRFTransmitRawTimingsRequest &msg) {
|
||||
this->infrared_rf_transmit_raw_timings(msg);
|
||||
}
|
||||
#endif
|
||||
|
||||
void APIServerConnection::read_message(uint32_t msg_size, uint32_t msg_type, const uint8_t *msg_data) {
|
||||
// Check authentication/connection requirements for messages
|
||||
switch (msg_type) {
|
||||
|
||||
@@ -229,7 +229,268 @@ class APIServerConnectionBase : public ProtoService {
|
||||
};
|
||||
|
||||
class APIServerConnection : public APIServerConnectionBase {
|
||||
public:
|
||||
virtual bool send_hello_response(const HelloRequest &msg) = 0;
|
||||
virtual bool send_disconnect_response() = 0;
|
||||
virtual bool send_ping_response() = 0;
|
||||
virtual bool send_device_info_response() = 0;
|
||||
virtual void list_entities() = 0;
|
||||
virtual void subscribe_states() = 0;
|
||||
virtual void subscribe_logs(const SubscribeLogsRequest &msg) = 0;
|
||||
#ifdef USE_API_HOMEASSISTANT_SERVICES
|
||||
virtual void subscribe_homeassistant_services() = 0;
|
||||
#endif
|
||||
#ifdef USE_API_HOMEASSISTANT_STATES
|
||||
virtual void subscribe_home_assistant_states() = 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() = 0;
|
||||
#endif
|
||||
#ifdef USE_BLUETOOTH_PROXY
|
||||
virtual void unsubscribe_bluetooth_le_advertisements() = 0;
|
||||
#endif
|
||||
#ifdef USE_BLUETOOTH_PROXY
|
||||
virtual void bluetooth_scanner_set_mode(const BluetoothScannerSetModeRequest &msg) = 0;
|
||||
#endif
|
||||
#ifdef USE_VOICE_ASSISTANT
|
||||
virtual void subscribe_voice_assistant(const SubscribeVoiceAssistantRequest &msg) = 0;
|
||||
#endif
|
||||
#ifdef USE_VOICE_ASSISTANT
|
||||
virtual bool send_voice_assistant_get_configuration_response(const VoiceAssistantConfigurationRequest &msg) = 0;
|
||||
#endif
|
||||
#ifdef USE_VOICE_ASSISTANT
|
||||
virtual void voice_assistant_set_configuration(const VoiceAssistantSetConfiguration &msg) = 0;
|
||||
#endif
|
||||
#ifdef USE_ALARM_CONTROL_PANEL
|
||||
virtual void alarm_control_panel_command(const AlarmControlPanelCommandRequest &msg) = 0;
|
||||
#endif
|
||||
#ifdef USE_ZWAVE_PROXY
|
||||
virtual void zwave_proxy_frame(const ZWaveProxyFrame &msg) = 0;
|
||||
#endif
|
||||
#ifdef USE_ZWAVE_PROXY
|
||||
virtual void zwave_proxy_request(const ZWaveProxyRequest &msg) = 0;
|
||||
#endif
|
||||
#ifdef USE_IR_RF
|
||||
virtual void infrared_rf_transmit_raw_timings(const InfraredRFTransmitRawTimingsRequest &msg) = 0;
|
||||
#endif
|
||||
protected:
|
||||
void on_hello_request(const HelloRequest &msg) override;
|
||||
void on_disconnect_request() override;
|
||||
void on_ping_request() override;
|
||||
void on_device_info_request() override;
|
||||
void on_list_entities_request() override;
|
||||
void on_subscribe_states_request() override;
|
||||
void on_subscribe_logs_request(const SubscribeLogsRequest &msg) override;
|
||||
#ifdef USE_API_HOMEASSISTANT_SERVICES
|
||||
void on_subscribe_homeassistant_services_request() override;
|
||||
#endif
|
||||
#ifdef USE_API_HOMEASSISTANT_STATES
|
||||
void on_subscribe_home_assistant_states_request() 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() override;
|
||||
#endif
|
||||
#ifdef USE_BLUETOOTH_PROXY
|
||||
void on_unsubscribe_bluetooth_le_advertisements_request() 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;
|
||||
};
|
||||
|
||||
|
||||
@@ -27,11 +27,6 @@ static const char *const TAG = "esp32_hosted.update";
|
||||
// Older coprocessor firmware versions have a 1500-byte limit per RPC call
|
||||
constexpr size_t CHUNK_SIZE = 1500;
|
||||
|
||||
#ifdef USE_ESP32_HOSTED_HTTP_UPDATE
|
||||
// Interval/timeout IDs (uint32_t to avoid string comparison)
|
||||
constexpr uint32_t INITIAL_CHECK_INTERVAL_ID = 0;
|
||||
#endif
|
||||
|
||||
// Compile-time version string from esp_hosted_host_fw_ver.h macros
|
||||
#define STRINGIFY_(x) #x
|
||||
#define STRINGIFY(x) STRINGIFY_(x)
|
||||
@@ -132,18 +127,15 @@ void Esp32HostedUpdate::setup() {
|
||||
this->status_clear_error();
|
||||
this->publish_state();
|
||||
#else
|
||||
// HTTP mode: check every 10s until network is ready (max 6 attempts)
|
||||
// HTTP mode: retry initial check every 10s until network is ready (max 6 attempts)
|
||||
// Only if update interval is > 1 minute to avoid redundant checks
|
||||
if (this->get_update_interval() > 60000) {
|
||||
this->initial_check_remaining_ = 6;
|
||||
this->set_interval(INITIAL_CHECK_INTERVAL_ID, 10000, [this]() {
|
||||
bool connected = network::is_connected();
|
||||
if (--this->initial_check_remaining_ == 0 || connected) {
|
||||
this->cancel_interval(INITIAL_CHECK_INTERVAL_ID);
|
||||
if (connected) {
|
||||
this->check();
|
||||
}
|
||||
this->set_retry("initial_check", 10000, 6, [this](uint8_t) {
|
||||
if (!network::is_connected()) {
|
||||
return RetryResult::RETRY;
|
||||
}
|
||||
this->check();
|
||||
return RetryResult::DONE;
|
||||
});
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -44,7 +44,6 @@ class Esp32HostedUpdate : public update::UpdateEntity, public PollingComponent {
|
||||
// HTTP mode helpers
|
||||
bool fetch_manifest_();
|
||||
bool stream_firmware_to_coprocessor_();
|
||||
uint8_t initial_check_remaining_{0};
|
||||
#else
|
||||
// Embedded mode members
|
||||
const uint8_t *firmware_data_{nullptr};
|
||||
|
||||
@@ -103,42 +103,6 @@ inline bool is_success(int const status) { return status >= HTTP_STATUS_OK && st
|
||||
* - ESP-IDF: blocking reads, 0 only returned when all content read
|
||||
* - Arduino: non-blocking, 0 means "no data yet" or "all content read"
|
||||
*
|
||||
* Chunked responses that complete in a reasonable time work correctly on both
|
||||
* platforms. The limitation below applies only to *streaming* chunked
|
||||
* responses where data arrives slowly over a long period.
|
||||
*
|
||||
* Streaming chunked responses are NOT supported (all platforms):
|
||||
* The read helpers (http_read_loop_result, http_read_fully) block the main
|
||||
* event loop until all response data is received. For streaming responses
|
||||
* where data trickles in slowly (e.g., TTS streaming via ffmpeg proxy),
|
||||
* this starves the event loop on both ESP-IDF and Arduino. If data arrives
|
||||
* just often enough to avoid the caller's timeout, the loop runs
|
||||
* indefinitely. If data stops entirely, ESP-IDF fails with
|
||||
* -ESP_ERR_HTTP_EAGAIN (transport timeout) while Arduino spins with
|
||||
* delay(1) until the caller's timeout fires. Supporting streaming requires
|
||||
* a non-blocking incremental read pattern that yields back to the event
|
||||
* loop between chunks. Components that need streaming should use
|
||||
* esp_http_client directly on a separate FreeRTOS task with
|
||||
* esp_http_client_is_complete_data_received() for completion detection
|
||||
* (see audio_reader.cpp for an example).
|
||||
*
|
||||
* Chunked transfer encoding - platform differences:
|
||||
* - ESP-IDF HttpContainer:
|
||||
* HttpContainerIDF overrides is_read_complete() to call
|
||||
* esp_http_client_is_complete_data_received(), which is the
|
||||
* authoritative completion check for both chunked and non-chunked
|
||||
* transfers. When esp_http_client_read() returns 0 for a completed
|
||||
* chunked response, read() returns 0 and is_read_complete() returns
|
||||
* true, so callers get COMPLETE from http_read_loop_result().
|
||||
*
|
||||
* - Arduino HttpContainer:
|
||||
* Chunked responses are decoded internally (see
|
||||
* HttpContainerArduino::read_chunked_()). When the final chunk arrives,
|
||||
* is_chunked_ is cleared and content_length is set to bytes_read_.
|
||||
* Completion is then detected via is_read_complete(), and a subsequent
|
||||
* read() returns 0 to indicate "all content read" (not
|
||||
* HTTP_ERROR_CONNECTION_CLOSED).
|
||||
*
|
||||
* Use the helper functions below instead of checking return values directly:
|
||||
* - http_read_loop_result(): for manual loops with per-chunk processing
|
||||
* - http_read_fully(): for simple "read N bytes into buffer" operations
|
||||
@@ -240,13 +204,9 @@ class HttpContainer : public Parented<HttpRequestComponent> {
|
||||
|
||||
size_t get_bytes_read() const { return this->bytes_read_; }
|
||||
|
||||
/// Check if all expected content has been read.
|
||||
/// Base implementation handles non-chunked responses and status-code-based no-body checks.
|
||||
/// Platform implementations may override for chunked completion detection:
|
||||
/// - ESP-IDF: overrides to call esp_http_client_is_complete_data_received() for chunked.
|
||||
/// - Arduino: read_chunked_() clears is_chunked_ and sets content_length on the final
|
||||
/// chunk, after which the base implementation detects completion.
|
||||
virtual bool is_read_complete() const {
|
||||
/// Check if all expected content has been read
|
||||
/// For chunked responses, returns false (completion detected via read() returning error/EOF)
|
||||
bool is_read_complete() const {
|
||||
// Per RFC 9112, these responses have no body:
|
||||
// - 1xx (Informational), 204 No Content, 205 Reset Content, 304 Not Modified
|
||||
if ((this->status_code >= 100 && this->status_code < 200) || this->status_code == HTTP_STATUS_NO_CONTENT ||
|
||||
|
||||
@@ -218,50 +218,32 @@ std::shared_ptr<HttpContainer> HttpRequestIDF::perform(const std::string &url, c
|
||||
return container;
|
||||
}
|
||||
|
||||
bool HttpContainerIDF::is_read_complete() const {
|
||||
// Base class handles no-body status codes and non-chunked content_length completion
|
||||
if (HttpContainer::is_read_complete()) {
|
||||
return true;
|
||||
}
|
||||
// For chunked responses, use the authoritative ESP-IDF completion check
|
||||
return this->is_chunked_ && esp_http_client_is_complete_data_received(this->client_);
|
||||
}
|
||||
|
||||
// ESP-IDF HTTP read implementation (blocking mode)
|
||||
//
|
||||
// WARNING: Return values differ from BSD sockets! See http_request.h for full documentation.
|
||||
//
|
||||
// esp_http_client_read() in blocking mode returns:
|
||||
// > 0: bytes read
|
||||
// 0: all chunked data received (is_chunk_complete true) or connection closed
|
||||
// -ESP_ERR_HTTP_EAGAIN: transport timeout, no data available yet
|
||||
// 0: connection closed (end of stream)
|
||||
// < 0: error
|
||||
//
|
||||
// We normalize to HttpContainer::read() contract:
|
||||
// > 0: bytes read
|
||||
// 0: all content read (for both content_length-based and chunked completion)
|
||||
// 0: all content read (only returned when content_length is known and fully read)
|
||||
// < 0: error/connection closed
|
||||
//
|
||||
// Note on chunked transfer encoding:
|
||||
// esp_http_client_fetch_headers() returns 0 for chunked responses (no Content-Length header).
|
||||
// When esp_http_client_read() returns 0 for a chunked response, is_read_complete() calls
|
||||
// esp_http_client_is_complete_data_received() to distinguish successful completion from
|
||||
// connection errors. Callers use http_read_loop_result() which checks is_read_complete()
|
||||
// to return COMPLETE for successful chunked EOF.
|
||||
//
|
||||
// Streaming chunked responses are not supported (see http_request.h for details).
|
||||
// When data stops arriving, esp_http_client_read() returns -ESP_ERR_HTTP_EAGAIN
|
||||
// after its internal transport timeout (configured via timeout_ms) expires.
|
||||
// This is passed through as a negative return value, which callers treat as an error.
|
||||
// We handle this by skipping the content_length check when content_length is 0,
|
||||
// allowing esp_http_client_read() to handle chunked decoding internally and signal EOF
|
||||
// by returning 0.
|
||||
int HttpContainerIDF::read(uint8_t *buf, size_t max_len) {
|
||||
const uint32_t start = millis();
|
||||
watchdog::WatchdogManager wdm(this->parent_->get_watchdog_timeout());
|
||||
|
||||
// Check if we've already read all expected content (non-chunked and no-body only).
|
||||
// Use the base class check here, NOT the override: esp_http_client_is_complete_data_received()
|
||||
// returns true as soon as all data arrives from the network, but data may still be in
|
||||
// the client's internal buffer waiting to be consumed by esp_http_client_read().
|
||||
if (HttpContainer::is_read_complete()) {
|
||||
// Check if we've already read all expected content (non-chunked only)
|
||||
// For chunked responses (content_length == 0), esp_http_client_read() handles EOF
|
||||
if (this->is_read_complete()) {
|
||||
return 0; // All content read successfully
|
||||
}
|
||||
|
||||
@@ -276,18 +258,15 @@ int HttpContainerIDF::read(uint8_t *buf, size_t max_len) {
|
||||
return read_len_or_error;
|
||||
}
|
||||
|
||||
// esp_http_client_read() returns 0 when:
|
||||
// - Known content_length: connection closed before all data received (error)
|
||||
// - Chunked encoding: all chunks received (is_chunk_complete true, genuine EOF)
|
||||
//
|
||||
// Return 0 in both cases. Callers use http_read_loop_result() which calls
|
||||
// is_read_complete() to distinguish these:
|
||||
// - Chunked complete: is_read_complete() returns true (via
|
||||
// esp_http_client_is_complete_data_received()), caller gets COMPLETE
|
||||
// - Non-chunked incomplete: is_read_complete() returns false, caller
|
||||
// eventually gets TIMEOUT (since no more data arrives)
|
||||
// esp_http_client_read() returns 0 in two cases:
|
||||
// 1. Known content_length: connection closed before all data received (error)
|
||||
// 2. Chunked encoding (content_length == 0): end of stream reached (EOF)
|
||||
// For case 1, returning HTTP_ERROR_CONNECTION_CLOSED is correct.
|
||||
// For case 2, 0 indicates that all chunked data has already been delivered
|
||||
// in previous successful read() calls, so treating this as a closed
|
||||
// connection does not cause any loss of response data.
|
||||
if (read_len_or_error == 0) {
|
||||
return 0;
|
||||
return HTTP_ERROR_CONNECTION_CLOSED;
|
||||
}
|
||||
|
||||
// Negative value - error, return the actual error code for debugging
|
||||
|
||||
@@ -16,7 +16,6 @@ class HttpContainerIDF : public HttpContainer {
|
||||
HttpContainerIDF(esp_http_client_handle_t client) : client_(client) {}
|
||||
int read(uint8_t *buf, size_t max_len) override;
|
||||
void end() override;
|
||||
bool is_read_complete() const override;
|
||||
|
||||
/// @brief Feeds the watchdog timer if the executing task has one attached
|
||||
void feed_wdt();
|
||||
|
||||
@@ -267,16 +267,26 @@ bool ImprovSerialComponent::parse_improv_payload_(improv::ImprovCommand &command
|
||||
for (auto &scan : results) {
|
||||
if (scan.get_is_hidden())
|
||||
continue;
|
||||
const std::string &ssid = scan.get_ssid();
|
||||
if (std::find(networks.begin(), networks.end(), ssid) != networks.end())
|
||||
const char *ssid_cstr = scan.get_ssid().c_str();
|
||||
// Check if we've already sent this SSID
|
||||
bool duplicate = false;
|
||||
for (const auto &seen : networks) {
|
||||
if (strcmp(seen.c_str(), ssid_cstr) == 0) {
|
||||
duplicate = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (duplicate)
|
||||
continue;
|
||||
// Only allocate std::string after confirming it's not a duplicate
|
||||
std::string ssid(ssid_cstr);
|
||||
// Send each ssid separately to avoid overflowing the buffer
|
||||
char rssi_buf[5]; // int8_t: -128 to 127, max 4 chars + null
|
||||
*int8_to_str(rssi_buf, scan.get_rssi()) = '\0';
|
||||
std::vector<uint8_t> data =
|
||||
improv::build_rpc_response(improv::GET_WIFI_NETWORKS, {ssid, rssi_buf, YESNO(scan.get_with_auth())}, false);
|
||||
this->send_response_(data);
|
||||
networks.push_back(ssid);
|
||||
networks.push_back(std::move(ssid));
|
||||
}
|
||||
// Send empty response to signify the end of the list.
|
||||
std::vector<uint8_t> data =
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from esphome import automation
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import audio, esp32, socket, speaker
|
||||
from esphome.components import audio, esp32, speaker
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_BITS_PER_SAMPLE,
|
||||
@@ -61,7 +61,7 @@ def _set_stream_limits(config):
|
||||
def _validate_source_speaker(config):
|
||||
fconf = fv.full_config.get()
|
||||
|
||||
# Get ID for the output speaker and add it to the source speakers config to easily inherit properties
|
||||
# Get ID for the output speaker and add it to the source speakrs config to easily inherit properties
|
||||
path = fconf.get_path_for_id(config[CONF_ID])[:-3]
|
||||
path.append(CONF_OUTPUT_SPEAKER)
|
||||
output_speaker_id = fconf.get_config_for_path(path)
|
||||
@@ -111,9 +111,6 @@ FINAL_VALIDATE_SCHEMA = cv.All(
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
# Enable wake_loop_threadsafe for immediate command processing from other tasks
|
||||
socket.require_wake_loop_threadsafe()
|
||||
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
|
||||
@@ -130,9 +127,6 @@ async def to_code(config):
|
||||
"CONFIG_SPIRAM_ALLOW_STACK_EXTERNAL_MEMORY", True
|
||||
)
|
||||
|
||||
# Initialize FixedVector with exact count of source speakers
|
||||
cg.add(var.init_source_speakers(len(config[CONF_SOURCE_SPEAKERS])))
|
||||
|
||||
for speaker_config in config[CONF_SOURCE_SPEAKERS]:
|
||||
source_speaker = cg.new_Pvariable(speaker_config[CONF_ID])
|
||||
|
||||
|
||||
@@ -8,8 +8,8 @@
|
||||
namespace esphome {
|
||||
namespace mixer_speaker {
|
||||
template<typename... Ts> class DuckingApplyAction : public Action<Ts...>, public Parented<SourceSpeaker> {
|
||||
TEMPLATABLE_VALUE(uint8_t, decibel_reduction);
|
||||
TEMPLATABLE_VALUE(uint32_t, duration);
|
||||
TEMPLATABLE_VALUE(uint8_t, decibel_reduction)
|
||||
TEMPLATABLE_VALUE(uint32_t, duration)
|
||||
void play(const Ts &...x) override {
|
||||
this->parent_->apply_ducking(this->decibel_reduction_.value(x...), this->duration_.value(x...));
|
||||
}
|
||||
|
||||
@@ -2,13 +2,11 @@
|
||||
|
||||
#ifdef USE_ESP32
|
||||
|
||||
#include "esphome/core/application.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cstring>
|
||||
|
||||
namespace esphome {
|
||||
@@ -16,7 +14,6 @@ namespace mixer_speaker {
|
||||
|
||||
static const UBaseType_t MIXER_TASK_PRIORITY = 10;
|
||||
|
||||
static const uint32_t STOPPING_TIMEOUT_MS = 5000;
|
||||
static const uint32_t TRANSFER_BUFFER_DURATION_MS = 50;
|
||||
static const uint32_t TASK_DELAY_MS = 25;
|
||||
|
||||
@@ -30,53 +27,21 @@ static const char *const TAG = "speaker_mixer";
|
||||
// Gives the Q15 fixed point scaling factor to reduce by 0 dB, 1dB, ..., 50 dB
|
||||
// dB to PCM scaling factor formula: floating_point_scale_factor = 2^(-db/6.014)
|
||||
// float to Q15 fixed point formula: q15_scale_factor = floating_point_scale_factor * 2^(15)
|
||||
static const std::array<int16_t, 51> DECIBEL_REDUCTION_TABLE = {
|
||||
static const std::vector<int16_t> DECIBEL_REDUCTION_TABLE = {
|
||||
32767, 29201, 26022, 23189, 20665, 18415, 16410, 14624, 13032, 11613, 10349, 9222, 8218, 7324, 6527, 5816, 5183,
|
||||
4619, 4116, 3668, 3269, 2913, 2596, 2313, 2061, 1837, 1637, 1459, 1300, 1158, 1032, 920, 820, 731,
|
||||
651, 580, 517, 461, 411, 366, 326, 291, 259, 231, 206, 183, 163, 146, 130, 116, 103};
|
||||
|
||||
// Event bits for SourceSpeaker command processing
|
||||
enum SourceSpeakerEventBits : uint32_t {
|
||||
SOURCE_SPEAKER_COMMAND_START = (1 << 0),
|
||||
SOURCE_SPEAKER_COMMAND_STOP = (1 << 1),
|
||||
SOURCE_SPEAKER_COMMAND_FINISH = (1 << 2),
|
||||
enum MixerEventGroupBits : uint32_t {
|
||||
COMMAND_STOP = (1 << 0), // stops the mixer task
|
||||
STATE_STARTING = (1 << 10),
|
||||
STATE_RUNNING = (1 << 11),
|
||||
STATE_STOPPING = (1 << 12),
|
||||
STATE_STOPPED = (1 << 13),
|
||||
ERR_ESP_NO_MEM = (1 << 19),
|
||||
ALL_BITS = 0x00FFFFFF, // All valid FreeRTOS event group bits
|
||||
};
|
||||
|
||||
// Event bits for mixer task control and state
|
||||
enum MixerTaskEventBits : uint32_t {
|
||||
MIXER_TASK_COMMAND_START = (1 << 0),
|
||||
MIXER_TASK_COMMAND_STOP = (1 << 1),
|
||||
MIXER_TASK_STATE_STARTING = (1 << 10),
|
||||
MIXER_TASK_STATE_RUNNING = (1 << 11),
|
||||
MIXER_TASK_STATE_STOPPING = (1 << 12),
|
||||
MIXER_TASK_STATE_STOPPED = (1 << 13),
|
||||
MIXER_TASK_ERR_ESP_NO_MEM = (1 << 19),
|
||||
MIXER_TASK_ALL_BITS = 0x00FFFFFF, // All valid FreeRTOS event group bits
|
||||
};
|
||||
|
||||
static inline uint32_t atomic_subtract_clamped(std::atomic<uint32_t> &var, uint32_t amount) {
|
||||
uint32_t current = var.load(std::memory_order_acquire);
|
||||
uint32_t subtracted = 0;
|
||||
if (current > 0) {
|
||||
uint32_t new_value;
|
||||
do {
|
||||
subtracted = std::min(amount, current);
|
||||
new_value = current - subtracted;
|
||||
} while (!var.compare_exchange_weak(current, new_value, std::memory_order_release, std::memory_order_acquire));
|
||||
}
|
||||
return subtracted;
|
||||
}
|
||||
|
||||
static bool create_event_group(EventGroupHandle_t &event_group, Component *component) {
|
||||
event_group = xEventGroupCreate();
|
||||
if (event_group == nullptr) {
|
||||
ESP_LOGE(TAG, "Failed to create event group");
|
||||
component->mark_failed();
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void SourceSpeaker::dump_config() {
|
||||
ESP_LOGCONFIG(TAG,
|
||||
"Mixer Source Speaker\n"
|
||||
@@ -90,70 +55,22 @@ void SourceSpeaker::dump_config() {
|
||||
}
|
||||
|
||||
void SourceSpeaker::setup() {
|
||||
if (!create_event_group(this->event_group_, this)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Start with loop disabled since we begin in STATE_STOPPED with no pending commands
|
||||
this->disable_loop();
|
||||
|
||||
this->parent_->get_output_speaker()->add_audio_output_callback([this](uint32_t new_frames, int64_t write_timestamp) {
|
||||
// First, drain the playback delay (frames in pipeline before this source started contributing)
|
||||
uint32_t delay_to_drain = atomic_subtract_clamped(this->playback_delay_frames_, new_frames);
|
||||
uint32_t remaining_frames = new_frames - delay_to_drain;
|
||||
// The SourceSpeaker may not have included any audio in the mixed output, so verify there were pending frames
|
||||
uint32_t speakers_playback_frames = std::min(new_frames, this->pending_playback_frames_);
|
||||
this->pending_playback_frames_ -= speakers_playback_frames;
|
||||
|
||||
// Then, count towards this source's pending playback frames
|
||||
if (remaining_frames > 0) {
|
||||
uint32_t speakers_playback_frames = atomic_subtract_clamped(this->pending_playback_frames_, remaining_frames);
|
||||
if (speakers_playback_frames > 0) {
|
||||
this->audio_output_callback_(speakers_playback_frames, write_timestamp);
|
||||
}
|
||||
if (speakers_playback_frames > 0) {
|
||||
this->audio_output_callback_(speakers_playback_frames, write_timestamp);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void SourceSpeaker::loop() {
|
||||
uint32_t event_bits = xEventGroupGetBits(this->event_group_);
|
||||
|
||||
// Process commands with priority: STOP > FINISH > START
|
||||
// This ensures stop commands take precedence over conflicting start commands
|
||||
if (event_bits & SOURCE_SPEAKER_COMMAND_STOP) {
|
||||
if (this->state_ == speaker::STATE_RUNNING) {
|
||||
// Clear both STOP and START bits - stop takes precedence
|
||||
xEventGroupClearBits(this->event_group_, SOURCE_SPEAKER_COMMAND_STOP | SOURCE_SPEAKER_COMMAND_START);
|
||||
this->enter_stopping_state_();
|
||||
} else if (this->state_ == speaker::STATE_STOPPED) {
|
||||
// Already stopped, just clear the command bits
|
||||
xEventGroupClearBits(this->event_group_, SOURCE_SPEAKER_COMMAND_STOP | SOURCE_SPEAKER_COMMAND_START);
|
||||
}
|
||||
// Leave bits set if transitioning states (STARTING/STOPPING) - will be processed once state allows
|
||||
} else if (event_bits & SOURCE_SPEAKER_COMMAND_FINISH) {
|
||||
if (this->state_ == speaker::STATE_RUNNING) {
|
||||
xEventGroupClearBits(this->event_group_, SOURCE_SPEAKER_COMMAND_FINISH);
|
||||
this->stop_gracefully_ = true;
|
||||
} else if (this->state_ == speaker::STATE_STOPPED) {
|
||||
// Already stopped, just clear the command bit
|
||||
xEventGroupClearBits(this->event_group_, SOURCE_SPEAKER_COMMAND_FINISH);
|
||||
}
|
||||
// Leave bit set if transitioning states - will be processed once state allows
|
||||
} else if (event_bits & SOURCE_SPEAKER_COMMAND_START) {
|
||||
if (this->state_ == speaker::STATE_STOPPED) {
|
||||
xEventGroupClearBits(this->event_group_, SOURCE_SPEAKER_COMMAND_START);
|
||||
this->state_ = speaker::STATE_STARTING;
|
||||
} else if (this->state_ == speaker::STATE_RUNNING) {
|
||||
// Already running, just clear the command bit
|
||||
xEventGroupClearBits(this->event_group_, SOURCE_SPEAKER_COMMAND_START);
|
||||
}
|
||||
// Leave bit set if transitioning states - will be processed once state allows
|
||||
}
|
||||
// Process state machine
|
||||
switch (this->state_) {
|
||||
case speaker::STATE_STARTING: {
|
||||
esp_err_t err = this->start_();
|
||||
if (err == ESP_OK) {
|
||||
this->pending_playback_frames_.store(0, std::memory_order_release); // reset pending playback frames
|
||||
this->playback_delay_frames_.store(0, std::memory_order_release); // reset playback delay
|
||||
this->has_contributed_.store(false, std::memory_order_release); // reset contribution tracking
|
||||
this->state_ = speaker::STATE_RUNNING;
|
||||
this->stop_gracefully_ = false;
|
||||
this->last_seen_data_ms_ = millis();
|
||||
@@ -161,62 +78,41 @@ void SourceSpeaker::loop() {
|
||||
} else {
|
||||
switch (err) {
|
||||
case ESP_ERR_NO_MEM:
|
||||
this->status_set_error(LOG_STR("Not enough memory"));
|
||||
this->status_set_error(LOG_STR("Failed to start mixer: not enough memory"));
|
||||
break;
|
||||
case ESP_ERR_NOT_SUPPORTED:
|
||||
this->status_set_error(LOG_STR("Unsupported bit depth"));
|
||||
this->status_set_error(LOG_STR("Failed to start mixer: unsupported bits per sample"));
|
||||
break;
|
||||
case ESP_ERR_INVALID_ARG:
|
||||
this->status_set_error(LOG_STR("Incompatible audio streams"));
|
||||
this->status_set_error(
|
||||
LOG_STR("Failed to start mixer: audio stream isn't compatible with the other audio stream."));
|
||||
break;
|
||||
case ESP_ERR_INVALID_STATE:
|
||||
this->status_set_error(LOG_STR("Task failed"));
|
||||
this->status_set_error(LOG_STR("Failed to start mixer: mixer task failed to start"));
|
||||
break;
|
||||
default:
|
||||
this->status_set_error(LOG_STR("Failed"));
|
||||
this->status_set_error(LOG_STR("Failed to start mixer"));
|
||||
break;
|
||||
}
|
||||
|
||||
this->enter_stopping_state_();
|
||||
this->state_ = speaker::STATE_STOPPING;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case speaker::STATE_RUNNING:
|
||||
if (!this->transfer_buffer_->has_buffered_data() &&
|
||||
(this->pending_playback_frames_.load(std::memory_order_acquire) == 0)) {
|
||||
// No audio data in buffer waiting to get mixed and no frames are pending playback
|
||||
if (!this->transfer_buffer_->has_buffered_data()) {
|
||||
if ((this->timeout_ms_.has_value() && ((millis() - this->last_seen_data_ms_) > this->timeout_ms_.value())) ||
|
||||
this->stop_gracefully_) {
|
||||
// Timeout exceeded or graceful stop requested
|
||||
this->enter_stopping_state_();
|
||||
this->state_ = speaker::STATE_STOPPING;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case speaker::STATE_STOPPING: {
|
||||
if ((this->parent_->get_output_speaker()->get_pause_state()) ||
|
||||
((millis() - this->stopping_start_ms_) > STOPPING_TIMEOUT_MS)) {
|
||||
// If parent speaker is paused or if the stopping timeout is exceeded, force stop the output speaker
|
||||
this->parent_->get_output_speaker()->stop();
|
||||
}
|
||||
|
||||
if (this->parent_->get_output_speaker()->is_stopped() ||
|
||||
(this->pending_playback_frames_.load(std::memory_order_acquire) == 0)) {
|
||||
// Output speaker is stopped OR all pending playback frames have played
|
||||
this->pending_playback_frames_.store(0, std::memory_order_release);
|
||||
this->stop_gracefully_ = false;
|
||||
|
||||
this->state_ = speaker::STATE_STOPPED;
|
||||
}
|
||||
case speaker::STATE_STOPPING:
|
||||
this->stop_();
|
||||
this->stop_gracefully_ = false;
|
||||
this->state_ = speaker::STATE_STOPPED;
|
||||
break;
|
||||
}
|
||||
case speaker::STATE_STOPPED:
|
||||
// Re-check event bits for any new commands that may have arrived
|
||||
event_bits = xEventGroupGetBits(this->event_group_);
|
||||
if (!(event_bits &
|
||||
(SOURCE_SPEAKER_COMMAND_START | SOURCE_SPEAKER_COMMAND_STOP | SOURCE_SPEAKER_COMMAND_FINISH))) {
|
||||
// No pending commands, disable loop to save CPU cycles
|
||||
this->disable_loop();
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -226,34 +122,17 @@ size_t SourceSpeaker::play(const uint8_t *data, size_t length, TickType_t ticks_
|
||||
this->start();
|
||||
}
|
||||
size_t bytes_written = 0;
|
||||
std::shared_ptr<RingBuffer> temp_ring_buffer = this->ring_buffer_.lock();
|
||||
if (temp_ring_buffer.use_count() > 0) {
|
||||
// Only write to the ring buffer if the reference is valid
|
||||
if (this->ring_buffer_.use_count() == 1) {
|
||||
std::shared_ptr<RingBuffer> temp_ring_buffer = this->ring_buffer_.lock();
|
||||
bytes_written = temp_ring_buffer->write_without_replacement(data, length, ticks_to_wait);
|
||||
if (bytes_written > 0) {
|
||||
this->last_seen_data_ms_ = millis();
|
||||
}
|
||||
} else {
|
||||
// Delay to avoid repeatedly hammering while waiting for the speaker to start
|
||||
vTaskDelay(ticks_to_wait);
|
||||
}
|
||||
return bytes_written;
|
||||
}
|
||||
|
||||
void SourceSpeaker::send_command_(uint32_t command_bit, bool wake_loop) {
|
||||
this->enable_loop_soon_any_context();
|
||||
uint32_t event_bits = xEventGroupGetBits(this->event_group_);
|
||||
if (!(event_bits & command_bit)) {
|
||||
xEventGroupSetBits(this->event_group_, command_bit);
|
||||
#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE)
|
||||
if (wake_loop) {
|
||||
App.wake_loop_threadsafe();
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
void SourceSpeaker::start() { this->send_command_(SOURCE_SPEAKER_COMMAND_START, true); }
|
||||
void SourceSpeaker::start() { this->state_ = speaker::STATE_STARTING; }
|
||||
|
||||
esp_err_t SourceSpeaker::start_() {
|
||||
const size_t ring_buffer_size = this->audio_stream_info_.ms_to_bytes(this->buffer_duration_ms_);
|
||||
@@ -264,26 +143,35 @@ esp_err_t SourceSpeaker::start_() {
|
||||
if (this->transfer_buffer_ == nullptr) {
|
||||
return ESP_ERR_NO_MEM;
|
||||
}
|
||||
std::shared_ptr<RingBuffer> temp_ring_buffer;
|
||||
|
||||
std::shared_ptr<RingBuffer> temp_ring_buffer = this->ring_buffer_.lock();
|
||||
if (!temp_ring_buffer) {
|
||||
if (!this->ring_buffer_.use_count()) {
|
||||
temp_ring_buffer = RingBuffer::create(ring_buffer_size);
|
||||
this->ring_buffer_ = temp_ring_buffer;
|
||||
}
|
||||
|
||||
if (!temp_ring_buffer) {
|
||||
if (!this->ring_buffer_.use_count()) {
|
||||
return ESP_ERR_NO_MEM;
|
||||
} else {
|
||||
this->transfer_buffer_->set_source(temp_ring_buffer);
|
||||
}
|
||||
}
|
||||
|
||||
this->pending_playback_frames_ = 0; // reset
|
||||
return this->parent_->start(this->audio_stream_info_);
|
||||
}
|
||||
|
||||
void SourceSpeaker::stop() { this->send_command_(SOURCE_SPEAKER_COMMAND_STOP); }
|
||||
void SourceSpeaker::stop() {
|
||||
if (this->state_ != speaker::STATE_STOPPED) {
|
||||
this->state_ = speaker::STATE_STOPPING;
|
||||
}
|
||||
}
|
||||
|
||||
void SourceSpeaker::finish() { this->send_command_(SOURCE_SPEAKER_COMMAND_FINISH); }
|
||||
void SourceSpeaker::stop_() {
|
||||
this->transfer_buffer_.reset(); // deallocates the transfer buffer
|
||||
}
|
||||
|
||||
void SourceSpeaker::finish() { this->stop_gracefully_ = true; }
|
||||
|
||||
bool SourceSpeaker::has_buffered_data() const {
|
||||
return ((this->transfer_buffer_.use_count() > 0) && this->transfer_buffer_->has_buffered_data());
|
||||
@@ -303,16 +191,19 @@ void SourceSpeaker::set_volume(float volume) {
|
||||
|
||||
float SourceSpeaker::get_volume() { return this->parent_->get_output_speaker()->get_volume(); }
|
||||
|
||||
size_t SourceSpeaker::process_data_from_source(std::shared_ptr<audio::AudioSourceTransferBuffer> &transfer_buffer,
|
||||
TickType_t ticks_to_wait) {
|
||||
// Store current offset, as these samples are already ducked
|
||||
const size_t current_length = transfer_buffer->available();
|
||||
size_t SourceSpeaker::process_data_from_source(TickType_t ticks_to_wait) {
|
||||
if (!this->transfer_buffer_.use_count()) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
size_t bytes_read = transfer_buffer->transfer_data_from_source(ticks_to_wait);
|
||||
// Store current offset, as these samples are already ducked
|
||||
const size_t current_length = this->transfer_buffer_->available();
|
||||
|
||||
size_t bytes_read = this->transfer_buffer_->transfer_data_from_source(ticks_to_wait);
|
||||
|
||||
uint32_t samples_to_duck = this->audio_stream_info_.bytes_to_samples(bytes_read);
|
||||
if (samples_to_duck > 0) {
|
||||
int16_t *current_buffer = reinterpret_cast<int16_t *>(transfer_buffer->get_buffer_start() + current_length);
|
||||
int16_t *current_buffer = reinterpret_cast<int16_t *>(this->transfer_buffer_->get_buffer_start() + current_length);
|
||||
|
||||
duck_samples(current_buffer, samples_to_duck, &this->current_ducking_db_reduction_,
|
||||
&this->ducking_transition_samples_remaining_, this->samples_per_ducking_step_,
|
||||
@@ -324,13 +215,10 @@ size_t SourceSpeaker::process_data_from_source(std::shared_ptr<audio::AudioSourc
|
||||
|
||||
void SourceSpeaker::apply_ducking(uint8_t decibel_reduction, uint32_t duration) {
|
||||
if (this->target_ducking_db_reduction_ != decibel_reduction) {
|
||||
// Start transition from the previous target (which becomes the new current level)
|
||||
this->current_ducking_db_reduction_ = this->target_ducking_db_reduction_;
|
||||
|
||||
this->target_ducking_db_reduction_ = decibel_reduction;
|
||||
|
||||
// Calculate the number of intermediate dB steps for the transition timing.
|
||||
// Subtract 1 because the first step is taken immediately after this calculation.
|
||||
uint8_t total_ducking_steps = 0;
|
||||
if (this->target_ducking_db_reduction_ > this->current_ducking_db_reduction_) {
|
||||
// The dB reduction level is increasing (which results in quieter audio)
|
||||
@@ -346,7 +234,7 @@ void SourceSpeaker::apply_ducking(uint8_t decibel_reduction, uint32_t duration)
|
||||
|
||||
this->samples_per_ducking_step_ = this->ducking_transition_samples_remaining_ / total_ducking_steps;
|
||||
this->ducking_transition_samples_remaining_ =
|
||||
this->samples_per_ducking_step_ * total_ducking_steps; // adjust for integer division rounding
|
||||
this->samples_per_ducking_step_ * total_ducking_steps; // Adjust for integer division rounding
|
||||
|
||||
this->current_ducking_db_reduction_ += this->db_change_per_ducking_step_;
|
||||
} else {
|
||||
@@ -405,12 +293,6 @@ void SourceSpeaker::duck_samples(int16_t *input_buffer, uint32_t input_samples_t
|
||||
}
|
||||
}
|
||||
|
||||
void SourceSpeaker::enter_stopping_state_() {
|
||||
this->state_ = speaker::STATE_STOPPING;
|
||||
this->stopping_start_ms_ = millis();
|
||||
this->transfer_buffer_.reset();
|
||||
}
|
||||
|
||||
void MixerSpeaker::dump_config() {
|
||||
ESP_LOGCONFIG(TAG,
|
||||
"Speaker Mixer:\n"
|
||||
@@ -419,74 +301,42 @@ void MixerSpeaker::dump_config() {
|
||||
}
|
||||
|
||||
void MixerSpeaker::setup() {
|
||||
if (!create_event_group(this->event_group_, this)) {
|
||||
this->event_group_ = xEventGroupCreate();
|
||||
|
||||
if (this->event_group_ == nullptr) {
|
||||
ESP_LOGE(TAG, "Failed to create event group");
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
|
||||
// Register callback to track frames in the output pipeline
|
||||
this->output_speaker_->add_audio_output_callback([this](uint32_t new_frames, int64_t write_timestamp) {
|
||||
atomic_subtract_clamped(this->frames_in_pipeline_, new_frames);
|
||||
});
|
||||
|
||||
// Start with loop disabled since no task is running and no commands are pending
|
||||
this->disable_loop();
|
||||
}
|
||||
|
||||
void MixerSpeaker::loop() {
|
||||
uint32_t event_group_bits = xEventGroupGetBits(this->event_group_);
|
||||
|
||||
// Handle pending start request
|
||||
if (event_group_bits & MIXER_TASK_COMMAND_START) {
|
||||
// Only start the task if it's fully stopped and cleaned up
|
||||
if (!this->status_has_error() && (this->task_handle_ == nullptr) && (this->task_stack_buffer_ == nullptr)) {
|
||||
esp_err_t err = this->start_task_();
|
||||
switch (err) {
|
||||
case ESP_OK:
|
||||
xEventGroupClearBits(this->event_group_, MIXER_TASK_COMMAND_START);
|
||||
break;
|
||||
case ESP_ERR_NO_MEM:
|
||||
ESP_LOGE(TAG, "Failed to start; retrying in 1 second");
|
||||
this->status_momentary_error("memory-failure", 1000);
|
||||
return;
|
||||
case ESP_ERR_INVALID_STATE:
|
||||
ESP_LOGE(TAG, "Failed to start; retrying in 1 second");
|
||||
this->status_momentary_error("task-failure", 1000);
|
||||
return;
|
||||
default:
|
||||
ESP_LOGE(TAG, "Failed to start; retrying in 1 second");
|
||||
this->status_momentary_error("failure", 1000);
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (event_group_bits & MixerEventGroupBits::STATE_STARTING) {
|
||||
ESP_LOGD(TAG, "Starting speaker mixer");
|
||||
xEventGroupClearBits(this->event_group_, MixerEventGroupBits::STATE_STARTING);
|
||||
}
|
||||
|
||||
if (event_group_bits & MIXER_TASK_STATE_STARTING) {
|
||||
ESP_LOGD(TAG, "Starting");
|
||||
xEventGroupClearBits(this->event_group_, MIXER_TASK_STATE_STARTING);
|
||||
if (event_group_bits & MixerEventGroupBits::ERR_ESP_NO_MEM) {
|
||||
this->status_set_error(LOG_STR("Failed to allocate the mixer's internal buffer"));
|
||||
xEventGroupClearBits(this->event_group_, MixerEventGroupBits::ERR_ESP_NO_MEM);
|
||||
}
|
||||
if (event_group_bits & MIXER_TASK_ERR_ESP_NO_MEM) {
|
||||
this->status_set_error(LOG_STR("Not enough memory"));
|
||||
xEventGroupClearBits(this->event_group_, MIXER_TASK_ERR_ESP_NO_MEM);
|
||||
}
|
||||
if (event_group_bits & MIXER_TASK_STATE_RUNNING) {
|
||||
ESP_LOGV(TAG, "Started");
|
||||
if (event_group_bits & MixerEventGroupBits::STATE_RUNNING) {
|
||||
ESP_LOGD(TAG, "Started speaker mixer");
|
||||
this->status_clear_error();
|
||||
xEventGroupClearBits(this->event_group_, MIXER_TASK_STATE_RUNNING);
|
||||
xEventGroupClearBits(this->event_group_, MixerEventGroupBits::STATE_RUNNING);
|
||||
}
|
||||
if (event_group_bits & MIXER_TASK_STATE_STOPPING) {
|
||||
ESP_LOGV(TAG, "Stopping");
|
||||
xEventGroupClearBits(this->event_group_, MIXER_TASK_STATE_STOPPING);
|
||||
if (event_group_bits & MixerEventGroupBits::STATE_STOPPING) {
|
||||
ESP_LOGD(TAG, "Stopping speaker mixer");
|
||||
xEventGroupClearBits(this->event_group_, MixerEventGroupBits::STATE_STOPPING);
|
||||
}
|
||||
if (event_group_bits & MIXER_TASK_STATE_STOPPED) {
|
||||
if (event_group_bits & MixerEventGroupBits::STATE_STOPPED) {
|
||||
if (this->delete_task_() == ESP_OK) {
|
||||
ESP_LOGD(TAG, "Stopped");
|
||||
xEventGroupClearBits(this->event_group_, MIXER_TASK_ALL_BITS);
|
||||
xEventGroupClearBits(this->event_group_, MixerEventGroupBits::ALL_BITS);
|
||||
}
|
||||
}
|
||||
|
||||
if (this->task_handle_ != nullptr) {
|
||||
// If the mixer task is running, check if all source speakers are stopped
|
||||
|
||||
bool all_stopped = true;
|
||||
|
||||
for (auto &speaker : this->source_speakers_) {
|
||||
@@ -494,15 +344,7 @@ void MixerSpeaker::loop() {
|
||||
}
|
||||
|
||||
if (all_stopped) {
|
||||
// Send stop command signal to the mixer task since no source speakers are active
|
||||
xEventGroupSetBits(this->event_group_, MIXER_TASK_COMMAND_STOP);
|
||||
}
|
||||
} else if (this->task_stack_buffer_ == nullptr) {
|
||||
// Task is fully stopped and cleaned up, check if we can disable loop
|
||||
event_group_bits = xEventGroupGetBits(this->event_group_);
|
||||
if (event_group_bits == 0) {
|
||||
// No pending events, disable loop to save CPU cycles
|
||||
this->disable_loop();
|
||||
this->stop();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -524,18 +366,7 @@ esp_err_t MixerSpeaker::start(audio::AudioStreamInfo &stream_info) {
|
||||
}
|
||||
}
|
||||
|
||||
this->enable_loop_soon_any_context(); // ensure loop processes command
|
||||
|
||||
uint32_t event_bits = xEventGroupGetBits(this->event_group_);
|
||||
if (!(event_bits & MIXER_TASK_COMMAND_START)) {
|
||||
// Set MIXER_TASK_COMMAND_START bit if not already set, and then immediately wake for low latency
|
||||
xEventGroupSetBits(this->event_group_, MIXER_TASK_COMMAND_START);
|
||||
#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE)
|
||||
App.wake_loop_threadsafe();
|
||||
#endif
|
||||
}
|
||||
|
||||
return ESP_OK;
|
||||
return this->start_task_();
|
||||
}
|
||||
|
||||
esp_err_t MixerSpeaker::start_task_() {
|
||||
@@ -566,32 +397,29 @@ esp_err_t MixerSpeaker::start_task_() {
|
||||
}
|
||||
|
||||
esp_err_t MixerSpeaker::delete_task_() {
|
||||
if (this->task_handle_ != nullptr) {
|
||||
// Delete the task
|
||||
vTaskDelete(this->task_handle_);
|
||||
if (!this->task_created_) {
|
||||
this->task_handle_ = nullptr;
|
||||
}
|
||||
|
||||
if ((this->task_handle_ == nullptr) && (this->task_stack_buffer_ != nullptr)) {
|
||||
// Deallocate the task stack buffer
|
||||
if (this->task_stack_in_psram_) {
|
||||
RAMAllocator<StackType_t> stack_allocator(RAMAllocator<StackType_t>::ALLOC_EXTERNAL);
|
||||
stack_allocator.deallocate(this->task_stack_buffer_, TASK_STACK_SIZE);
|
||||
} else {
|
||||
RAMAllocator<StackType_t> stack_allocator(RAMAllocator<StackType_t>::ALLOC_INTERNAL);
|
||||
stack_allocator.deallocate(this->task_stack_buffer_, TASK_STACK_SIZE);
|
||||
if (this->task_stack_buffer_ != nullptr) {
|
||||
if (this->task_stack_in_psram_) {
|
||||
RAMAllocator<StackType_t> stack_allocator(RAMAllocator<StackType_t>::ALLOC_EXTERNAL);
|
||||
stack_allocator.deallocate(this->task_stack_buffer_, TASK_STACK_SIZE);
|
||||
} else {
|
||||
RAMAllocator<StackType_t> stack_allocator(RAMAllocator<StackType_t>::ALLOC_INTERNAL);
|
||||
stack_allocator.deallocate(this->task_stack_buffer_, TASK_STACK_SIZE);
|
||||
}
|
||||
|
||||
this->task_stack_buffer_ = nullptr;
|
||||
}
|
||||
|
||||
this->task_stack_buffer_ = nullptr;
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
if ((this->task_handle_ != nullptr) || (this->task_stack_buffer_ != nullptr)) {
|
||||
return ESP_ERR_INVALID_STATE;
|
||||
}
|
||||
|
||||
return ESP_OK;
|
||||
return ESP_ERR_INVALID_STATE;
|
||||
}
|
||||
|
||||
void MixerSpeaker::stop() { xEventGroupSetBits(this->event_group_, MixerEventGroupBits::COMMAND_STOP); }
|
||||
|
||||
void MixerSpeaker::copy_frames(const int16_t *input_buffer, audio::AudioStreamInfo input_stream_info,
|
||||
int16_t *output_buffer, audio::AudioStreamInfo output_stream_info,
|
||||
uint32_t frames_to_transfer) {
|
||||
@@ -644,34 +472,32 @@ void MixerSpeaker::mix_audio_samples(const int16_t *primary_buffer, audio::Audio
|
||||
}
|
||||
|
||||
void MixerSpeaker::audio_mixer_task(void *params) {
|
||||
MixerSpeaker *this_mixer = static_cast<MixerSpeaker *>(params);
|
||||
MixerSpeaker *this_mixer = (MixerSpeaker *) params;
|
||||
|
||||
xEventGroupSetBits(this_mixer->event_group_, MIXER_TASK_STATE_STARTING);
|
||||
xEventGroupSetBits(this_mixer->event_group_, MixerEventGroupBits::STATE_STARTING);
|
||||
|
||||
this_mixer->task_created_ = true;
|
||||
|
||||
std::unique_ptr<audio::AudioSinkTransferBuffer> output_transfer_buffer = audio::AudioSinkTransferBuffer::create(
|
||||
this_mixer->audio_stream_info_.value().ms_to_bytes(TRANSFER_BUFFER_DURATION_MS));
|
||||
|
||||
if (output_transfer_buffer == nullptr) {
|
||||
xEventGroupSetBits(this_mixer->event_group_, MIXER_TASK_STATE_STOPPED | MIXER_TASK_ERR_ESP_NO_MEM);
|
||||
xEventGroupSetBits(this_mixer->event_group_,
|
||||
MixerEventGroupBits::STATE_STOPPED | MixerEventGroupBits::ERR_ESP_NO_MEM);
|
||||
|
||||
vTaskSuspend(nullptr); // Suspend this task indefinitely until the loop method deletes it
|
||||
this_mixer->task_created_ = false;
|
||||
vTaskDelete(nullptr);
|
||||
}
|
||||
|
||||
output_transfer_buffer->set_sink(this_mixer->output_speaker_);
|
||||
|
||||
xEventGroupSetBits(this_mixer->event_group_, MIXER_TASK_STATE_RUNNING);
|
||||
xEventGroupSetBits(this_mixer->event_group_, MixerEventGroupBits::STATE_RUNNING);
|
||||
|
||||
bool sent_finished = false;
|
||||
|
||||
// Pre-allocate vectors to avoid heap allocation in the loop (max 8 source speakers per schema)
|
||||
FixedVector<SourceSpeaker *> speakers_with_data;
|
||||
FixedVector<std::shared_ptr<audio::AudioSourceTransferBuffer>> transfer_buffers_with_data;
|
||||
speakers_with_data.init(this_mixer->source_speakers_.size());
|
||||
transfer_buffers_with_data.init(this_mixer->source_speakers_.size());
|
||||
|
||||
while (true) {
|
||||
uint32_t event_group_bits = xEventGroupGetBits(this_mixer->event_group_);
|
||||
if (event_group_bits & MIXER_TASK_COMMAND_STOP) {
|
||||
if (event_group_bits & MixerEventGroupBits::COMMAND_STOP) {
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -681,20 +507,15 @@ void MixerSpeaker::audio_mixer_task(void *params) {
|
||||
const uint32_t output_frames_free =
|
||||
this_mixer->audio_stream_info_.value().bytes_to_frames(output_transfer_buffer->free());
|
||||
|
||||
speakers_with_data.clear();
|
||||
transfer_buffers_with_data.clear();
|
||||
std::vector<SourceSpeaker *> speakers_with_data;
|
||||
std::vector<std::shared_ptr<audio::AudioSourceTransferBuffer>> transfer_buffers_with_data;
|
||||
|
||||
for (auto &speaker : this_mixer->source_speakers_) {
|
||||
if (speaker->is_running() && !speaker->get_pause_state()) {
|
||||
// Speaker is running and not paused, so it possibly can provide audio data
|
||||
if (speaker->get_transfer_buffer().use_count() > 0) {
|
||||
std::shared_ptr<audio::AudioSourceTransferBuffer> transfer_buffer = speaker->get_transfer_buffer().lock();
|
||||
if (transfer_buffer.use_count() == 0) {
|
||||
// No transfer buffer allocated, so skip processing this speaker
|
||||
continue;
|
||||
}
|
||||
speaker->process_data_from_source(transfer_buffer, 0); // Transfers and ducks audio from source ring buffers
|
||||
speaker->process_data_from_source(0); // Transfers and ducks audio from source ring buffers
|
||||
|
||||
if (transfer_buffer->available() > 0) {
|
||||
if ((transfer_buffer->available() > 0) && !speaker->get_pause_state()) {
|
||||
// Store the locked transfer buffers in their own vector to avoid releasing ownership until after the loop
|
||||
transfer_buffers_with_data.push_back(transfer_buffer);
|
||||
speakers_with_data.push_back(speaker);
|
||||
@@ -726,21 +547,13 @@ void MixerSpeaker::audio_mixer_task(void *params) {
|
||||
reinterpret_cast<int16_t *>(output_transfer_buffer->get_buffer_end()),
|
||||
this_mixer->audio_stream_info_.value(), frames_to_mix);
|
||||
|
||||
// Set playback delay for newly contributing source
|
||||
if (!speakers_with_data[0]->has_contributed_.load(std::memory_order_acquire)) {
|
||||
speakers_with_data[0]->playback_delay_frames_.store(
|
||||
this_mixer->frames_in_pipeline_.load(std::memory_order_acquire), std::memory_order_release);
|
||||
speakers_with_data[0]->has_contributed_.store(true, std::memory_order_release);
|
||||
}
|
||||
|
||||
// Update source speaker pending frames
|
||||
speakers_with_data[0]->pending_playback_frames_.fetch_add(frames_to_mix, std::memory_order_release);
|
||||
// Update source speaker buffer length
|
||||
transfer_buffers_with_data[0]->decrease_buffer_length(active_stream_info.frames_to_bytes(frames_to_mix));
|
||||
speakers_with_data[0]->pending_playback_frames_ += frames_to_mix;
|
||||
|
||||
// Update output transfer buffer length and pipeline frame count
|
||||
// Update output transfer buffer length
|
||||
output_transfer_buffer->increase_buffer_length(
|
||||
this_mixer->audio_stream_info_.value().frames_to_bytes(frames_to_mix));
|
||||
this_mixer->frames_in_pipeline_.fetch_add(frames_to_mix, std::memory_order_release);
|
||||
} else {
|
||||
// Speaker's stream info doesn't match the output speaker's, so it's a new source speaker
|
||||
if (!this_mixer->output_speaker_->is_stopped()) {
|
||||
@@ -755,8 +568,6 @@ void MixerSpeaker::audio_mixer_task(void *params) {
|
||||
active_stream_info.get_sample_rate());
|
||||
this_mixer->output_speaker_->set_audio_stream_info(this_mixer->audio_stream_info_.value());
|
||||
this_mixer->output_speaker_->start();
|
||||
// Reset pipeline frame count since we're starting fresh with a new sample rate
|
||||
this_mixer->frames_in_pipeline_.store(0, std::memory_order_release);
|
||||
sent_finished = false;
|
||||
}
|
||||
}
|
||||
@@ -785,39 +596,26 @@ void MixerSpeaker::audio_mixer_task(void *params) {
|
||||
}
|
||||
}
|
||||
|
||||
// Get current pipeline depth for delay calculation (before incrementing)
|
||||
uint32_t current_pipeline_frames = this_mixer->frames_in_pipeline_.load(std::memory_order_acquire);
|
||||
|
||||
// Update source transfer buffer lengths and add new audio durations to the source speaker pending playbacks
|
||||
for (size_t i = 0; i < transfer_buffers_with_data.size(); ++i) {
|
||||
// Set playback delay for newly contributing sources
|
||||
if (!speakers_with_data[i]->has_contributed_.load(std::memory_order_acquire)) {
|
||||
speakers_with_data[i]->playback_delay_frames_.store(current_pipeline_frames, std::memory_order_release);
|
||||
speakers_with_data[i]->has_contributed_.store(true, std::memory_order_release);
|
||||
}
|
||||
|
||||
speakers_with_data[i]->pending_playback_frames_.fetch_add(frames_to_mix, std::memory_order_release);
|
||||
transfer_buffers_with_data[i]->decrease_buffer_length(
|
||||
speakers_with_data[i]->get_audio_stream_info().frames_to_bytes(frames_to_mix));
|
||||
speakers_with_data[i]->pending_playback_frames_ += frames_to_mix;
|
||||
}
|
||||
|
||||
// Update output transfer buffer length and pipeline frame count (once, not per source)
|
||||
// Update output transfer buffer length
|
||||
output_transfer_buffer->increase_buffer_length(
|
||||
this_mixer->audio_stream_info_.value().frames_to_bytes(frames_to_mix));
|
||||
this_mixer->frames_in_pipeline_.fetch_add(frames_to_mix, std::memory_order_release);
|
||||
}
|
||||
}
|
||||
|
||||
xEventGroupSetBits(this_mixer->event_group_, MIXER_TASK_STATE_STOPPING);
|
||||
|
||||
// Reset pipeline frame count since the task is stopping
|
||||
this_mixer->frames_in_pipeline_.store(0, std::memory_order_release);
|
||||
xEventGroupSetBits(this_mixer->event_group_, MixerEventGroupBits::STATE_STOPPING);
|
||||
|
||||
output_transfer_buffer.reset();
|
||||
|
||||
xEventGroupSetBits(this_mixer->event_group_, MIXER_TASK_STATE_STOPPED);
|
||||
|
||||
vTaskSuspend(nullptr); // Suspend this task indefinitely until the loop method deletes it
|
||||
xEventGroupSetBits(this_mixer->event_group_, MixerEventGroupBits::STATE_STOPPED);
|
||||
this_mixer->task_created_ = false;
|
||||
vTaskDelete(nullptr);
|
||||
}
|
||||
|
||||
} // namespace mixer_speaker
|
||||
|
||||
@@ -7,31 +7,26 @@
|
||||
#include "esphome/components/speaker/speaker.h"
|
||||
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
|
||||
#include <freertos/FreeRTOS.h>
|
||||
#include <freertos/event_groups.h>
|
||||
|
||||
#include <atomic>
|
||||
#include <freertos/FreeRTOS.h>
|
||||
|
||||
namespace esphome {
|
||||
namespace mixer_speaker {
|
||||
|
||||
/* Classes for mixing several source speaker audio streams and writing it to another speaker component.
|
||||
* - Volume controls are passed through to the output speaker
|
||||
* - Source speaker commands are signaled via event group bits and processed in its loop function to ensure thread
|
||||
* safety
|
||||
* - Directly handles pausing at the SourceSpeaker level; pause state is not passed through to the output speaker.
|
||||
* - Audio sent to the SourceSpeaker must have 16 bits per sample.
|
||||
* - Audio sent to the SourceSpeaker's must have 16 bits per sample.
|
||||
* - Audio sent to the SourceSpeaker can have any number of channels. They are duplicated or ignored as needed to match
|
||||
* the number of channels required for the output speaker.
|
||||
* - In queue mode, the audio sent to the SourceSpeakers can have different sample rates.
|
||||
* - In queue mode, the audio sent to the SoureSpeakers can have different sample rates.
|
||||
* - In non-queue mode, the audio sent to the SourceSpeakers must have the same sample rates.
|
||||
* - SourceSpeaker has an internal ring buffer. It also allocates a shared_ptr for an AudioTranserBuffer object.
|
||||
* - Audio Data Flow:
|
||||
* - Audio data played on a SourceSpeaker first writes to its internal ring buffer.
|
||||
* - MixerSpeaker task temporarily takes shared ownership of each SourceSpeaker's AudioTransferBuffer.
|
||||
* - MixerSpeaker calls SourceSpeaker's `process_data_from_source`, which transfers audio from the SourceSpeaker's
|
||||
* - MixerSpeaker calls SourceSpeaker's `process_data_from_source`, which tranfers audio from the SourceSpeaker's
|
||||
* ring buffer to its AudioTransferBuffer. Audio ducking is applied at this step.
|
||||
* - In queue mode, MixerSpeaker prioritizes the earliest configured SourceSpeaker with audio data. Audio data is
|
||||
* sent to the output speaker.
|
||||
@@ -68,15 +63,13 @@ class SourceSpeaker : public speaker::Speaker, public Component {
|
||||
bool get_pause_state() const override { return this->pause_state_; }
|
||||
|
||||
/// @brief Transfers audio from the ring buffer into the transfer buffer. Ducks audio while transferring.
|
||||
/// @param transfer_buffer Locked shared_ptr to the transfer buffer (must be valid, not null)
|
||||
/// @param ticks_to_wait FreeRTOS ticks to wait while waiting to read from the ring buffer.
|
||||
/// @return Number of bytes transferred from the ring buffer.
|
||||
size_t process_data_from_source(std::shared_ptr<audio::AudioSourceTransferBuffer> &transfer_buffer,
|
||||
TickType_t ticks_to_wait);
|
||||
size_t process_data_from_source(TickType_t ticks_to_wait);
|
||||
|
||||
/// @brief Sets the ducking level for the source speaker.
|
||||
/// @param decibel_reduction The dB reduction level. For example, 0 is no change, 10 is a reduction by 10 dB
|
||||
/// @param duration The number of milliseconds to transition from the current level to the new level
|
||||
/// @param decibel_reduction (uint8_t) The dB reduction level. For example, 0 is no change, 10 is a reduction by 10 dB
|
||||
/// @param duration (uint32_t) The number of milliseconds to transition from the current level to the new level
|
||||
void apply_ducking(uint8_t decibel_reduction, uint32_t duration);
|
||||
|
||||
void set_buffer_duration(uint32_t buffer_duration_ms) { this->buffer_duration_ms_ = buffer_duration_ms; }
|
||||
@@ -88,15 +81,14 @@ class SourceSpeaker : public speaker::Speaker, public Component {
|
||||
protected:
|
||||
friend class MixerSpeaker;
|
||||
esp_err_t start_();
|
||||
void enter_stopping_state_();
|
||||
void send_command_(uint32_t command_bit, bool wake_loop = false);
|
||||
void stop_();
|
||||
|
||||
/// @brief Ducks audio samples by a specified amount. When changing the ducking amount, it can transition gradually
|
||||
/// over a specified amount of samples.
|
||||
/// @param input_buffer buffer with audio samples to be ducked in place
|
||||
/// @param input_samples_to_duck number of samples to process in ``input_buffer``
|
||||
/// @param current_ducking_db_reduction pointer to the current dB reduction
|
||||
/// @param ducking_transition_samples_remaining pointer to the total number of samples left before the
|
||||
/// @param ducking_transition_samples_remaining pointer to the total number of samples left before the the
|
||||
/// transition is finished
|
||||
/// @param samples_per_ducking_step total number of samples per ducking step for the transition
|
||||
/// @param db_change_per_ducking_step the change in dB reduction per step
|
||||
@@ -122,12 +114,7 @@ class SourceSpeaker : public speaker::Speaker, public Component {
|
||||
uint32_t ducking_transition_samples_remaining_{0};
|
||||
uint32_t samples_per_ducking_step_{0};
|
||||
|
||||
std::atomic<uint32_t> pending_playback_frames_{0};
|
||||
std::atomic<uint32_t> playback_delay_frames_{0}; // Frames in output pipeline when this source started contributing
|
||||
std::atomic<bool> has_contributed_{false}; // Tracks if source has contributed during this session
|
||||
|
||||
EventGroupHandle_t event_group_{nullptr};
|
||||
uint32_t stopping_start_ms_{0};
|
||||
uint32_t pending_playback_frames_{0};
|
||||
};
|
||||
|
||||
class MixerSpeaker : public Component {
|
||||
@@ -136,11 +123,10 @@ class MixerSpeaker : public Component {
|
||||
void setup() override;
|
||||
void loop() override;
|
||||
|
||||
void init_source_speakers(size_t count) { this->source_speakers_.init(count); }
|
||||
void add_source_speaker(SourceSpeaker *source_speaker) { this->source_speakers_.push_back(source_speaker); }
|
||||
|
||||
/// @brief Starts the mixer task. Called by a source speaker giving the current audio stream information
|
||||
/// @param stream_info The calling source speaker's audio stream information
|
||||
/// @param stream_info The calling source speakers audio stream information
|
||||
/// @return ESP_ERR_NOT_SUPPORTED if the incoming stream is incompatible due to unsupported bits per sample
|
||||
/// ESP_ERR_INVALID_ARG if the incoming stream is incompatible to be mixed with the other input audio stream
|
||||
/// ESP_ERR_NO_MEM if there isn't enough memory for the task's stack
|
||||
@@ -148,6 +134,8 @@ class MixerSpeaker : public Component {
|
||||
/// ESP_OK if the incoming stream is compatible and the mixer task starts
|
||||
esp_err_t start(audio::AudioStreamInfo &stream_info);
|
||||
|
||||
void stop();
|
||||
|
||||
void set_output_channels(uint8_t output_channels) { this->output_channels_ = output_channels; }
|
||||
void set_output_speaker(speaker::Speaker *speaker) { this->output_speaker_ = speaker; }
|
||||
void set_queue_mode(bool queue_mode) { this->queue_mode_ = queue_mode; }
|
||||
@@ -155,9 +143,6 @@ class MixerSpeaker : public Component {
|
||||
|
||||
speaker::Speaker *get_output_speaker() const { return this->output_speaker_; }
|
||||
|
||||
/// @brief Returns the current number of frames in the output pipeline (written but not yet played)
|
||||
uint32_t get_frames_in_pipeline() const { return this->frames_in_pipeline_.load(std::memory_order_acquire); }
|
||||
|
||||
protected:
|
||||
/// @brief Copies audio frames from the input buffer to the output buffer taking into account the number of channels
|
||||
/// in each stream. If the output stream has more channels, the input samples are duplicated. If the output stream has
|
||||
@@ -174,11 +159,11 @@ class MixerSpeaker : public Component {
|
||||
/// and secondary samples are duplicated or dropped as necessary to ensure the output stream has the configured number
|
||||
/// of channels. Output samples are clamped to the corresponding int16 min or max values if the mixed sample
|
||||
/// overflows.
|
||||
/// @param primary_buffer samples buffer for the primary stream
|
||||
/// @param primary_buffer (int16_t *) samples buffer for the primary stream
|
||||
/// @param primary_stream_info stream info for the primary stream
|
||||
/// @param secondary_buffer samples buffer for secondary stream
|
||||
/// @param secondary_buffer (int16_t *) samples buffer for secondary stream
|
||||
/// @param secondary_stream_info stream info for the secondary stream
|
||||
/// @param output_buffer buffer for the mixed samples
|
||||
/// @param output_buffer (int16_t *) buffer for the mixed samples
|
||||
/// @param output_stream_info stream info for the output buffer
|
||||
/// @param frames_to_mix number of frames in the primary and secondary buffers to mix together
|
||||
static void mix_audio_samples(const int16_t *primary_buffer, audio::AudioStreamInfo primary_stream_info,
|
||||
@@ -200,20 +185,20 @@ class MixerSpeaker : public Component {
|
||||
|
||||
EventGroupHandle_t event_group_{nullptr};
|
||||
|
||||
FixedVector<SourceSpeaker *> source_speakers_;
|
||||
std::vector<SourceSpeaker *> source_speakers_;
|
||||
speaker::Speaker *output_speaker_{nullptr};
|
||||
|
||||
uint8_t output_channels_;
|
||||
bool queue_mode_;
|
||||
bool task_stack_in_psram_{false};
|
||||
|
||||
bool task_created_{false};
|
||||
|
||||
TaskHandle_t task_handle_{nullptr};
|
||||
StaticTask_t task_stack_;
|
||||
StackType_t *task_stack_buffer_{nullptr};
|
||||
|
||||
optional<audio::AudioStreamInfo> audio_stream_info_;
|
||||
|
||||
std::atomic<uint32_t> frames_in_pipeline_{0}; // Frames written to output but not yet played
|
||||
};
|
||||
|
||||
} // namespace mixer_speaker
|
||||
|
||||
@@ -72,55 +72,53 @@ void MS8607Component::setup() {
|
||||
|
||||
// I do not know why the device sometimes NACKs the reset command, but
|
||||
// try 3 times in case it's a transitory issue on this boot
|
||||
// Backoff: executes at now, +5ms, +30ms
|
||||
this->reset_attempts_remaining_ = 3;
|
||||
this->reset_interval_ = 5;
|
||||
this->try_reset_();
|
||||
}
|
||||
this->set_retry(
|
||||
"reset", 5, 3,
|
||||
[this](const uint8_t remaining_setup_attempts) {
|
||||
ESP_LOGD(TAG, "Resetting both I2C addresses: 0x%02X, 0x%02X", this->address_,
|
||||
this->humidity_device_->get_address());
|
||||
// I believe sending the reset command to both addresses is preferable to
|
||||
// skipping humidity if PT fails for some reason.
|
||||
// However, only consider the reset successful if they both ACK
|
||||
bool const pt_successful = this->write_bytes(MS8607_PT_CMD_RESET, nullptr, 0);
|
||||
bool const h_successful = this->humidity_device_->write_bytes(MS8607_CMD_H_RESET, nullptr, 0);
|
||||
|
||||
void MS8607Component::try_reset_() {
|
||||
ESP_LOGD(TAG, "Resetting both I2C addresses: 0x%02X, 0x%02X", this->address_, this->humidity_device_->get_address());
|
||||
// I believe sending the reset command to both addresses is preferable to
|
||||
// skipping humidity if PT fails for some reason.
|
||||
// However, only consider the reset successful if they both ACK
|
||||
bool const pt_successful = this->write_bytes(MS8607_PT_CMD_RESET, nullptr, 0);
|
||||
bool const h_successful = this->humidity_device_->write_bytes(MS8607_CMD_H_RESET, nullptr, 0);
|
||||
if (!(pt_successful && h_successful)) {
|
||||
ESP_LOGE(TAG, "Resetting I2C devices failed");
|
||||
if (!pt_successful && !h_successful) {
|
||||
this->error_code_ = ErrorCode::PTH_RESET_FAILED;
|
||||
} else if (!pt_successful) {
|
||||
this->error_code_ = ErrorCode::PT_RESET_FAILED;
|
||||
} else {
|
||||
this->error_code_ = ErrorCode::H_RESET_FAILED;
|
||||
}
|
||||
|
||||
if (!(pt_successful && h_successful)) {
|
||||
ESP_LOGE(TAG, "Resetting I2C devices failed");
|
||||
if (!pt_successful && !h_successful) {
|
||||
this->error_code_ = ErrorCode::PTH_RESET_FAILED;
|
||||
} else if (!pt_successful) {
|
||||
this->error_code_ = ErrorCode::PT_RESET_FAILED;
|
||||
} else {
|
||||
this->error_code_ = ErrorCode::H_RESET_FAILED;
|
||||
}
|
||||
if (remaining_setup_attempts > 0) {
|
||||
this->status_set_error();
|
||||
} else {
|
||||
this->mark_failed();
|
||||
}
|
||||
return RetryResult::RETRY;
|
||||
}
|
||||
|
||||
if (--this->reset_attempts_remaining_ > 0) {
|
||||
uint32_t delay = this->reset_interval_;
|
||||
this->reset_interval_ *= 5;
|
||||
this->set_timeout("reset", delay, [this]() { this->try_reset_(); });
|
||||
this->status_set_error();
|
||||
} else {
|
||||
this->mark_failed();
|
||||
}
|
||||
return;
|
||||
}
|
||||
this->setup_status_ = SetupStatus::NEEDS_PROM_READ;
|
||||
this->error_code_ = ErrorCode::NONE;
|
||||
this->status_clear_error();
|
||||
|
||||
this->setup_status_ = SetupStatus::NEEDS_PROM_READ;
|
||||
this->error_code_ = ErrorCode::NONE;
|
||||
this->status_clear_error();
|
||||
// 15ms delay matches datasheet, Adafruit_MS8607 & SparkFun_PHT_MS8607_Arduino_Library
|
||||
this->set_timeout("prom-read", 15, [this]() {
|
||||
if (this->read_calibration_values_from_prom_()) {
|
||||
this->setup_status_ = SetupStatus::SUCCESSFUL;
|
||||
this->status_clear_error();
|
||||
} else {
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
// 15ms delay matches datasheet, Adafruit_MS8607 & SparkFun_PHT_MS8607_Arduino_Library
|
||||
this->set_timeout("prom-read", 15, [this]() {
|
||||
if (this->read_calibration_values_from_prom_()) {
|
||||
this->setup_status_ = SetupStatus::SUCCESSFUL;
|
||||
this->status_clear_error();
|
||||
} else {
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
});
|
||||
return RetryResult::DONE;
|
||||
},
|
||||
5.0f); // executes at now, +5ms, +25ms
|
||||
}
|
||||
|
||||
void MS8607Component::update() {
|
||||
|
||||
@@ -44,8 +44,6 @@ class MS8607Component : public PollingComponent, public i2c::I2CDevice {
|
||||
void set_humidity_device(MS8607HumidityDevice *humidity_device) { humidity_device_ = humidity_device; }
|
||||
|
||||
protected:
|
||||
/// Attempt to reset both I2C devices, retrying with backoff on failure
|
||||
void try_reset_();
|
||||
/**
|
||||
Read and store the Pressure & Temperature calibration settings from the PROM.
|
||||
Intended to be called during setup(), this will set the `failure_reason_`
|
||||
@@ -104,8 +102,6 @@ class MS8607Component : public PollingComponent, public i2c::I2CDevice {
|
||||
enum class SetupStatus;
|
||||
/// Current step in the multi-step & possibly delayed setup() process
|
||||
SetupStatus setup_status_;
|
||||
uint32_t reset_interval_{5};
|
||||
uint8_t reset_attempts_remaining_{0};
|
||||
};
|
||||
|
||||
} // namespace ms8607
|
||||
|
||||
@@ -103,20 +103,6 @@ void SpeakerMediaPlayer::set_playlist_delay_ms(AudioPipelineType pipeline_type,
|
||||
}
|
||||
}
|
||||
|
||||
void SpeakerMediaPlayer::stop_and_unpause_media_() {
|
||||
this->media_pipeline_->stop();
|
||||
this->unpause_media_remaining_ = 3;
|
||||
this->set_interval("unpause_med", 50, [this]() {
|
||||
if (this->media_pipeline_state_ == AudioPipelineState::STOPPED) {
|
||||
this->cancel_interval("unpause_med");
|
||||
this->media_pipeline_->set_pause_state(false);
|
||||
this->is_paused_ = false;
|
||||
} else if (--this->unpause_media_remaining_ == 0) {
|
||||
this->cancel_interval("unpause_med");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void SpeakerMediaPlayer::watch_media_commands_() {
|
||||
if (!this->is_ready()) {
|
||||
return;
|
||||
@@ -158,7 +144,15 @@ void SpeakerMediaPlayer::watch_media_commands_() {
|
||||
if (this->is_paused_) {
|
||||
// If paused, stop the media pipeline and unpause it after confirming its stopped. This avoids playing a
|
||||
// short segment of the paused file before starting the new one.
|
||||
this->stop_and_unpause_media_();
|
||||
this->media_pipeline_->stop();
|
||||
this->set_retry("unpause_med", 50, 3, [this](const uint8_t remaining_attempts) {
|
||||
if (this->media_pipeline_state_ == AudioPipelineState::STOPPED) {
|
||||
this->media_pipeline_->set_pause_state(false);
|
||||
this->is_paused_ = false;
|
||||
return RetryResult::DONE;
|
||||
}
|
||||
return RetryResult::RETRY;
|
||||
});
|
||||
} else {
|
||||
// Not paused, just directly start the file
|
||||
if (media_command.file.has_value()) {
|
||||
@@ -203,21 +197,27 @@ void SpeakerMediaPlayer::watch_media_commands_() {
|
||||
this->cancel_timeout("next_ann");
|
||||
this->announcement_playlist_.clear();
|
||||
this->announcement_pipeline_->stop();
|
||||
this->unpause_announcement_remaining_ = 3;
|
||||
this->set_interval("unpause_ann", 50, [this]() {
|
||||
this->set_retry("unpause_ann", 50, 3, [this](const uint8_t remaining_attempts) {
|
||||
if (this->announcement_pipeline_state_ == AudioPipelineState::STOPPED) {
|
||||
this->cancel_interval("unpause_ann");
|
||||
this->announcement_pipeline_->set_pause_state(false);
|
||||
} else if (--this->unpause_announcement_remaining_ == 0) {
|
||||
this->cancel_interval("unpause_ann");
|
||||
return RetryResult::DONE;
|
||||
}
|
||||
return RetryResult::RETRY;
|
||||
});
|
||||
}
|
||||
} else {
|
||||
if (this->media_pipeline_ != nullptr) {
|
||||
this->cancel_timeout("next_media");
|
||||
this->media_playlist_.clear();
|
||||
this->stop_and_unpause_media_();
|
||||
this->media_pipeline_->stop();
|
||||
this->set_retry("unpause_med", 50, 3, [this](const uint8_t remaining_attempts) {
|
||||
if (this->media_pipeline_state_ == AudioPipelineState::STOPPED) {
|
||||
this->media_pipeline_->set_pause_state(false);
|
||||
this->is_paused_ = false;
|
||||
return RetryResult::DONE;
|
||||
}
|
||||
return RetryResult::RETRY;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -112,9 +112,6 @@ class SpeakerMediaPlayer : public Component,
|
||||
/// media pipelines are defined.
|
||||
inline bool single_pipeline_() { return (this->media_speaker_ == nullptr); }
|
||||
|
||||
/// Stops the media pipeline and polls until stopped to unpause it, avoiding an audible glitch.
|
||||
void stop_and_unpause_media_();
|
||||
|
||||
// Processes commands from media_control_command_queue_.
|
||||
void watch_media_commands_();
|
||||
|
||||
@@ -144,8 +141,6 @@ class SpeakerMediaPlayer : public Component,
|
||||
|
||||
bool is_paused_{false};
|
||||
bool is_muted_{false};
|
||||
uint8_t unpause_media_remaining_{0};
|
||||
uint8_t unpause_announcement_remaining_{0};
|
||||
|
||||
// The amount to change the volume on volume up/down commands
|
||||
float volume_increment_;
|
||||
|
||||
@@ -349,7 +349,7 @@ bool WiFiComponent::needs_scan_results_() const {
|
||||
return this->scan_result_.empty() || !this->scan_result_[0].get_matches();
|
||||
}
|
||||
|
||||
bool WiFiComponent::ssid_was_seen_in_scan_(const std::string &ssid) const {
|
||||
bool WiFiComponent::ssid_was_seen_in_scan_(const CompactString &ssid) const {
|
||||
// Check if this SSID is configured as hidden
|
||||
// If explicitly marked hidden, we should always try hidden mode regardless of scan results
|
||||
for (const auto &conf : this->sta_) {
|
||||
@@ -960,9 +960,12 @@ WiFiAP WiFiComponent::get_sta() const {
|
||||
return config ? *config : WiFiAP{};
|
||||
}
|
||||
void WiFiComponent::save_wifi_sta(const std::string &ssid, const std::string &password) {
|
||||
this->save_wifi_sta(ssid.c_str(), password.c_str());
|
||||
}
|
||||
void WiFiComponent::save_wifi_sta(const char *ssid, const char *password) {
|
||||
SavedWifiSettings save{}; // zero-initialized - all bytes set to \0, guaranteeing null termination
|
||||
strncpy(save.ssid, ssid.c_str(), sizeof(save.ssid) - 1); // max 32 chars, byte 32 remains \0
|
||||
strncpy(save.password, password.c_str(), sizeof(save.password) - 1); // max 64 chars, byte 64 remains \0
|
||||
strncpy(save.ssid, ssid, sizeof(save.ssid) - 1); // max 32 chars, byte 32 remains \0
|
||||
strncpy(save.password, password, sizeof(save.password) - 1); // max 64 chars, byte 64 remains \0
|
||||
this->pref_.save(&save);
|
||||
// ensure it's written immediately
|
||||
global_preferences->sync();
|
||||
@@ -1825,11 +1828,11 @@ void WiFiComponent::log_and_adjust_priority_for_failed_connect_() {
|
||||
}
|
||||
|
||||
// Get SSID for logging (use pointer to avoid copy)
|
||||
const std::string *ssid = nullptr;
|
||||
const char *ssid = nullptr;
|
||||
if (this->retry_phase_ == WiFiRetryPhase::SCAN_CONNECTING && !this->scan_result_.empty()) {
|
||||
ssid = &this->scan_result_[0].get_ssid();
|
||||
ssid = this->scan_result_[0].get_ssid().c_str();
|
||||
} else if (const WiFiAP *config = this->get_selected_sta_()) {
|
||||
ssid = &config->get_ssid();
|
||||
ssid = config->get_ssid().c_str();
|
||||
}
|
||||
|
||||
// Only decrease priority on the last attempt for this phase
|
||||
@@ -1849,8 +1852,8 @@ void WiFiComponent::log_and_adjust_priority_for_failed_connect_() {
|
||||
}
|
||||
char bssid_s[18];
|
||||
format_mac_addr_upper(failed_bssid.value().data(), bssid_s);
|
||||
ESP_LOGD(TAG, "Failed " LOG_SECRET("'%s'") " " LOG_SECRET("(%s)") ", priority %d → %d",
|
||||
ssid != nullptr ? ssid->c_str() : "", bssid_s, old_priority, new_priority);
|
||||
ESP_LOGD(TAG, "Failed " LOG_SECRET("'%s'") " " LOG_SECRET("(%s)") ", priority %d → %d", ssid != nullptr ? ssid : "",
|
||||
bssid_s, old_priority, new_priority);
|
||||
|
||||
// After adjusting priority, check if all priorities are now at minimum
|
||||
// If so, clear the vector to save memory and reset for fresh start
|
||||
@@ -2098,10 +2101,14 @@ void WiFiComponent::save_fast_connect_settings_() {
|
||||
}
|
||||
#endif
|
||||
|
||||
void WiFiAP::set_ssid(const std::string &ssid) { this->ssid_ = ssid; }
|
||||
void WiFiAP::set_ssid(const std::string &ssid) { this->ssid_ = CompactString(ssid.c_str(), ssid.size()); }
|
||||
void WiFiAP::set_ssid(const char *ssid) { this->ssid_ = CompactString(ssid, strlen(ssid)); }
|
||||
void WiFiAP::set_bssid(const bssid_t &bssid) { this->bssid_ = bssid; }
|
||||
void WiFiAP::clear_bssid() { this->bssid_ = {}; }
|
||||
void WiFiAP::set_password(const std::string &password) { this->password_ = password; }
|
||||
void WiFiAP::set_password(const std::string &password) {
|
||||
this->password_ = CompactString(password.c_str(), password.size());
|
||||
}
|
||||
void WiFiAP::set_password(const char *password) { this->password_ = CompactString(password, strlen(password)); }
|
||||
#ifdef USE_WIFI_WPA2_EAP
|
||||
void WiFiAP::set_eap(optional<EAPAuth> eap_auth) { this->eap_ = std::move(eap_auth); }
|
||||
#endif
|
||||
@@ -2111,10 +2118,8 @@ void WiFiAP::clear_channel() { this->channel_ = 0; }
|
||||
void WiFiAP::set_manual_ip(optional<ManualIP> manual_ip) { this->manual_ip_ = manual_ip; }
|
||||
#endif
|
||||
void WiFiAP::set_hidden(bool hidden) { this->hidden_ = hidden; }
|
||||
const std::string &WiFiAP::get_ssid() const { return this->ssid_; }
|
||||
const bssid_t &WiFiAP::get_bssid() const { return this->bssid_; }
|
||||
bool WiFiAP::has_bssid() const { return this->bssid_ != bssid_t{}; }
|
||||
const std::string &WiFiAP::get_password() const { return this->password_; }
|
||||
#ifdef USE_WIFI_WPA2_EAP
|
||||
const optional<EAPAuth> &WiFiAP::get_eap() const { return this->eap_; }
|
||||
#endif
|
||||
@@ -2125,12 +2130,12 @@ const optional<ManualIP> &WiFiAP::get_manual_ip() const { return this->manual_ip
|
||||
#endif
|
||||
bool WiFiAP::get_hidden() const { return this->hidden_; }
|
||||
|
||||
WiFiScanResult::WiFiScanResult(const bssid_t &bssid, std::string ssid, uint8_t channel, int8_t rssi, bool with_auth,
|
||||
bool is_hidden)
|
||||
WiFiScanResult::WiFiScanResult(const bssid_t &bssid, const char *ssid, size_t ssid_len, uint8_t channel, int8_t rssi,
|
||||
bool with_auth, bool is_hidden)
|
||||
: bssid_(bssid),
|
||||
channel_(channel),
|
||||
rssi_(rssi),
|
||||
ssid_(std::move(ssid)),
|
||||
ssid_(ssid, ssid_len),
|
||||
with_auth_(with_auth),
|
||||
is_hidden_(is_hidden) {}
|
||||
bool WiFiScanResult::matches(const WiFiAP &config) const {
|
||||
@@ -2173,7 +2178,6 @@ bool WiFiScanResult::matches(const WiFiAP &config) const {
|
||||
bool WiFiScanResult::get_matches() const { return this->matches_; }
|
||||
void WiFiScanResult::set_matches(bool matches) { this->matches_ = matches; }
|
||||
const bssid_t &WiFiScanResult::get_bssid() const { return this->bssid_; }
|
||||
const std::string &WiFiScanResult::get_ssid() const { return this->ssid_; }
|
||||
uint8_t WiFiScanResult::get_channel() const { return this->channel_; }
|
||||
int8_t WiFiScanResult::get_rssi() const { return this->rssi_; }
|
||||
bool WiFiScanResult::get_with_auth() const { return this->with_auth_; }
|
||||
@@ -2284,7 +2288,7 @@ void WiFiComponent::process_roaming_scan_() {
|
||||
|
||||
for (const auto &result : this->scan_result_) {
|
||||
// Must be same SSID, different BSSID
|
||||
if (current_ssid != result.get_ssid() || result.get_bssid() == current_bssid)
|
||||
if (result.get_ssid() != current_ssid.c_str() || result.get_bssid() == current_bssid)
|
||||
continue;
|
||||
|
||||
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
|
||||
|
||||
@@ -175,9 +175,13 @@ template<typename T> using wifi_scan_vector_t = FixedVector<T>;
|
||||
class WiFiAP {
|
||||
public:
|
||||
void set_ssid(const std::string &ssid);
|
||||
void set_ssid(const char *ssid);
|
||||
void set_ssid(const CompactString &ssid) { this->ssid_ = ssid; }
|
||||
void set_bssid(const bssid_t &bssid);
|
||||
void clear_bssid();
|
||||
void set_password(const std::string &password);
|
||||
void set_password(const char *password);
|
||||
void set_password(const CompactString &password) { this->password_ = password; }
|
||||
#ifdef USE_WIFI_WPA2_EAP
|
||||
void set_eap(optional<EAPAuth> eap_auth);
|
||||
#endif // USE_WIFI_WPA2_EAP
|
||||
@@ -188,10 +192,10 @@ class WiFiAP {
|
||||
void set_manual_ip(optional<ManualIP> manual_ip);
|
||||
#endif
|
||||
void set_hidden(bool hidden);
|
||||
const std::string &get_ssid() const;
|
||||
const CompactString &get_ssid() const { return this->ssid_; }
|
||||
const CompactString &get_password() const { return this->password_; }
|
||||
const bssid_t &get_bssid() const;
|
||||
bool has_bssid() const;
|
||||
const std::string &get_password() const;
|
||||
#ifdef USE_WIFI_WPA2_EAP
|
||||
const optional<EAPAuth> &get_eap() const;
|
||||
#endif // USE_WIFI_WPA2_EAP
|
||||
@@ -204,8 +208,8 @@ class WiFiAP {
|
||||
bool get_hidden() const;
|
||||
|
||||
protected:
|
||||
std::string ssid_;
|
||||
std::string password_;
|
||||
CompactString ssid_;
|
||||
CompactString password_;
|
||||
#ifdef USE_WIFI_WPA2_EAP
|
||||
optional<EAPAuth> eap_;
|
||||
#endif // USE_WIFI_WPA2_EAP
|
||||
@@ -221,14 +225,15 @@ class WiFiAP {
|
||||
|
||||
class WiFiScanResult {
|
||||
public:
|
||||
WiFiScanResult(const bssid_t &bssid, std::string ssid, uint8_t channel, int8_t rssi, bool with_auth, bool is_hidden);
|
||||
WiFiScanResult(const bssid_t &bssid, const char *ssid, size_t ssid_len, uint8_t channel, int8_t rssi, bool with_auth,
|
||||
bool is_hidden);
|
||||
|
||||
bool matches(const WiFiAP &config) const;
|
||||
|
||||
bool get_matches() const;
|
||||
void set_matches(bool matches);
|
||||
const bssid_t &get_bssid() const;
|
||||
const std::string &get_ssid() const;
|
||||
const CompactString &get_ssid() const { return this->ssid_; }
|
||||
uint8_t get_channel() const;
|
||||
int8_t get_rssi() const;
|
||||
bool get_with_auth() const;
|
||||
@@ -242,7 +247,7 @@ class WiFiScanResult {
|
||||
bssid_t bssid_;
|
||||
uint8_t channel_;
|
||||
int8_t rssi_;
|
||||
std::string ssid_;
|
||||
CompactString ssid_;
|
||||
int8_t priority_{0};
|
||||
bool matches_{false};
|
||||
bool with_auth_;
|
||||
@@ -381,6 +386,10 @@ class WiFiComponent : public Component {
|
||||
void set_passive_scan(bool passive);
|
||||
|
||||
void save_wifi_sta(const std::string &ssid, const std::string &password);
|
||||
void save_wifi_sta(const char *ssid, const char *password);
|
||||
void save_wifi_sta(const CompactString &ssid, const CompactString &password) {
|
||||
this->save_wifi_sta(ssid.c_str(), password.c_str());
|
||||
}
|
||||
|
||||
// ========== INTERNAL METHODS ==========
|
||||
// (In most use cases you won't need these)
|
||||
@@ -545,7 +554,7 @@ class WiFiComponent : public Component {
|
||||
int8_t find_first_non_hidden_index_() const;
|
||||
/// Check if an SSID was seen in the most recent scan results
|
||||
/// Used to skip hidden mode for SSIDs we know are visible
|
||||
bool ssid_was_seen_in_scan_(const std::string &ssid) const;
|
||||
bool ssid_was_seen_in_scan_(const CompactString &ssid) const;
|
||||
/// Check if full scan results are needed (captive portal active, improv, listeners)
|
||||
bool needs_full_scan_results_() const;
|
||||
/// Check if network matches any configured network (for scan result filtering)
|
||||
|
||||
@@ -738,8 +738,8 @@ void WiFiComponent::wifi_scan_done_callback_(void *arg, STATUS status) {
|
||||
const char *ssid_cstr = reinterpret_cast<const char *>(it->ssid);
|
||||
if (needs_full || this->matches_configured_network_(ssid_cstr, it->bssid)) {
|
||||
this->scan_result_.emplace_back(
|
||||
bssid_t{it->bssid[0], it->bssid[1], it->bssid[2], it->bssid[3], it->bssid[4], it->bssid[5]},
|
||||
std::string(ssid_cstr, it->ssid_len), it->channel, it->rssi, it->authmode != AUTH_OPEN, it->is_hidden != 0);
|
||||
bssid_t{it->bssid[0], it->bssid[1], it->bssid[2], it->bssid[3], it->bssid[4], it->bssid[5]}, ssid_cstr,
|
||||
it->ssid_len, it->channel, it->rssi, it->authmode != AUTH_OPEN, it->is_hidden != 0);
|
||||
} else {
|
||||
this->log_discarded_scan_result_(ssid_cstr, it->bssid, it->rssi, it->channel);
|
||||
}
|
||||
|
||||
@@ -864,8 +864,7 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) {
|
||||
if (needs_full || this->matches_configured_network_(ssid_cstr, record.bssid)) {
|
||||
bssid_t bssid;
|
||||
std::copy(record.bssid, record.bssid + 6, bssid.begin());
|
||||
std::string ssid(ssid_cstr);
|
||||
this->scan_result_.emplace_back(bssid, std::move(ssid), record.primary, record.rssi,
|
||||
this->scan_result_.emplace_back(bssid, ssid_cstr, strlen(ssid_cstr), record.primary, record.rssi,
|
||||
record.authmode != WIFI_AUTH_OPEN, ssid_cstr[0] == '\0');
|
||||
} else {
|
||||
this->log_discarded_scan_result_(ssid_cstr, record.bssid, record.rssi, record.primary);
|
||||
|
||||
@@ -688,7 +688,7 @@ void WiFiComponent::wifi_scan_done_callback_() {
|
||||
auto &ap = scan->ap[i];
|
||||
this->scan_result_.emplace_back(bssid_t{ap.bssid.addr[0], ap.bssid.addr[1], ap.bssid.addr[2], ap.bssid.addr[3],
|
||||
ap.bssid.addr[4], ap.bssid.addr[5]},
|
||||
std::string(ssid_cstr), ap.channel, ap.rssi, ap.auth != WIFI_AUTH_OPEN,
|
||||
ssid_cstr, strlen(ssid_cstr), ap.channel, ap.rssi, ap.auth != WIFI_AUTH_OPEN,
|
||||
ssid_cstr[0] == '\0');
|
||||
} else {
|
||||
auto &ap = scan->ap[i];
|
||||
|
||||
@@ -149,9 +149,8 @@ void WiFiComponent::wifi_scan_result(void *env, const cyw43_ev_scan_result_t *re
|
||||
|
||||
bssid_t bssid;
|
||||
std::copy(result->bssid, result->bssid + 6, bssid.begin());
|
||||
std::string ssid(ssid_cstr);
|
||||
WiFiScanResult res(bssid, std::move(ssid), result->channel, result->rssi, result->auth_mode != CYW43_AUTH_OPEN,
|
||||
ssid_cstr[0] == '\0');
|
||||
WiFiScanResult res(bssid, ssid_cstr, strlen(ssid_cstr), result->channel, result->rssi,
|
||||
result->auth_mode != CYW43_AUTH_OPEN, ssid_cstr[0] == '\0');
|
||||
if (std::find(this->scan_result_.begin(), this->scan_result_.end(), res) == this->scan_result_.end()) {
|
||||
this->scan_result_.push_back(res);
|
||||
}
|
||||
|
||||
@@ -89,7 +89,7 @@ void ScanResultsWiFiInfo::on_wifi_scan_results(const wifi::wifi_scan_vector_t<wi
|
||||
for (const auto &scan : results) {
|
||||
if (scan.get_is_hidden())
|
||||
continue;
|
||||
const std::string &ssid = scan.get_ssid();
|
||||
const auto &ssid = scan.get_ssid();
|
||||
// Max space: ssid + ": " (2) + "-128" (4) + "dB\n" (3) = ssid + 9
|
||||
if (ptr + ssid.size() + 9 > end)
|
||||
break;
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
#include <cstdarg>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <new>
|
||||
|
||||
#ifdef USE_ESP32
|
||||
#include "rom/crc.h"
|
||||
@@ -858,4 +859,60 @@ void IRAM_ATTR HOT delay_microseconds_safe(uint32_t us) {
|
||||
;
|
||||
}
|
||||
|
||||
// CompactString implementation
|
||||
CompactString::CompactString(const char *str, size_t len) {
|
||||
if (len > MAX_LENGTH) {
|
||||
len = MAX_LENGTH; // Clamp to max valid length
|
||||
}
|
||||
|
||||
this->length_ = len;
|
||||
if (len <= INLINE_CAPACITY) {
|
||||
// Store inline with null terminator
|
||||
this->is_heap_ = 0;
|
||||
if (len > 0) {
|
||||
std::memcpy(this->storage_, str, len);
|
||||
}
|
||||
this->storage_[len] = '\0';
|
||||
} else {
|
||||
// Heap allocate with null terminator
|
||||
this->is_heap_ = 1;
|
||||
char *heap_data = new char[len + 1]; // NOLINT(cppcoreguidelines-owning-memory)
|
||||
std::memcpy(heap_data, str, len);
|
||||
heap_data[len] = '\0';
|
||||
this->set_heap_ptr_(heap_data);
|
||||
}
|
||||
}
|
||||
|
||||
CompactString::CompactString(const CompactString &other) : CompactString(other.data(), other.size()) {}
|
||||
|
||||
CompactString &CompactString::operator=(const CompactString &other) {
|
||||
if (this != &other) {
|
||||
this->~CompactString();
|
||||
new (this) CompactString(other);
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
|
||||
CompactString::CompactString(CompactString &&other) noexcept : length_(other.length_), is_heap_(other.is_heap_) {
|
||||
// Copy full storage (includes null terminator for inline, or pointer for heap)
|
||||
std::memcpy(this->storage_, other.storage_, INLINE_CAPACITY + 1);
|
||||
other.length_ = 0;
|
||||
other.is_heap_ = 0;
|
||||
other.storage_[0] = '\0';
|
||||
}
|
||||
|
||||
CompactString &CompactString::operator=(CompactString &&other) noexcept {
|
||||
if (this != &other) {
|
||||
this->~CompactString();
|
||||
new (this) CompactString(std::move(other));
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
|
||||
CompactString::~CompactString() {
|
||||
if (this->is_heap_) {
|
||||
delete[] this->get_heap_ptr_(); // NOLINT(cppcoreguidelines-owning-memory)
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace esphome
|
||||
|
||||
@@ -1787,4 +1787,58 @@ template<typename T, enable_if_t<std::is_pointer<T *>::value, int> = 0> T &id(T
|
||||
|
||||
///@}
|
||||
|
||||
/// 20-byte string: 18 chars inline + null, heap for longer. Always null-terminated.
|
||||
class CompactString {
|
||||
public:
|
||||
static constexpr uint8_t MAX_LENGTH = 127;
|
||||
static constexpr uint8_t INLINE_CAPACITY = 18; // 18 chars + null terminator fits in 19 bytes
|
||||
static constexpr uint8_t BUFFER_SIZE = MAX_LENGTH + 1; // For external buffer (128 bytes)
|
||||
|
||||
CompactString() : length_(0), is_heap_(0) { this->storage_[0] = '\0'; }
|
||||
CompactString(const char *str, size_t len);
|
||||
CompactString(const CompactString &other);
|
||||
CompactString(CompactString &&other) noexcept;
|
||||
CompactString &operator=(const CompactString &other);
|
||||
CompactString &operator=(CompactString &&other) noexcept;
|
||||
~CompactString();
|
||||
|
||||
const char *data() const { return this->is_heap_ ? this->get_heap_ptr_() : this->storage_; }
|
||||
const char *c_str() const { return this->data(); } // Always null-terminated
|
||||
size_t size() const { return this->length_; }
|
||||
bool empty() const { return this->length_ == 0; }
|
||||
|
||||
// Implicit conversion to std::string for backwards compatibility
|
||||
operator std::string() const { return std::string(this->data(), this->size()); }
|
||||
|
||||
bool operator==(const CompactString &other) const {
|
||||
return this->size() == other.size() && std::memcmp(this->data(), other.data(), this->size()) == 0;
|
||||
}
|
||||
bool operator==(const std::string &other) const {
|
||||
return this->size() == other.size() && std::memcmp(this->data(), other.data(), this->size()) == 0;
|
||||
}
|
||||
bool operator==(const char *other) const {
|
||||
return this->size() == std::strlen(other) && std::memcmp(this->data(), other, this->size()) == 0;
|
||||
}
|
||||
bool operator!=(const CompactString &other) const { return !(*this == other); }
|
||||
bool operator!=(const std::string &other) const { return !(*this == other); }
|
||||
bool operator!=(const char *other) const { return !(*this == other); }
|
||||
|
||||
protected:
|
||||
char *get_heap_ptr_() const {
|
||||
char *ptr;
|
||||
std::memcpy(&ptr, this->storage_, sizeof(ptr));
|
||||
return ptr;
|
||||
}
|
||||
void set_heap_ptr_(char *ptr) { std::memcpy(this->storage_, &ptr, sizeof(ptr)); }
|
||||
|
||||
// Storage for string data. When is_heap_=0, contains the string directly (null-terminated).
|
||||
// When is_heap_=1, first sizeof(char*) bytes contain pointer to heap allocation.
|
||||
char storage_[INLINE_CAPACITY + 1]; // 19 bytes: 18 chars + null terminator
|
||||
uint8_t length_ : 7; // String length (0-127)
|
||||
uint8_t is_heap_ : 1; // 1 if using heap pointer, 0 if using inline storage
|
||||
// Total size: 20 bytes (19 bytes storage + 1 byte bitfields)
|
||||
};
|
||||
|
||||
static_assert(sizeof(CompactString) == 20, "CompactString must be exactly 20 bytes");
|
||||
|
||||
} // namespace esphome
|
||||
|
||||
@@ -390,19 +390,20 @@ void Scheduler::full_cleanup_removed_items_() {
|
||||
// 4. No operations inside can block or take other locks, so no deadlock risk
|
||||
LockGuard guard{this->lock_};
|
||||
|
||||
// Compact in-place: move valid items forward, recycle removed ones
|
||||
size_t write = 0;
|
||||
for (size_t read = 0; read < this->items_.size(); ++read) {
|
||||
if (!is_item_removed_(this->items_[read].get())) {
|
||||
if (write != read) {
|
||||
this->items_[write] = std::move(this->items_[read]);
|
||||
}
|
||||
++write;
|
||||
std::vector<std::unique_ptr<SchedulerItem>> valid_items;
|
||||
|
||||
// Move all non-removed items to valid_items, recycle removed ones
|
||||
for (auto &item : this->items_) {
|
||||
if (!is_item_removed_(item.get())) {
|
||||
valid_items.push_back(std::move(item));
|
||||
} else {
|
||||
this->recycle_item_main_loop_(std::move(this->items_[read]));
|
||||
// Recycle removed items
|
||||
this->recycle_item_main_loop_(std::move(item));
|
||||
}
|
||||
}
|
||||
this->items_.erase(this->items_.begin() + write, this->items_.end());
|
||||
|
||||
// Replace items_ with the filtered list
|
||||
this->items_ = std::move(valid_items);
|
||||
// Rebuild the heap structure since items are no longer in heap order
|
||||
std::make_heap(this->items_.begin(), this->items_.end(), SchedulerItem::cmp);
|
||||
this->to_remove_ = 0;
|
||||
|
||||
@@ -32,7 +32,7 @@ class DashboardSettings:
|
||||
def __init__(self) -> None:
|
||||
"""Initialize the dashboard settings."""
|
||||
self.config_dir: Path = None
|
||||
self.password_hash: bytes = b""
|
||||
self.password_hash: str = ""
|
||||
self.username: str = ""
|
||||
self.using_password: bool = False
|
||||
self.on_ha_addon: bool = False
|
||||
@@ -84,14 +84,11 @@ class DashboardSettings:
|
||||
def check_password(self, username: str, password: str) -> bool:
|
||||
if not self.using_auth:
|
||||
return True
|
||||
# Compare in constant running time (to prevent timing attacks)
|
||||
username_matches = hmac.compare_digest(
|
||||
username.encode("utf-8"), self.username.encode("utf-8")
|
||||
)
|
||||
password_matches = hmac.compare_digest(
|
||||
self.password_hash, password_hash(password)
|
||||
)
|
||||
return username_matches and password_matches
|
||||
if username != self.username:
|
||||
return False
|
||||
|
||||
# Compare password in constant running time (to prevent timing attacks)
|
||||
return hmac.compare_digest(self.password_hash, password_hash(password))
|
||||
|
||||
def rel_path(self, *args: Any) -> Path:
|
||||
"""Return a path relative to the ESPHome config folder."""
|
||||
|
||||
@@ -120,11 +120,8 @@ def is_authenticated(handler: BaseHandler) -> bool:
|
||||
if auth_header := handler.request.headers.get("Authorization"):
|
||||
assert isinstance(auth_header, str)
|
||||
if auth_header.startswith("Basic "):
|
||||
try:
|
||||
auth_decoded = base64.b64decode(auth_header[6:]).decode()
|
||||
username, password = auth_decoded.split(":", 1)
|
||||
except (binascii.Error, ValueError, UnicodeDecodeError):
|
||||
return False
|
||||
auth_decoded = base64.b64decode(auth_header[6:]).decode()
|
||||
username, password = auth_decoded.split(":", 1)
|
||||
return settings.check_password(username, password)
|
||||
return handler.get_secure_cookie(AUTH_COOKIE_NAME) == COOKIE_AUTHENTICATED_YES
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import hashlib
|
||||
import io
|
||||
import logging
|
||||
from pathlib import Path
|
||||
import secrets
|
||||
import random
|
||||
import socket
|
||||
import sys
|
||||
import time
|
||||
@@ -300,8 +300,8 @@ def perform_ota(
|
||||
nonce = nonce_bytes.decode()
|
||||
_LOGGER.debug("Auth: %s Nonce is %s", hash_name, nonce)
|
||||
|
||||
# Generate cnonce matching the hash algorithm's digest size
|
||||
cnonce = secrets.token_hex(nonce_size // 2)
|
||||
# Generate cnonce
|
||||
cnonce = hash_func(str(random.random()).encode()).hexdigest()
|
||||
_LOGGER.debug("Auth: %s CNonce is %s", hash_name, cnonce)
|
||||
|
||||
send_check(sock, cnonce, "auth cnonce")
|
||||
|
||||
@@ -2906,6 +2906,7 @@ static const char *const TAG = "api.service";
|
||||
class_name = "APIServerConnection"
|
||||
hpp += "\n"
|
||||
hpp += f"class {class_name} : public {class_name}Base {{\n"
|
||||
hpp += " public:\n"
|
||||
hpp_protected = ""
|
||||
cpp += "\n"
|
||||
|
||||
@@ -2913,8 +2914,14 @@ static const char *const TAG = "api.service";
|
||||
message_auth_map: dict[str, bool] = {}
|
||||
message_conn_map: dict[str, bool] = {}
|
||||
|
||||
m = serv.method[0]
|
||||
for m in serv.method:
|
||||
func = m.name
|
||||
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_auth = get_opt(m, pb.needs_authentication, True)
|
||||
|
||||
@@ -2922,6 +2929,39 @@ static const char *const TAG = "api.service";
|
||||
message_auth_map[inp] = needs_auth
|
||||
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"
|
||||
|
||||
is_empty = inp in EMPTY_MESSAGES
|
||||
param = "" if is_empty else f"const {inp} &msg"
|
||||
arg = "" if is_empty else "msg"
|
||||
|
||||
hpp_protected += f" void {on_func}({param}) override;\n"
|
||||
if is_void:
|
||||
hpp += f" virtual void {func}({param}) = 0;\n"
|
||||
else:
|
||||
hpp += f" virtual bool send_{func}_response({param}) = 0;\n"
|
||||
|
||||
cpp += f"void {class_name}::{on_func}({param}) {{\n"
|
||||
body = ""
|
||||
if is_void:
|
||||
body += f"this->{func}({arg});\n"
|
||||
else:
|
||||
body += f"if (!this->send_{func}_response({arg})) {{\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
|
||||
# Categorize messages by their authentication requirements
|
||||
no_conn_ids: set[int] = set()
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""Tests for DashboardSettings (path resolution and authentication)."""
|
||||
"""Tests for dashboard settings Path-related functionality."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -10,7 +10,6 @@ import pytest
|
||||
|
||||
from esphome.core import CORE
|
||||
from esphome.dashboard.settings import DashboardSettings
|
||||
from esphome.dashboard.util.password import password_hash
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@@ -222,66 +221,3 @@ def test_config_path_parent_resolves_to_config_dir(tmp_path: Path) -> None:
|
||||
# Verify that CORE.config_path itself uses the sentinel file
|
||||
assert CORE.config_path.name == "___DASHBOARD_SENTINEL___.yaml"
|
||||
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,7 +2,6 @@ from __future__ import annotations
|
||||
|
||||
from argparse import Namespace
|
||||
import asyncio
|
||||
import base64
|
||||
from collections.abc import Generator
|
||||
from contextlib import asynccontextmanager
|
||||
import gzip
|
||||
@@ -1742,85 +1741,3 @@ def test_proc_on_exit_skips_when_already_closed() -> None:
|
||||
|
||||
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
|
||||
|
||||
@@ -18,8 +18,8 @@ from esphome import espota2
|
||||
from esphome.core import EsphomeError
|
||||
|
||||
# Test constants
|
||||
MOCK_MD5_CNONCE = "a" * 32 # Mock 32-char hex string from secrets.token_hex(16)
|
||||
MOCK_SHA256_CNONCE = "b" * 64 # Mock 64-char hex string from secrets.token_hex(32)
|
||||
MOCK_RANDOM_VALUE = 0.123456
|
||||
MOCK_RANDOM_BYTES = b"0.123456"
|
||||
MOCK_MD5_NONCE = b"12345678901234567890123456789012" # 32 char nonce for MD5
|
||||
MOCK_SHA256_NONCE = b"1234567890123456789012345678901234567890123456789012345678901234" # 64 char nonce for SHA256
|
||||
|
||||
@@ -55,18 +55,10 @@ def mock_time() -> Generator[None]:
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_token_hex() -> Generator[Mock]:
|
||||
"""Mock secrets.token_hex for predictable test values."""
|
||||
|
||||
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
|
||||
def mock_random() -> Generator[Mock]:
|
||||
"""Mock random for predictable test values."""
|
||||
with patch("random.random", return_value=MOCK_RANDOM_VALUE) as mock_rand:
|
||||
yield mock_rand
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@@ -244,7 +236,7 @@ def test_send_check_socket_error(mock_socket: Mock) -> None:
|
||||
|
||||
@pytest.mark.usefixtures("mock_time")
|
||||
def test_perform_ota_successful_md5_auth(
|
||||
mock_socket: Mock, mock_file: io.BytesIO, mock_token_hex: Mock
|
||||
mock_socket: Mock, mock_file: io.BytesIO, mock_random: Mock
|
||||
) -> None:
|
||||
"""Test successful OTA with MD5 authentication."""
|
||||
# Setup socket responses for recv calls
|
||||
@@ -280,11 +272,8 @@ def test_perform_ota_successful_md5_auth(
|
||||
)
|
||||
)
|
||||
|
||||
# Verify token_hex was called with MD5 digest size
|
||||
mock_token_hex.assert_called_once_with(16)
|
||||
|
||||
# Verify cnonce was sent
|
||||
cnonce = MOCK_MD5_CNONCE
|
||||
# Verify cnonce was sent (MD5 of random.random())
|
||||
cnonce = hashlib.md5(MOCK_RANDOM_BYTES).hexdigest()
|
||||
assert mock_socket.sendall.call_args_list[2] == call(cnonce.encode())
|
||||
|
||||
# Verify auth result was computed correctly
|
||||
@@ -377,7 +366,7 @@ def test_perform_ota_auth_without_password(mock_socket: Mock) -> None:
|
||||
|
||||
@pytest.mark.usefixtures("mock_time")
|
||||
def test_perform_ota_md5_auth_wrong_password(
|
||||
mock_socket: Mock, mock_file: io.BytesIO, mock_token_hex: Mock
|
||||
mock_socket: Mock, mock_file: io.BytesIO, mock_random: Mock
|
||||
) -> None:
|
||||
"""Test OTA fails when MD5 authentication is rejected due to wrong password."""
|
||||
# Setup socket responses for recv calls
|
||||
@@ -401,7 +390,7 @@ def test_perform_ota_md5_auth_wrong_password(
|
||||
|
||||
@pytest.mark.usefixtures("mock_time")
|
||||
def test_perform_ota_sha256_auth_wrong_password(
|
||||
mock_socket: Mock, mock_file: io.BytesIO, mock_token_hex: Mock
|
||||
mock_socket: Mock, mock_file: io.BytesIO, mock_random: Mock
|
||||
) -> None:
|
||||
"""Test OTA fails when SHA256 authentication is rejected due to wrong password."""
|
||||
# Setup socket responses for recv calls
|
||||
@@ -614,7 +603,7 @@ def test_progress_bar(capsys: CaptureFixture[str]) -> None:
|
||||
# Tests for SHA256 authentication
|
||||
@pytest.mark.usefixtures("mock_time")
|
||||
def test_perform_ota_successful_sha256_auth(
|
||||
mock_socket: Mock, mock_file: io.BytesIO, mock_token_hex: Mock
|
||||
mock_socket: Mock, mock_file: io.BytesIO, mock_random: Mock
|
||||
) -> None:
|
||||
"""Test successful OTA with SHA256 authentication."""
|
||||
# Setup socket responses for recv calls
|
||||
@@ -650,11 +639,8 @@ def test_perform_ota_successful_sha256_auth(
|
||||
)
|
||||
)
|
||||
|
||||
# Verify token_hex was called with SHA256 digest size
|
||||
mock_token_hex.assert_called_once_with(32)
|
||||
|
||||
# Verify cnonce was sent
|
||||
cnonce = MOCK_SHA256_CNONCE
|
||||
# Verify cnonce was sent (SHA256 of random.random())
|
||||
cnonce = hashlib.sha256(MOCK_RANDOM_BYTES).hexdigest()
|
||||
assert mock_socket.sendall.call_args_list[2] == call(cnonce.encode())
|
||||
|
||||
# Verify auth result was computed correctly with SHA256
|
||||
@@ -668,7 +654,7 @@ def test_perform_ota_successful_sha256_auth(
|
||||
|
||||
@pytest.mark.usefixtures("mock_time")
|
||||
def test_perform_ota_sha256_fallback_to_md5(
|
||||
mock_socket: Mock, mock_file: io.BytesIO, mock_token_hex: Mock
|
||||
mock_socket: Mock, mock_file: io.BytesIO, mock_random: Mock
|
||||
) -> None:
|
||||
"""Test SHA256-capable client falls back to MD5 for compatibility."""
|
||||
# This test verifies the temporary backward compatibility
|
||||
@@ -706,8 +692,7 @@ def test_perform_ota_sha256_fallback_to_md5(
|
||||
)
|
||||
|
||||
# But authentication was done with MD5
|
||||
mock_token_hex.assert_called_once_with(16)
|
||||
cnonce = MOCK_MD5_CNONCE
|
||||
cnonce = hashlib.md5(MOCK_RANDOM_BYTES).hexdigest()
|
||||
expected_hash = hashlib.md5()
|
||||
expected_hash.update(b"testpass")
|
||||
expected_hash.update(MOCK_MD5_NONCE)
|
||||
|
||||
Reference in New Issue
Block a user